Skip to content

Commit c299054

Browse files
authored
Merge pull request #62 from multiscale/feature/resolve-timelines
Feature/resolve timelines
2 parents 3e002cb + 29bb5d3 commit c299054

7 files changed

Lines changed: 766 additions & 2 deletions

File tree

ymmsl/v0_2/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
SupportedSetting,
2828
SupportedSettings,
2929
)
30+
from ymmsl.v0_2.timeline_resolver import (
31+
ConduitTimelineError,
32+
CyclicDependency,
33+
InconsistentTimelines,
34+
ResolveTimelineException,
35+
TooManyReducerFilters,
36+
resolve_timelines,
37+
)
3038

3139
__all__ = [
3240
"BaseEnv",
@@ -38,13 +46,16 @@
3846
"Ports",
3947
"Conduit",
4048
"ConduitFilter",
49+
"ConduitTimelineError",
4150
"Configuration",
51+
"CyclicDependency",
4252
"Document",
4353
"ExecutionModel",
4454
"Identifier",
4555
"Implementation",
4656
"ImportKind",
4757
"ImportStatement",
58+
"InconsistentTimelines",
4859
"KeepsStateForNextUse",
4960
"Model",
5061
"MPICoresResReq",
@@ -56,6 +67,8 @@
5667
"Reference",
5768
"ReferencePart",
5869
"resolve",
70+
"resolve_timelines",
71+
"ResolveTimelineException",
5972
"ResourceRequirements",
6073
"Settings",
6174
"SettingType",
@@ -64,4 +77,5 @@
6477
"SupportedSettings",
6578
"ThreadedResReq",
6679
"Timeline",
80+
"TooManyReducerFilters",
6781
]

ymmsl/v0_2/component.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ymmsl.util import remove_trailing_whitespace
77
from ymmsl.v0_2.identity import Reference
8-
from ymmsl.v0_2.ports import Ports
8+
from ymmsl.v0_2.ports import Ports, Timeline
99

1010

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

