Skip to content
14 changes: 14 additions & 0 deletions ymmsl/v0_2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
SupportedSetting,
SupportedSettings,
)
from ymmsl.v0_2.timeline_resolver import (
ConduitTimelineError,
CyclicDependency,
InconsistentTimelines,
ResolveTimelineException,
TooManyReducerFilters,
resolve_timelines,
)

__all__ = [
"BaseEnv",
Expand All @@ -38,13 +46,16 @@
"Ports",
"Conduit",
"ConduitFilter",
"ConduitTimelineError",
"Configuration",
"CyclicDependency",
"Document",
"ExecutionModel",
"Identifier",
"Implementation",
"ImportKind",
"ImportStatement",
"InconsistentTimelines",
"KeepsStateForNextUse",
"Model",
"MPICoresResReq",
Expand All @@ -56,6 +67,8 @@
"Reference",
"ReferencePart",
"resolve",
"resolve_timelines",
"ResolveTimelineException",
"ResourceRequirements",
"Settings",
"SettingType",
Expand All @@ -64,4 +77,5 @@
"SupportedSettings",
"ThreadedResReq",
"Timeline",
"TooManyReducerFilters",
]
6 changes: 5 additions & 1 deletion ymmsl/v0_2/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ymmsl.util import remove_trailing_whitespace
from ymmsl.v0_2.identity import Reference
from ymmsl.v0_2.ports import Ports
from ymmsl.v0_2.ports import Ports, Timeline


class Component:
Expand All @@ -30,6 +30,9 @@ class Component:
implementation: A Model or Program implementing this component
optional: Whether this component is optional
multiplicity: The shape of the set of instances
timeline: The resolved (absolute) timeline for this component inside the model.
This will be ``None`` until resolved by
:meth:`~ymmsl.v0_2.resolve_timelines()`.
"""

def __init__(
Expand All @@ -56,6 +59,7 @@ def __init__(
self.ports = ports
self.description = description
self.optional = optional
self.timeline: Optional[Timeline] = None

if implementation is not None:
self.implementation: Optional[Reference] = Reference(implementation)
Expand Down
34 changes: 33 additions & 1 deletion ymmsl/v0_2/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def make_new_reference(x: Union[str, Reference]) -> Reference:

self._parts = list(map(make_new_reference, parts))

elif isinstance(timeline, list):
else:
self.absolute = absolute
self._parts = list(map(make_new_reference, timeline))

Expand Down Expand Up @@ -109,6 +109,10 @@ def __getitem__(self, index: int) -> Reference:
"""Return the index'th item in the timeline."""
return self._parts[index]

def __iter__(self) -> Iterator[Reference]:
"""Iterate over the timeline parts."""
yield from self._parts

def __add__(self, other: Any) -> "Timeline":
"""Concatenate this timeline with another (relative!) Timeline."""
if isinstance(other, Timeline):
Expand All @@ -119,6 +123,25 @@ def __add__(self, other: Any) -> "Timeline":
return Timeline(self._parts + other._parts, self.absolute)
return NotImplemented

@property
def parent(self) -> "Timeline | None":
"""Get the parent of this timeline. Returns None when there is no parent."""
if not self._parts:
return None
return Timeline(self._parts[:-1], self.absolute)

def relative_to(self, other: "Timeline") -> "Timeline":
"""Compute a version of this timeline relative to `other`.

Both timelines must be absolute, and the other timeline must be a parent
timeline of this one.
"""
if not self.absolute or not other.absolute:
raise ValueError("Both timelines must be absolute")
if self._parts[: len(other)] != other._parts:
raise ValueError(f"{self} is not a subtimeline of {other}")
return Timeline(self._parts[len(other) :], False)


class Port:
"""A port on a component.
Expand Down Expand Up @@ -285,6 +308,15 @@ def __iter__(self) -> Iterator[Identifier]:
"""Iterate through the ports' names."""
yield from self._ports

def items(self) -> Iterator[tuple[Identifier, Port]]:
yield from self._ports.items()

def keys(self) -> Iterator[Identifier]:
yield from self._ports.keys()

def values(self) -> Iterator[Port]:
yield from self._ports.values()

