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: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ license = "Apache-2.0"
dependencies = [
"click",
"svg.py",
# "ymmsl >= 0.15",
"ymmsl @ git+https://github.com/multiscale/ymmsl-python.git@feature/timelines",
# "ymmsl >= 0.18",
"ymmsl @ git+https://github.com/multiscale/ymmsl-python.git@develop",
]
dynamic = ["version"]
requires-python = ">= 3.10"
Expand Down
8 changes: 4 additions & 4 deletions src/ymmsl2svg/model_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from functools import cmp_to_key

import svg
from ymmsl.v0_2 import Conduit, Identifier, Model, Operator, TimelineTree
from ymmsl.v0_2 import Conduit, Identifier, Model, Operator

from ymmsl2svg.base import SvgBlock
from ymmsl2svg.settings import settings
from ymmsl2svg.timeline_block import TimelineBlock
from ymmsl2svg.timeline_node import create_timeline_nodes

logger = logging.getLogger(__name__)

Expand All @@ -33,11 +34,10 @@ def __init__(self, model: Model) -> None:
}

# Determine timelines
self.timeline_tree = TimelineTree(self.model)
self.timeline_tree.check_consistent()
self.root_timeline_node = create_timeline_nodes(self.model)

# Create graph components
self.timeline_block = TimelineBlock(self.timeline_tree, self.timeline_tree.root)
self.timeline_block = TimelineBlock(self.root_timeline_node)

# Route conduits
self.components = self.timeline_block.map_components()
Expand Down
18 changes: 7 additions & 11 deletions src/ymmsl2svg/timeline_block.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from collections.abc import Callable, Iterator

import svg
from ymmsl.v0_2 import Reference, TimelineNode, TimelineTree
from ymmsl.v0_2 import Reference

from ymmsl2svg.base import SvgBlock
from ymmsl2svg.component_block import ComponentBlock
from ymmsl2svg.conduit_ducts import ConduitDuct, TopConduitDuct
from ymmsl2svg.timeline_node import TimelineNode


