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
2 changes: 1 addition & 1 deletion ymmsl/conversion/convert_v0_1_to_v0_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def convert_model(model: v0_1.ModelReference) -> v0_2.Model:
conduits = list(map(convert_conduit, model.conduits))
infer_ports(components, conduits)
return v0_2.Model(
str(model.name), None, description, None, components, conduits
str(model.name), None, description, None, components, None, conduits
)
else:
return v0_2.Model(str(model.name), None, description, None, [], [])
Expand Down
2 changes: 2 additions & 0 deletions ymmsl/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
v0_2.ImportKind,
v0_2.ImportStatement,
v0_2.KeepsStateForNextUse,
v0_2.MatchingTimelines,
v0_2.Model,
v0_2.MPICoresResReq,
v0_2.MPINodesResReq,
Expand All @@ -67,6 +68,7 @@
v0_2.Settings,
v0_2.SupportedSetting,
v0_2.SupportedSettings,
v0_2.Timeline,
v0_2.ThreadedResReq,
)

Expand Down
3 changes: 2 additions & 1 deletion ymmsl/v0_2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ymmsl.v0_2.identity import Identifier, Reference, ReferencePart
from ymmsl.v0_2.implementation import Implementation
from ymmsl.v0_2.imports import ImportKind, ImportStatement
from ymmsl.v0_2.model import Conduit, ConduitFilter, Model
from ymmsl.v0_2.model import Conduit, ConduitFilter, MatchingTimelines, Model
from ymmsl.v0_2.ports import Operator, Port, Ports, Timeline
from ymmsl.v0_2.program import Program
from ymmsl.v0_2.resolver import resolve
Expand Down Expand Up @@ -59,6 +59,7 @@
"ImportStatement",
"InconsistentTimelines",
"KeepsStateForNextUse",
"MatchingTimelines",
"Model",
"MPICoresResReq",
"MPINodesResReq",
Expand Down
92 changes: 91 additions & 1 deletion ymmsl/v0_2/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ymmsl.v0_2.component import Component
from ymmsl.v0_2.identity import Identifier, Reference
from ymmsl.v0_2.implementation import Implementation
from ymmsl.v0_2.ports import Ports
from ymmsl.v0_2.ports import Ports, Timeline
from ymmsl.v0_2.supported_settings import SupportedSettings


Expand Down Expand Up @@ -267,6 +267,83 @@ def as_conduits(self) -> List[Conduit]:
AnyConduit: TypeAlias = Conduit | MulticastConduit


class MatchingTimelines:
"""Represents a set of matching timelines.

Matching timelines are timelines generated by different components that have the
same time points. This can happen if multiple components are configured to generate
the same time points, or if a component adapts its timeline to that of another
component, like time bridges do.

Semantically, this is an equivalence class and so all timelines are equivalent, but
we record a "head" of the set so that we can express which timeline is leading and
which ones are following it in asymmetric situations like with time bridges.

In YAML, these can be expressed as a dictionary, similarly to conduits:

timeline1: timeline2
timeline3:
- timeline4
- timeline5
timeline6: timeline7 timeline8
macro1:micro1: macro2:micro2

Attributes:
head: "Main" timeline of this set of matching timelines
matches: Set of all matching timelines, including the head
"""

def __init__(
self,
head: str | Timeline,
matches: str | Timeline | List[str] | List[Timeline],
) -> None:
"""Create a MatchingTimelines.

The matches argument may be a string containing whitespace-separated timelines,
a Timeline object, a list of Timelines, or a list of strings representing a
single timeline each.

The "head" timeline doesn't have to be passed in both arguments, it will be
added to matches if needed.

Args:
head: "Main" timeline of this set of matching timelines
matches: Matching timelines
"""
self.head = head if isinstance(head, Timeline) else Timeline(head)

if isinstance(matches, str):
self.matches = {Timeline(t) for t in matches.split()}
elif isinstance(matches, Timeline):
self.matches = {matches}
else:
if not matches:
raise RuntimeError(
"To declare matching timelines, you need to specify at least two of"
" them"
)
self.matches = {
Timeline(tl) if isinstance(tl, str) else tl for tl in matches
}
self.matches.add(self.head)

def __contains__(self, timeline: Timeline) -> bool:
return timeline in self.matches