def sending_port_names(self) -> List[Identifier]:
"""Return the names of all the sending ports.

Expand Down
21 changes: 21 additions & 0 deletions ymmsl/v0_2/tests/test_ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,27 @@ def test_timeline_concatenate_empty() -> None:
assert tl4 == tl1


def test_timeline_parent() -> None:
assert Timeline("").parent is None
assert Timeline(":").parent is None

assert Timeline("a:b").parent == Timeline("a")
assert Timeline(":a:b").parent == Timeline(":a")


def test_timeline_relative_to() -> None:
assert Timeline(":a:b:c").relative_to(Timeline(":a:b")) == Timeline("c")
assert Timeline(":a:b:c").relative_to(Timeline(":a")) == Timeline("b:c")

with pytest.raises(ValueError, match="absolute"):
Timeline("a:b").relative_to(Timeline(":a"))
with pytest.raises(ValueError, match="absolute"):
Timeline(":a:b").relative_to(Timeline("a"))

with pytest.raises(ValueError, match="subtimeline"):
Timeline(":a:b").relative_to(Timeline(":b"))


def test_create_empty_ports() -> None:
p = Ports()
assert len(p._ports) == 0
Expand Down
151 changes: 151 additions & 0 deletions ymmsl/v0_2/tests/test_timeline_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from pathlib import Path

import pytest

import ymmsl
from ymmsl.v0_2 import ConduitFilter, Configuration, Timeline
from ymmsl.v0_2 import Reference as Ref
from ymmsl.v0_2.timeline_resolver import (
ROOT_TIMELINE,
ConduitTimelineError,
CyclicDependency,
InconsistentTimelines,
TooManyReducerFilters,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have too many repeaters as well? If I connect an init component to a micro with two repeaters? Or would that come out as InconsistentTimelines? But there aren't two known timelines in this case, so that's not what I would expect...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would indeed come out as InconsistentTimelines (which then suggests to add a reducer, or remove a repeater).

Note that TooManyReducerFilters only triggers when trying to reduce "above" the root timeline. You can also have too many reducers and end up with an InconsistentTimelines error (which will suggest to remove a reducer or add a repeater).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to rename this exception to ReducerFilterOnRootTimeline (or similar) to avoid this confusion?

resolve_timelines,
)


@pytest.fixture()
def timelines_configuration() -> Configuration:
return ymmsl.load_as(
Configuration, Path(__file__).parent / "ymmsl1/timelines.ymmsl"
)


def test_consistent_configuration(timelines_configuration: Configuration) -> None:
timelines_configuration.check_consistent()


def test_dispatch(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("dispatch")]
resolve_timelines(model)
assert model.components[Ref("first")].timeline == ROOT_TIMELINE
assert model.components[Ref("second")].timeline == ROOT_TIMELINE


def test_macromicro(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("macromicro")]
resolve_timelines(model)
assert model.components[Ref("macro")].timeline == ROOT_TIMELINE
assert model.components[Ref("micro")].timeline == Timeline(":macro")

# Check that ports have the correct relative timelines:
macro = model.components[Ref("macro")]
assert macro.ports["init"].timeline == Timeline("")
assert macro.ports["out"].timeline == Timeline("macro")
assert macro.ports["in"].timeline == Timeline("macro")

for micro_port in model.components[Ref("micro")].ports.values():
assert micro_port.timeline == Timeline("")


def test_cycle(timelines_configuration: Configuration) -> None:
with pytest.raises(CyclicDependency, match="cycle in model"):
resolve_timelines(timelines_configuration.models[Ref("cycle")])


def test_reducer(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("reducer")]
resolve_timelines(model)
assert model.components[Ref("first")].timeline == ROOT_TIMELINE
assert model.components[Ref("second")].timeline == ROOT_TIMELINE


def test_only_reducer(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("reducer")]
# Remove the conduit first.final -> second.init1 and keep only the reduced conduit
del model.conduits[0]
assert model.conduits[0].filters == [ConduitFilter("last")]
resolve_timelines(model)
assert model.components[Ref("first")].timeline == ROOT_TIMELINE
assert model.components[Ref("second")].timeline == ROOT_TIMELINE


def test_too_many_reducers(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("reducer")]
model.conduits[-1].filters.append(model.conduits[-1].filters[0])
with pytest.raises(TooManyReducerFilters, match="too many reducer filters"):
resolve_timelines(model)


def test_inconsistent_timelines(timelines_configuration: Configuration) -> None:
with pytest.raises(InconsistentTimelines):
resolve_timelines(timelines_configuration.models[Ref("inconsistent")])


def test_repeaters(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("repeaters")]
resolve_timelines(model)
assert model.components[Ref("macro")].timeline == ROOT_TIMELINE
assert model.components[Ref("meso")].timeline == Timeline(":macro")
assert model.components[Ref("micro")].timeline == Timeline(":macro:meso")


def test_too_many_repeaters(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("repeaters")]
model.conduits[-2].filters.append(ConduitFilter.REPEAT)
with pytest.raises(ConduitTimelineError, match="remove a repeater"):
resolve_timelines(model)


def test_too_few_repeaters(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("repeaters")]
model.conduits[-1].filters.pop()
with pytest.raises(ConduitTimelineError, match="add a repeater"):
resolve_timelines(model)


def test_repeater_and_too_many_reducers(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("repeaters")]
model.conduits[-1].filters.insert(0, ConduitFilter.LAST)
with pytest.raises(TooManyReducerFilters, match="too many reducer filters"):
resolve_timelines(model)


def test_repeater_after_reducer(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("repeater_reducer")]
resolve_timelines(model)
assert model.components[Ref("macro1")].timeline == ROOT_TIMELINE
assert model.components[Ref("macro2")].timeline == ROOT_TIMELINE
assert model.components[Ref("micro1")].timeline == Timeline(":macro1")
assert model.components[Ref("micro2")].timeline == Timeline(":macro2")

# Remove filters on the last conduit to make the incoming timelines inconsistent
model.conduits[-1].filters = []
with pytest.raises(InconsistentTimelines, match="different timelines"):
resolve_timelines(model)


def test_repeater_after_reducer_error(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("repeater_reducer_error")]
with pytest.raises(ConduitTimelineError, match="remove a repeater and reducer"):
resolve_timelines(model)
model.conduits[-1].filters = []
resolve_timelines(model)
assert model.components[Ref("macro")].timeline == ROOT_TIMELINE
assert model.components[Ref("micro1")].timeline == Timeline(":macro")
assert model.components[Ref("micro2")].timeline == Timeline(":macro")


def test_inconsistent_interact(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("inconsistent_interact")]
with pytest.raises(ConduitTimelineError, match="missing timeline annotations"):
resolve_timelines(model)
model.components[Ref("B")].ports["out"].timeline = Timeline("A")
model.components[Ref("B")].ports["in"].timeline = Timeline("A")
resolve_timelines(model)


def test_model_ports(timelines_configuration: Configuration) -> None:
model = timelines_configuration.models[Ref("model_ports")]
resolve_timelines(model)
Loading