From a958c484057f2ea259ba7a4bb127a75f501bdc46 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Thu, 30 Jul 2026 16:50:28 +0200 Subject: [PATCH 1/7] infer tendency type --- docs/source/yaml_format.rst | 4 ++++ tests/test_waveform.py | 31 +++++++++++++++++++++++++++++++ waveform_editor/waveform.py | 35 +++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/docs/source/yaml_format.rst b/docs/source/yaml_format.rst index 72fd8d7c..abb1e592 100644 --- a/docs/source/yaml_format.rst +++ b/docs/source/yaml_format.rst @@ -108,6 +108,10 @@ a list of waveforms, or a single number (float or integer). Refer to the :ref:`Available Tendencies ` documentation for details on the different tendency types and their parameters. + .. note:: + The ``type`` may be omitted. The type of the tendency is then inferred from + the other keys present, if possible. + 2. **Constant Value:** A simple number (integer or float) defines a constant waveform over time. .. code-block:: yaml diff --git a/tests/test_waveform.py b/tests/test_waveform.py index 9942af6f..35477962 100644 --- a/tests/test_waveform.py +++ b/tests/test_waveform.py @@ -4,6 +4,8 @@ from waveform_editor.tendencies.constant import ConstantTendency from waveform_editor.tendencies.linear import LinearTendency from waveform_editor.tendencies.periodic.sine_wave import SineWaveTendency +from waveform_editor.tendencies.piecewise import PiecewiseLinearTendency +from waveform_editor.tendencies.repeat import RepeatTendency from waveform_editor.tendencies.smooth import SmoothTendency from waveform_editor.waveform import Waveform @@ -16,6 +18,35 @@ def test_empty(): assert waveform.annotations == [] +@pytest.mark.parametrize( + "entry,expected_type", + [ + ({"user_value": 3, "user_duration": 2}, ConstantTendency), + ( + {"user_time": [0, 1, 2], "user_value": [0, 1, 0]}, + PiecewiseLinearTendency, + ), + ( + { + "user_waveform": [ + {"user_type": "constant", "user_value": 1, "user_duration": 1} + ], + "user_duration": 2, + }, + RepeatTendency, + ), + ({"user_base": 5, "user_amplitude": 2, "user_duration": 2}, SineWaveTendency), + ({"user_to": 5, "user_duration": 2}, LinearTendency), + ({"user_duration": 2}, LinearTendency), + ], + ids=["value", "time+value", "waveform", "periodic", "to-only", "no-keys"], +) +def test_infer_tendency_type(entry, expected_type): + waveform = Waveform(waveform=[entry], name="w") + assert not waveform.annotations + assert type(waveform.tendencies[0]) is expected_type + + @pytest.fixture def waveform_list(): return [ diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 7a7a69b7..02e4850a 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -45,6 +45,33 @@ "repeat": RepeatTendency, } +INFERRED_TYPE_BY_KEY = { + "user_time": "piecewise", + "user_value": "constant", + "user_waveform": "repeat", + "user_base": "sine", + "user_amplitude": "sine", + "user_min": "sine", + "user_max": "sine", + "user_phase": "sine", +} + + +def _infer_tendency_type(entry): + """Infer a tendency's type from keys in the tendency entry, defaulting to + linear if nothing distinctive is present. + + Args: + entry: Entry in the YAML file. + + Returns: + The inferred tendency type. + """ + for key, tendency_type in INFERRED_TYPE_BY_KEY.items(): + if key in entry: + return tendency_type + return "linear" + class Waveform(BaseWaveform): def __init__( @@ -252,10 +279,6 @@ def _has_type_error(self, entry): line_number = entry.get("line_number", 0) ignore_msg = "This tendency will be ignored.\n" - # If no type is given, take linear as default - if "user_type" not in entry: - entry["user_type"] = "linear" - tendency_type = entry.get("user_type", None) if tendency_type is None: error_msg = f"The tendency type cannot be empty.\n{ignore_msg}" @@ -305,6 +328,10 @@ def _handle_tendency(self, entry): Returns: The created tendency or None, if the tendency cannot be created """ + # If no type is given, infer it from the entry's keys + if "user_type" not in entry: + entry["user_type"] = _infer_tendency_type(entry) + if self._has_type_error(entry): return None else: From a4bcf0044b8c2b7afa5a6e135f178026fdb64fe4 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Fri, 31 Jul 2026 09:37:32 +0200 Subject: [PATCH 2/7] map to tendency class instead --- waveform_editor/waveform.py | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 02e4850a..25d95b87 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -29,7 +29,7 @@ } -tendency_map = { +TENDENCY_MAP = { "linear": LinearTendency, "sine-wave": SineWaveTendency, "sine": SineWaveTendency, @@ -46,31 +46,31 @@ } INFERRED_TYPE_BY_KEY = { - "user_time": "piecewise", - "user_value": "constant", - "user_waveform": "repeat", - "user_base": "sine", - "user_amplitude": "sine", - "user_min": "sine", - "user_max": "sine", - "user_phase": "sine", + "user_time": PiecewiseLinearTendency, + "user_value": ConstantTendency, + "user_waveform": RepeatTendency, + "user_base": SineWaveTendency, + "user_amplitude": SineWaveTendency, + "user_min": SineWaveTendency, + "user_max": SineWaveTendency, + "user_phase": SineWaveTendency, } -def _infer_tendency_type(entry): - """Infer a tendency's type from keys in the tendency entry, defaulting to - linear if nothing distinctive is present. +def _infer_tendency_class(entry): + """Infer a tendency's class from keys in the tendency entry, defaulting to + linear tendency if no distinctive keys are present. Args: entry: Entry in the YAML file. Returns: - The inferred tendency type. + The inferred tendency class. """ - for key, tendency_type in INFERRED_TYPE_BY_KEY.items(): + for key, tendency_class in INFERRED_TYPE_BY_KEY.items(): if key in entry: - return tendency_type - return "linear" + return tendency_class + return LinearTendency class Waveform(BaseWaveform): @@ -290,8 +290,8 @@ def _has_type_error(self, entry): self.annotations.add(line_number, error_msg) return True - if tendency_type not in tendency_map: - suggestion = self.annotations.suggest(tendency_type, tendency_map.keys()) + if tendency_type not in TENDENCY_MAP: + suggestion = self.annotations.suggest(tendency_type, TENDENCY_MAP.keys()) error_msg = ( f"Unsupported tendency type: '{tendency_type}'. {suggestion}" @@ -330,12 +330,12 @@ def _handle_tendency(self, entry): """ # If no type is given, infer it from the entry's keys if "user_type" not in entry: - entry["user_type"] = _infer_tendency_type(entry) + return _infer_tendency_class(entry)(**entry) if self._has_type_error(entry): return None else: tendency_type = entry.pop("user_type") - tendency_class = tendency_map[tendency_type] + tendency_class = TENDENCY_MAP[tendency_type] tendency = tendency_class(**entry) return tendency From e9dc1ffce4e616e78a9f051da011ac0f02ab5546 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Wed, 5 Aug 2026 16:03:31 +0200 Subject: [PATCH 3/7] remove sine wave as default --- tests/test_waveform.py | 3 +-- waveform_editor/waveform.py | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/test_waveform.py b/tests/test_waveform.py index 35477962..274903a4 100644 --- a/tests/test_waveform.py +++ b/tests/test_waveform.py @@ -35,11 +35,10 @@ def test_empty(): }, RepeatTendency, ), - ({"user_base": 5, "user_amplitude": 2, "user_duration": 2}, SineWaveTendency), ({"user_to": 5, "user_duration": 2}, LinearTendency), ({"user_duration": 2}, LinearTendency), ], - ids=["value", "time+value", "waveform", "periodic", "to-only", "no-keys"], + ids=["value", "time+value", "waveform", "to-only", "no-keys"], ) def test_infer_tendency_type(entry, expected_type): waveform = Waveform(waveform=[entry], name="w") diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 25d95b87..a3c2d4b1 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -49,11 +49,6 @@ "user_time": PiecewiseLinearTendency, "user_value": ConstantTendency, "user_waveform": RepeatTendency, - "user_base": SineWaveTendency, - "user_amplitude": SineWaveTendency, - "user_min": SineWaveTendency, - "user_max": SineWaveTendency, - "user_phase": SineWaveTendency, } From f3d42a60adb49dac835611041cdd0f8e03521d8f Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Wed, 5 Aug 2026 16:37:51 +0200 Subject: [PATCH 4/7] cleanup tendency inference --- waveform_editor/waveform.py | 68 ++++++++++++++----------------------- 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index a3c2d4b1..5fbdfa8a 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -262,40 +262,6 @@ def update_annotations(self, event=None): if tendency.annotations and tendency.annotations not in self.annotations: self.annotations.add_annotations(tendency.annotations) - def _has_type_error(self, entry): - """Check if the YAML entry contains an error related to the tendency type. - - Args: - entry: Entry in the YAML file. - - Returns: - True if there is a type error, False otherwise. - """ - line_number = entry.get("line_number", 0) - ignore_msg = "This tendency will be ignored.\n" - - tendency_type = entry.get("user_type", None) - if tendency_type is None: - error_msg = f"The tendency type cannot be empty.\n{ignore_msg}" - self.annotations.add(line_number, error_msg) - return True - - if not isinstance(tendency_type, str): - error_msg = f"The tendency type should be of type 'string'.\n{ignore_msg}" - self.annotations.add(line_number, error_msg) - return True - - if tendency_type not in TENDENCY_MAP: - suggestion = self.annotations.suggest(tendency_type, TENDENCY_MAP.keys()) - - error_msg = ( - f"Unsupported tendency type: '{tendency_type}'. {suggestion}" - f"{ignore_msg}" - ) - self.annotations.add(line_number, error_msg) - return True - return False - def get_yaml_string(self): """Converts the internal YAML waveform description to a string. @@ -325,12 +291,30 @@ def _handle_tendency(self, entry): """ # If no type is given, infer it from the entry's keys if "user_type" not in entry: - return _infer_tendency_class(entry)(**entry) - - if self._has_type_error(entry): - return None + tendency_class = _infer_tendency_class(entry) else: - tendency_type = entry.pop("user_type") - tendency_class = TENDENCY_MAP[tendency_type] - tendency = tendency_class(**entry) - return tendency + user_type = entry.pop("user_type") + tendency_class = ( + TENDENCY_MAP.get(user_type) if isinstance(user_type, str) else None + ) + if tendency_class is None: + line_number = entry.get("line_number", 0) + ignore_msg = "This tendency will be ignored.\n" + if user_type is None: + error_msg = f"The tendency type cannot be empty.\n{ignore_msg}" + elif not isinstance(user_type, str): + error_msg = ( + f"The tendency type should be of type 'string'.\n{ignore_msg}" + ) + else: + suggestion = self.annotations.suggest( + user_type, TENDENCY_MAP.keys() + ) + error_msg = ( + f"Unsupported tendency type: '{user_type}'. " + f"{suggestion}{ignore_msg}" + ) + self.annotations.add(line_number, error_msg) + return None + + return tendency_class(**entry) From 4f4d5086d2dded4929fb2fc8639b060f1d9cda46 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Thu, 6 Aug 2026 09:13:07 +0200 Subject: [PATCH 5/7] update type error --- waveform_editor/waveform.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 5fbdfa8a..137c6187 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -302,13 +302,9 @@ def _handle_tendency(self, entry): ignore_msg = "This tendency will be ignored.\n" if user_type is None: error_msg = f"The tendency type cannot be empty.\n{ignore_msg}" - elif not isinstance(user_type, str): - error_msg = ( - f"The tendency type should be of type 'string'.\n{ignore_msg}" - ) else: suggestion = self.annotations.suggest( - user_type, TENDENCY_MAP.keys() + str(user_type), TENDENCY_MAP.keys() ) error_msg = ( f"Unsupported tendency type: '{user_type}'. " From 5da2910069f68838eb9e246e6d8bf3a747c15ddd Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Thu, 6 Aug 2026 09:57:08 +0200 Subject: [PATCH 6/7] simplify type handling --- waveform_editor/waveform.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 137c6187..5821eb73 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -293,24 +293,15 @@ def _handle_tendency(self, entry): if "user_type" not in entry: tendency_class = _infer_tendency_class(entry) else: - user_type = entry.pop("user_type") - tendency_class = ( - TENDENCY_MAP.get(user_type) if isinstance(user_type, str) else None - ) + user_type = str(entry.pop("user_type")) + tendency_class = TENDENCY_MAP.get(user_type) if tendency_class is None: - line_number = entry.get("line_number", 0) - ignore_msg = "This tendency will be ignored.\n" - if user_type is None: - error_msg = f"The tendency type cannot be empty.\n{ignore_msg}" - else: - suggestion = self.annotations.suggest( - str(user_type), TENDENCY_MAP.keys() - ) - error_msg = ( - f"Unsupported tendency type: '{user_type}'. " - f"{suggestion}{ignore_msg}" - ) - self.annotations.add(line_number, error_msg) + suggestion = self.annotations.suggest(user_type, TENDENCY_MAP.keys()) + error_msg = ( + f"Unsupported tendency type: '{user_type}'. " + f"{suggestion}This tendency will be ignored.\n" + ) + self.annotations.add(entry.get("line_number", 0), error_msg) return None return tendency_class(**entry) From 82b22f0a026bddf97e3208fb5482bdab354a6815 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Thu, 6 Aug 2026 11:31:04 +0200 Subject: [PATCH 7/7] Update waveform_editor/waveform.py Co-authored-by: Maarten Sebregts <110895564+maarten-ic@users.noreply.github.com> --- waveform_editor/waveform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waveform_editor/waveform.py b/waveform_editor/waveform.py index 5821eb73..a3f8c60c 100644 --- a/waveform_editor/waveform.py +++ b/waveform_editor/waveform.py @@ -293,7 +293,8 @@ def _handle_tendency(self, entry): if "user_type" not in entry: tendency_class = _infer_tendency_class(entry) else: - user_type = str(entry.pop("user_type")) + user_type = entry.pop("user_type") + user_type = "" if user_type is None else str(user_type) tendency_class = TENDENCY_MAP.get(user_type) if tendency_class is None: suggestion = self.annotations.suggest(user_type, TENDENCY_MAP.keys())