def _yatiml_attributes(self) -> OrderedDict:
matches: str | list[str] = list(map(str, sorted(self.matches - {self.head})))
if len(matches) < 6 and sum(len(m) for m in matches) < 60:
matches = " ".join(matches)

return OrderedDict(
[
("head", str(self.head)),
("matches", matches),
]
)


class Model(Implementation):
"""Describes a simulation model.

Expand All @@ -284,6 +361,7 @@ class Model(Implementation):
supported_settings: Settings supported by this model.
components: A list of components making up the model.
conduits: A list of conduits connecting the components.
matching_timelines: A list of sets of matching timelines.
"""

def __init__(
Expand All @@ -293,6 +371,7 @@ def __init__(
description: str = "",
supported_settings: SupportedSettings | None = None,
components: Sequence[Component] | None = None,
matching_timelines: Sequence[MatchingTimelines] | None = None,
conduits: Sequence[AnyConduit] | None = None,
) -> None:
"""Create a Model.
Expand All @@ -303,6 +382,7 @@ def __init__(
description: Human-readable description of the model
supported_settings: Settings supported by this model
components: A list of components making up the model
matching_timelines: A list of matching timelines
conduits: A list of conduits connecting the components
"""
super().__init__(name, ports, description, supported_settings)
Expand All @@ -322,6 +402,8 @@ def __init__(

self.components = {copy(c.name): c for c in components}

self.matching_timelines = matching_timelines

self.conduits: list[Conduit] = list()
if conduits:
for conduit in conduits:
Expand Down Expand Up @@ -448,6 +530,7 @@ def _yatiml_attributes(self) -> OrderedDict:
("description", self.description),
("supported_settings", self.supported_settings),
("components", self.components),
("matching_timelines", self.matching_timelines),
("conduits", self._conduits_for_export()),
]
)
Expand All @@ -461,6 +544,7 @@ def _yatiml_recognize(cls, node: yatiml.UnknownNode) -> None:
def _yatiml_savorize(cls, node: yatiml.Node) -> None:
node.map_attribute_to_seq("components", "name")
node.map_attribute_to_seq("conduits", "sender", "receiver")
node.map_attribute_to_seq("matching_timelines", "head", "matches")

@classmethod
def _yatiml_sweeten(cls, node: yatiml.Node) -> None:
Expand All @@ -470,3 +554,9 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None:
node.remove_attribute("conduits")

node.seq_attribute_to_map("conduits", "sender", "receiver")

mt_node = node.get_attribute("matching_timelines")
if mt_node.is_scalar(None) or len(mt_node.seq_items()) == 0:
node.remove_attribute("matching_timelines")

