Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/yaml_format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ a list of waveforms, or a single number (float or integer).

Refer to the :ref:`Available Tendencies <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
Expand Down
30 changes: 30 additions & 0 deletions tests/test_waveform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 [
Expand Down
84 changes: 39 additions & 45 deletions waveform_editor/waveform.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
}


tendency_map = {
TENDENCY_MAP = {
"linear": LinearTendency,
"sine-wave": SineWaveTendency,
"sine": SineWaveTendency,
Expand All @@ -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__(
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)
Loading