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,
Comment thread
LourensVeen marked this conversation as resolved.
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