node.seq_attribute_to_map("matching_timelines", "head", "matches")
108 changes: 48 additions & 60 deletions ymmsl/v0_2/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,31 @@
class Timeline:
"""Identify a timeline on which a port sends or receives.

Timeline objects describe when a port sends or receives, either relative to a parent
timeline that calls the component they're a part of, or by describing the whole list
of components calling each other from the root timeline down.

A component c1 that is not called by any other component will have its F_INIT and
O_F ports (if any) on the root timeline, which is represented by ':'. If c1's
implementation has a loop in which it sends on an O_I port and receives on an S
port, then those ports are on a subtimeline, which is named after the component by
default, ':c1'.

If we add a component c2 and connect its F_INIT and O_F to those ports, then we
create a macro-micro type coupling. The F_INIT and O_F ports of c2 will then be on
timeline ':c1', because they'll receive and send at the exact points in simulated
time that c1's O_I and S ports send and receive.

If c2 has its own O_I and S ports, then those will be on timeline ':c1:c2', this
being the concatenation of the parent timeline and the relative timeline within c2.

Some implementations may have more than one set of O_I/S ports, on which they
communicate at different rates. In that case, each port should be given a local
relative timeline explicitly. In the above example, if c1's O_I and S ports were
specified to be on local relative timeline 'tl1', then their full relative timeline
is 'c1.tl' and their absolute timeline is ':c1.tl`, putting c2's O_I and S ports on
':c1.tl:c2' unless they too have an explicit timeline designation.
A timeline is a stretch of time with a beginning and an end that is subdivided into
zero or more time steps that step from one intermediate time point to the next.

F_INIT ports receive at the beginning of the timeline, O_F ports send at the end of
the timeline, O_I ports send at the first and intermediate points on the timeline,
and S ports receive at intermediate and the last time point of the timeline.

Timeline objects are used to annotate components and ports, to describe which
timeline they're on.

For components, the timeline depends on where in a call hierarchy the component
sits. A component ``c1`` that is not called (via an O_I to F_INIT conduit) by any
other model has timeline ``c1``, relative to the model it's in. If ``c2`` is called
by ``c1``, then it is in timeline ``c2`` relative to its *parent timeline* ``c1``,
and in ``c1:c2`` relative to the model. If ``c1`` dispatches (O_F to F_INIT) to
``c3``, then ``c3``'s parent timeline is the same as ``c1``'s parent timeline, which
is the empty model timeline, putting component ``c3`` into timeline ``c3``.

Ports are on the timeline of the component they're associated with. Some components
however have multiple sets of O_I/S ports, on which they communicate at different
rates. Each group of such ports is annotated, using this class, with a timeline
annotation naming the sub-timeline that port is on. If an O_I port on component
``c1`` is annotated with ``tl1``, then that port will be on timeline ``c1.tl1``
rather than on ``c1``, and if it were on ``c2`` then the full timeline would be
``c1:c2.tl1``.

Timelines have a technical representation as a list of References, and a string
representation in which those References are joined using colons.
Expand All @@ -41,14 +42,11 @@ class Timeline:
def __init__(self, timeline: str) -> None: ...

@overload
def __init__(
self, timeline: Sequence[str | Reference], absolute: bool = True
) -> None: ...
def __init__(self, timeline: Sequence[str | Reference]) -> None: ...

def __init__(
self,
timeline: str | Sequence[str | Reference],
absolute: bool = True,
) -> None:
"""Create a Timeline.

Expand All @@ -60,42 +58,39 @@ def make_new_reference(x: str | Reference) -> Reference:
return Reference(str(x))

if isinstance(timeline, str):
timeline = timeline.strip(":")
if timeline == "":
self.absolute = False
parts: Sequence[str | Reference] = []
elif timeline == ":":
self.absolute = True
parts = []
else:
self.absolute = False
if timeline[0] == ":":
self.absolute = True
timeline = timeline[1:]
self._parts = []
return

parts = timeline.split(":")
timeline = timeline.split(":")

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

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

def __eq__(self, other: Any) -> bool:
"""Compare with another Timeline or a string for equality."""
if isinstance(other, str):
return str(self) == other
elif isinstance(other, Timeline):
return self.absolute == other.absolute and self._parts == other._parts
return self._parts == other._parts
return NotImplemented

def __hash__(self) -> int:
"""Make this hashable so we can make sets of Timelines."""
return hash(str(self))

def __lt__(self, other: Any) -> bool:
"""Compare lexicographically by parts."""
if isinstance(other, str):
other_tl = Timeline(other)
else:
other_tl = other

return tuple(self._parts) < tuple(other_tl._parts)

def __str__(self) -> str:
"""Return the string representation of this Timeline."""
anchor = ":" if self.absolute else ""
return anchor + ":".join(map(str, self._parts))
return ":".join(map(str, self._parts))

def __repr__(self) -> str:
"""Return a representation of the object."""
Expand Down Expand Up @@ -129,16 +124,12 @@ def __iter__(self) -> Iterator[Reference]:
yield from self._parts

def __add__(self, other: Any) -> "Timeline":
"""Concatenate this timeline with another (relative!) Timeline."""
"""Concatenate this timeline with another Timeline."""
if isinstance(other, Timeline):
if other.absolute:
raise ValueError(
"Cannot concatenate an absolute Timeline onto another one"
)
return Timeline(self._parts + other._parts, self.absolute)
return Timeline(self._parts + other._parts)

if isinstance(other, Reference):
return Timeline(self._parts + [other], self.absolute)
return Timeline(self._parts + [other])

return NotImplemented

Expand All @@ -150,19 +141,16 @@ def parent(self) -> "Timeline":
RuntimeError if this is the root and there is no parent."""
if not self._parts:
raise RuntimeError("The root timeline does not have a parent")
return Timeline(self._parts[:-1], self.absolute)
return Timeline(self._parts[:-1])

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.
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)
return Timeline(self._parts[len(other) :])


class Port:
Expand Down
Loading
Loading