class TimelineBlock(SvgBlock):
Expand All @@ -15,10 +16,9 @@ class TimelineBlock(SvgBlock):
yMMSL timeline. Subtimelines are instantiated and layed out recursively.
"""

def __init__(self, tree: TimelineTree, node: TimelineNode) -> None:
def __init__(self, node: TimelineNode) -> None:
super().__init__()

self.tree = tree
self.node = node
if len(node.parent_components) > 1:
raise NotImplementedError(
Expand All @@ -27,7 +27,7 @@ def __init__(self, tree: TimelineTree, node: TimelineNode) -> None:

self.transform: svg.Transform = svg.Translate(0, 0)

self.top_conduit_duct = TopConduitDuct(self, node.name)
self.top_conduit_duct = TopConduitDuct(self, node.timeline)
self.conduit_ducts: list[ConduitDuct] = [
ConduitDuct(self.top_conduit_duct) for _ in range(len(node.components) + 1)
]
Expand All @@ -36,17 +36,13 @@ def __init__(self, tree: TimelineTree, node: TimelineNode) -> None:
# Subtimelines
self.subtimelines: list[TimelineBlock] = []
subtl_per_component: dict[Reference, list[TimelineBlock]] = {}
for subnode in node.children:
subtimeline = TimelineBlock(tree, subnode)
for subnode in node.children.values():
subtimeline = TimelineBlock(subnode)
self.subtimelines.append(subtimeline)
for component in subnode.parent_components:
subtl_per_component.setdefault(component.name, []).append(subtimeline)

# For now we take the order of components from the timeline, but we should
# revisit this:
# - Support interact coupling (components with shared timelines must be next to
# each other)
# - Minimize conduit crossings
# Order of components is determined by TimelineNode
for i, component in enumerate(node.components):
subtimelines = subtl_per_component.get(component.name, [])
cblock = ComponentBlock(
Expand Down
222 changes: 222 additions & 0 deletions src/ymmsl2svg/timeline_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import heapq
import itertools

from ymmsl.v0_2 import (
Component,
Model,
Operator,
Reference,
Timeline,
resolve_timelines,
)


def create_timeline_nodes(model: Model) -> "TimelineNode":
"""Create all timeline nodes for a model and return the root node."""
resolve_timelines(model)
root = TimelineNode(Timeline(":"), None)

for component in model.components.values():
assert component.timeline is not None
node = root[component.timeline]
node.add_component(component)

root.calculate_component_order(model)
return root


class TimelineNode:
def __init__(self, timeline: Timeline, parent: "TimelineNode | None") -> None:
self.timeline = timeline
"""Timeline for this node."""
self.parent = parent
"""Parent of this node, will be None for the root timeline."""

self.children: dict[Reference, TimelineNode] = {}
"""Child timeline nodes for direct subtimelines."""
self.components: list[Component] = []
"""Components that are part of this timeline."""
self.parent_components: list[Component] = []
"""Parent components, i.e. those with O_I or S ports that send/receive in
this timeline."""

def __getitem__(self, timeline: Timeline) -> "TimelineNode":
"""Get a sub-timeline of this one, creating a new one if required."""
assert not timeline.absolute or self.parent is None
node = self
for part in timeline:
if part not in node.children:
subtimeline = node.timeline + Timeline([part], absolute=False)
node.children[part] = TimelineNode(subtimeline, node)
node = node.children[part]
return node

def add_component(self, component: Component) -> None:
"""Register a component with this timeline node"""
self.components.append(component)
for port in component.ports.values():
if port.timeline: # Only take O_I and S ports with a (sub)timeline
node = self[port.timeline]
if component not in node.parent_components:
node.parent_components.append(component)

def calculate_component_order(self, model: Model) -> None:
"""Determine order of components in this timeline."""
# Group all components with shared timelines
component_groups = self._group_components()
group_per_component = {}
for i, group in enumerate(component_groups):
for component in group:
group_per_component[component] = i

# Find dependencies between the groups
group_deps: set[tuple[int, int]] = set()
for sender, receiver in self._finit_dependencies(model):
# Check that we don't have dependencies between components in the same group
if group_per_component[sender] == group_per_component[receiver]:
raise RuntimeError(
"Unsupported coupling graph: found an F_INIT connection between "
f"'{sender}' and '{receiver}', who also share a child timeline."
)
group_deps.add((group_per_component[sender], group_per_component[receiver]))

# Sequence components
new_components = []
for idx in self._topological_sort(list(group_deps), len(component_groups)):
# Component sequence within a group is determined in _group_components()
new_components.extend(component_groups[idx])
assert set(new_components) == set(self.components)
self.components = new_components

# Repeat for each subtimeline
for subtl in self.children.values():
subtl.calculate_component_order(model)

def _group_components(self) -> list[list[Component]]:
"""Group all components that (indirectly) share a timeline.

For example, a timeline bridge connecting two components A and B will have two
subtimelines: ":A" shared between components A and bridge, and ":B" shared
between components B and bridge. All three components are part of a single
group.
"""
shared_subtl_per_component: dict[Component, list[TimelineNode]] = {}
for subtl in self.children.values():
if len(subtl.parent_components) > 2:
raise RuntimeError(
f"Unsupported coupling graph: subtimeline '{subtl.timeline}' "
f"has {len(subtl.parent_components)} parent components, but we "
"support no more than 2."
)
if len(subtl.parent_components) > 1:
for component in subtl.parent_components:
shared_subtl_per_component.setdefault(component, []).append(subtl)

for comp, timelines in shared_subtl_per_component.items():
if len(timelines) > 2:
names = ", ".join(str(tl.timeline) for tl in timelines)
raise RuntimeError(
f"Unsupported coupling graph: component '{comp.name}' has "
f"{len(timelines)} shared subtimelines ({names}), "
"but we support no more than 2."
)

component_groups = []
done = set()
for component in self.components:
if component in done:
continue
if len(shared_subtl_per_component.get(component, [])) > 1:
# We'll get back to this component later, the first component in the
# group should be one with only a single shared timeline
continue

group = []
next_component = component
while next_component is not None:
group.append(next_component)
component = next_component
next_component = None
for subtl in shared_subtl_per_component.get(component, []):
for comp in subtl.parent_components:
if comp not in group:
assert next_component is None
next_component = comp
component_groups.append(group)
done.update(group)

# Sanity check
if len(done) != len(self.components):
raise RuntimeError(
"Could not assign components into groups, this may be a bug or an "
"unsupported coupling graph."
)
return component_groups

def _ancestors_of(self, component: Component) -> list[Component]:
"""Get ancestors of the component in our timeline."""
assert component.timeline is not None
if component.timeline == self.timeline:
return [component]
try:
subtl = component.timeline.relative_to(self.timeline)
return self.children[subtl[0]].parent_components
except ValueError: # component is not in a subtimeline
return []

def _finit_dependencies(self, model: Model) -> set[tuple[Component, Component]]:
"""Determine dependencies resulting from conduits connected to F_INIT ports.

