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..274903a4 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,34 @@ 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_to": 5, "user_duration": 2}, LinearTendency), + ({"user_duration": 2}, LinearTendency), + ], + ids=["value", "time+value", "waveform", "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..a3f8c60c 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, @@ -45,6 +45,28 @@ "repeat": RepeatTendency, } +INFERRED_TYPE_BY_KEY = { + "user_time": PiecewiseLinearTendency, + "user_value": ConstantTendency, + "user_waveform": RepeatTendency, +} + + +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 class. + """ + for key, tendency_class in INFERRED_TYPE_BY_KEY.items(): + if key in entry: + return tendency_class + return LinearTendency + class Waveform(BaseWaveform): def __init__( @@ -240,44 +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" - - # 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}" - 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. @@ -305,10 +289,20 @@ def _handle_tendency(self, entry): Returns: The created tendency or None, if the tendency cannot be created """ - if self._has_type_error(entry): - return None + # If no type is given, infer it from the entry's keys + if "user_type" not in entry: + 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") + 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()) + 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)