3538
def __init__(
@@ -56,6 +59,7 @@ def __init__(
5659
self.ports = ports
5760
self.description = description
5861
self.optional = optional
62+
self.timeline: Optional[Timeline] = None
5963

6064
if implementation is not None:
6165
self.implementation: Optional[Reference] = Reference(implementation)

ymmsl/v0_2/ports.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def make_new_reference(x: Union[str, Reference]) -> Reference:
7676

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

79-
elif isinstance(timeline, list):
79+
else:
8080
self.absolute = absolute
8181
self._parts = list(map(make_new_reference, timeline))
8282

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

112+
def __iter__(self) -> Iterator[Reference]:
113+
"""Iterate over the timeline parts."""
114+
yield from self._parts
115+
112116
def __add__(self, other: Any) -> "Timeline":
113117
"""Concatenate this timeline with another (relative!) Timeline."""
114118
if isinstance(other, Timeline):
@@ -119,6 +123,25 @@ def __add__(self, other: Any) -> "Timeline":
119123
return Timeline(self._parts + other._parts, self.absolute)
120124
return NotImplemented
121125

126+
@property
127+
def parent(self) -> "Timeline | None":
128+
"""Get the parent of this timeline. Returns None when there is no parent."""
129+
if not self._parts:
130+
return None
131+
return Timeline(self._parts[:-1], self.absolute)
132+
133+
def relative_to(self, other: "Timeline") -> "Timeline":
134+
"""Compute a version of this timeline relative to `other`.
135+
136+
Both timelines must be absolute, and the other timeline must be a parent
137+
timeline of this one.
138+
"""
139+
if not self.absolute or not other.absolute:
140+
raise ValueError("Both timelines must be absolute")
141+
if self._parts[: len(other)] != other._parts:
142+
raise ValueError(f"{self} is not a subtimeline of {other}")
143+
return Timeline(self._parts[len(other) :], False)
144+
122145

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

311+
def items(self) -> Iterator[tuple[Identifier, Port]]:
312+
yield from self._ports.items()
313+
314+
def keys(self) -> Iterator[Identifier]:
315+
yield from self._ports.keys()
316+
317+
def values(self) -> Iterator[Port]:
318+
yield from self._ports.values()
319+
288320
def sending_port_names(self) -> List[Identifier]:
289321
"""Return the names of all the sending ports.
290322

ymmsl/v0_2/tests/test_ports.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,27 @@ def test_timeline_concatenate_empty() -> None:
125125
assert tl4 == tl1
126126

127127

128+
def test_timeline_parent() -> None:
129+
assert Timeline("").parent is None
130+
assert Timeline(":").parent is None
131+
132+
assert Timeline("a:b").parent == Timeline("a")
133+
assert Timeline(":a:b").parent == Timeline(":a")
134+
135+
136+
def test_timeline_relative_to() -> None:
137+
assert Timeline(":a:b:c").relative_to(Timeline(":a:b")) == Timeline("c")
138+
assert Timeline(":a:b:c").relative_to(Timeline(":a")) == Timeline("b:c")
139+
140+
with pytest.raises(ValueError, match="absolute"):
141+
Timeline("a:b").relative_to(Timeline(":a"))
142+
with pytest.raises(ValueError, match="absolute"):
143+
Timeline(":a:b").relative_to(Timeline("a"))
144+
145+
with pytest.raises(ValueError, match="subtimeline"):
146+
Timeline(":a:b").relative_to(Timeline(":b"))
147+
148+
128149
def test_create_empty_ports() -> None:
129150
p = Ports()
130151
assert len(p._ports) == 0
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
import ymmsl
6+
from ymmsl.v0_2 import ConduitFilter, Configuration, Timeline
7+
from ymmsl.v0_2 import Reference as Ref
8+
from ymmsl.v0_2.timeline_resolver import (
9+
ROOT_TIMELINE,
10+
ConduitTimelineError,
11+
CyclicDependency,
12+
InconsistentTimelines,
13+
TooManyReducerFilters,
14+
resolve_timelines,
15+
)
16+
17+
18+
@pytest.fixture()
19+
def timelines_configuration() -> Configuration:
20+
return ymmsl.load_as(
21+
Configuration, Path(__file__).parent / "ymmsl1/timelines.ymmsl"
22+
)
23+
24+
25+
def test_consistent_configuration(timelines_configuration: Configuration) -> None:
26+
timelines_configuration.check_consistent()
27+
28+
29+
def test_dispatch(timelines_configuration: Configuration) -> None:
30+
model = timelines_configuration.models[Ref("dispatch")]
31+
resolve_timelines(model)
32+
assert model.components[Ref("first")].timeline == ROOT_TIMELINE
33+
assert model.components[Ref("second")].timeline == ROOT_TIMELINE
34+
35+
36+
def test_macromicro(timelines_configuration: Configuration) -> None:
37+
model = timelines_configuration.models[Ref("macromicro")]
38+
resolve_timelines(model)
39+
assert model.components[Ref("macro")].timeline == ROOT_TIMELINE
40+
assert model.components[Ref("micro")].timeline == Timeline(":macro")
41+
42+
# Check that ports have the correct relative timelines:
43+
macro = model.components[Ref("macro")]
44+
assert macro.ports["init"].timeline == Timeline("")
45+
assert macro.ports["out"].timeline == Timeline("macro")
46+
assert macro.ports["in"].timeline == Timeline("macro")
47+
48+
for micro_port in model.components[Ref("micro")].ports.values():
49+
assert micro_port.timeline == Timeline("")
50+
51+
52+
def test_cycle(timelines_configuration: Configuration) -> None:
53+
with pytest.raises(CyclicDependency, match="cycle in model"):
54+
resolve_timelines(timelines_configuration.models[Ref("cycle")])
55+
56+
57+
def test_reducer(timelines_configuration: Configuration) -> None:
58+
model = timelines_configuration.models[Ref("reducer")]
59+
resolve_timelines(model)
60+
assert model.components[Ref("first")].timeline == ROOT_TIMELINE
61+
assert model.components[Ref("second")].timeline == ROOT_TIMELINE
62+
63+
64+
def test_only_reducer(timelines_configuration: Configuration) -> None:
65+
model = timelines_configuration.models[Ref("reducer")]
66+
# Remove the conduit first.final -> second.init1 and keep only the reduced conduit
67+
del model.conduits[0]
68+
assert model.conduits[0].filters == [ConduitFilter("last")]
69+
resolve_timelines(model)
70+
assert model.components[Ref("first")].timeline == ROOT_TIMELINE
71+
assert model.components[Ref("second")].timeline == ROOT_TIMELINE
72+
73+
74+
def test_too_many_reducers(timelines_configuration: Configuration) -> None:
75+
model = timelines_configuration.models[Ref("reducer")]
76+
model.conduits[-1].filters.append(model.conduits[-1].filters[0])
77+
with pytest.raises(TooManyReducerFilters, match="too many reducer filters"):
78+
resolve_timelines(model)
79+
80+
81+
def test_inconsistent_timelines(timelines_configuration: Configuration) -> None:
82+
with pytest.raises(InconsistentTimelines):
83+
resolve_timelines(timelines_configuration.models[Ref("inconsistent")])
84+
85+
86+
def test_repeaters(timelines_configuration: Configuration) -> None:
87+
model = timelines_configuration.models[Ref("repeaters")]
88+
resolve_timelines(model)
89+
assert model.components[Ref("macro")].timeline == ROOT_TIMELINE
90+
assert model.components[Ref("meso")].timeline == Timeline(":macro")
91+
assert model.components[Ref("micro")].timeline == Timeline(":macro:meso")
92+
93+
94+
def test_too_many_repeaters(timelines_configuration: Configuration) -> None:
95+
model = timelines_configuration.models[Ref("repeaters")]
96+
model.conduits[-2].filters.append(ConduitFilter.REPEAT)
97+
with pytest.raises(ConduitTimelineError, match="remove a repeater"):
98+
resolve_timelines(model)
99+
100+
101+
def test_too_few_repeaters(timelines_configuration: Configuration) -> None:
102+
model = timelines_configuration.models[Ref("repeaters")]
103+
model.conduits[-1].filters.pop()
104+
with pytest.raises(ConduitTimelineError, match="add a repeater"):
105+
resolve_timelines(model)
106+
107+
108+
def test_repeater_and_too_many_reducers(timelines_configuration: Configuration) -> None:
109+
model = timelines_configuration.models[Ref("repeaters")]
110+
model.conduits[-1].filters.insert(0, ConduitFilter.LAST)
111+
with pytest.raises(TooManyReducerFilters, match="too many reducer filters"):
112+
resolve_timelines(model)
113+
114+
115+
def test_repeater_after_reducer(timelines_configuration: Configuration) -> None:
116+
model = timelines_configuration.models[Ref("repeater_reducer")]
117+
resolve_timelines(model)
118+
assert model.components[Ref("macro1")].timeline == ROOT_TIMELINE
119+
assert model.components[Ref("macro2")].timeline == ROOT_TIMELINE
120+
assert model.components[Ref("micro1")].timeline == Timeline(":macro1")
121+
assert model.components[Ref("micro2")].timeline == Timeline(":macro2")
122+
123+
# Remove filters on the last conduit to make the incoming timelines inconsistent
124+
model.conduits[-1].filters = []
125+
with pytest.raises(InconsistentTimelines, match="different timelines"):
126+
resolve_timelines(model)
127+
128+
129+
def test_repeater_after_reducer_error(timelines_configuration: Configuration) -> None:
130+
model = timelines_configuration.models[Ref("repeater_reducer_error")]
131+
with pytest.raises(ConduitTimelineError, match="remove a repeater and reducer"):
132+
resolve_timelines(model)
133+
model.conduits[-1].filters = []
134+
resolve_timelines(model)
135+
assert model.components[Ref("macro")].timeline == ROOT_TIMELINE
136+
assert model.components[Ref("micro1")].timeline == Timeline(":macro")
137+
assert model.components[Ref("micro2")].timeline == Timeline(":macro")
138+
139+
140+
def test_inconsistent_interact(timelines_configuration: Configuration) -> None:
141+
model = timelines_configuration.models[Ref("inconsistent_interact")]
142+
with pytest.raises(ConduitTimelineError, match="missing timeline annotations"):
143+
resolve_timelines(model)
144+
model.components[Ref("B")].ports["out"].timeline = Timeline("A")
145+
model.components[Ref("B")].ports["in"].timeline = Timeline("A")
146+
resolve_timelines(model)
147+
148+
149+
def test_model_ports(timelines_configuration: Configuration) -> None:
150+
model = timelines_configuration.models[Ref("model_ports")]
151+
resolve_timelines(model)

0 commit comments

Comments
 (0)