Returns a list of component pairs, where the second component depends on a
message of the first component.
"""
dependencies: set[tuple[Component, Component]] = set()
# Determine dependencies between component groups
for conduit in model.conduits:
# Look for conduits connected to F_INIT ports relevant for this timeline
if not conduit.sending_component() or not conduit.receiving_component():
continue # Ignore model ports
receiver = model.components[conduit.receiving_component()]
if receiver.ports[conduit.receiving_port()].operator is not Operator.F_INIT:
continue
sender = model.components[conduit.sending_component()]

# Both sender and receiver must be in our timeline, or a subtimeline
sender_ancestors = self._ancestors_of(sender)
receiver_ancestors = self._ancestors_of(receiver)
if sender_ancestors == receiver_ancestors:
# Sender and receiver have the same ancestor components in our timeline,
# so we can ignore this conduit for sequencing this timeline
continue
for dependency in itertools.product(sender_ancestors, receiver_ancestors):
dependencies.add(dependency)
return dependencies

def _topological_sort(
self, dependencies: list[tuple[int, int]], num_groups: int
) -> list[int]:
"""Sort groups topologically, use group number as tie breaker.

Algorithm based on networkx.algorithms.dag.lexicographical_topological_sort, see
https://networkx.org/documentation/stable/_modules/networkx/algorithms/dag.html#lexicographical_topological_sort
"""
indegrees = [0] * num_groups
for _, to in dependencies:
indegrees[to] += 1
zero_indegree = [i for i in range(num_groups) if indegrees[i] == 0]
heapq.heapify(zero_indegree)

order = []
while zero_indegree:
node = heapq.heappop(zero_indegree)
order.append(node)
for from_, to in dependencies:
if from_ != node:
continue
indegrees[to] -= 1
if indegrees[to] == 0:
heapq.heappush(zero_indegree, to)

if any(indegrees):
raise ValueError("Graph contains a cycle")
return order
29 changes: 29 additions & 0 deletions tests/configurations/node-ordering.ymmsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ymmsl_version: v0.2

models:
more_filters:
components:
A:
description: Macro model
ports:
f_init: init
B:
description: Meso model
ports:
o_i: output
s: input
C:
description: Micro model
ports:
f_init: in1 in2
o_f: output
D:
description: Macro model
ports:
o_f: final
conduits:
B.output: C.in1
C.output:
- B.input
- last A.init
D.final: repeat C.in2
29 changes: 29 additions & 0 deletions tests/configurations/timescale-bridge.ymmsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ymmsl_version: v0.2
description: timescale bridge
models:
interact:
components:
A:
description: A
ports:
o_i: output
s: input
B:
description: B
ports:
o_i: output
s: input
bridge:
description: timescale bridge
ports:
timeline A:
o_i: a_out
s: a_in
timeline B:
o_i: b_out
s: b_in
conduits:
A.output: bridge.a_in
bridge.a_out: A.input
B.output: bridge.b_in
bridge.b_out: B.input
Loading