easylink 0.1.10__py3-none-any.whl → 0.1.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
easylink/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.10"
1
+ __version__ = "0.1.12"
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
 
14
14
  from layered_config_tree import LayeredConfigTree
15
15
 
16
- from easylink.graph_components import EdgeParams
16
+ from easylink.graph_components import EdgeParams, ImplementationGraph
17
17
  from easylink.pipeline_schema_constants import ALLOWED_SCHEMA_PARAMS
18
18
  from easylink.step import HierarchicalStep, NonLeafConfigurationState, Step
19
19
 
@@ -54,6 +54,26 @@ class PipelineSchema(HierarchicalStep):
54
54
  def __repr__(self) -> str:
55
55
  return f"PipelineSchema.{self.name}"
56
56
 
57
+ def get_implementation_graph(self) -> ImplementationGraph:
58
+ """Gets the :class:`~easylink.graph_components.ImplementationGraph`.
59
+
60
+ The ``PipelineSchema`` is by definition a :class:`~easylink.step.HierarchicalStep`
61
+ which has a :class:`~easylink.graph_components.StepGraph` containing
62
+ sub-:class:`Steps<easylink.step.Step>` that need to be unrolled. This method
63
+ recursively traverses that ``StepGraph`` and its childrens' ``StepGraphs``
64
+ until all sub-``Steps`` are in a :class:`~easylink.step.LeafConfigurationState`,
65
+ i.e. all ``Steps`` are implemented by a single ``Implementation`` and we
66
+ have the desired ``ImplementationGraph``.
67
+
68
+ Returns
69
+ -------
70
+ The ``ImplementationGraph`` of this ``PipelineSchema``.
71
+ """
72
+ implementation_graph = ImplementationGraph()
73
+ self.add_nodes_to_implementation_graph(implementation_graph)
74
+ self.add_edges_to_implementation_graph(implementation_graph)
75
+ return implementation_graph
76
+
57
77
  def validate_step(
58
78
  self, pipeline_config: LayeredConfigTree, input_data_config: LayeredConfigTree
59
79
  ) -> dict[str, list[str]]:
@@ -66,16 +66,37 @@ NODES = [
66
66
  name="step_3_main_input",
67
67
  env_var="DUMMY_CONTAINER_MAIN_INPUT_FILE_PATHS",
68
68
  validator=validate_input_file_dummy,
69
- splitter=split_data_by_size,
70
69
  ),
71
70
  ],
72
71
  output_slots=[
73
72
  OutputSlot(
74
73
  name="step_3_main_output",
75
- aggregator=concatenate_datasets,
76
74
  ),
77
75
  ],
78
76
  ),
77
+ input_slots=[
78
+ InputSlot(
79
+ name="step_3_main_input",
80
+ env_var="DUMMY_CONTAINER_MAIN_INPUT_FILE_PATHS",
81
+ validator=validate_input_file_dummy,
82
+ splitter=split_data_by_size,
83
+ ),
84
+ ],
85
+ output_slots=[OutputSlot("step_3_main_output", aggregator=concatenate_datasets)],
86
+ input_slot_mappings=[
87
+ InputSlotMapping(
88
+ parent_slot="step_3_main_input",
89
+ child_node="step_3",
90
+ child_slot="step_3_main_input",
91
+ ),
92
+ ],
93
+ output_slot_mappings=[
94
+ OutputSlotMapping(
95
+ parent_slot="step_3_main_output",
96
+ child_node="step_3",
97
+ child_slot="step_3_main_output",
98
+ ),
99
+ ],
79
100
  ),
80
101
  self_edges=[
81
102
  EdgeParams(
easylink/step.py CHANGED
@@ -15,7 +15,6 @@ import copy
15
15
  from abc import ABC, abstractmethod
16
16
  from collections import defaultdict
17
17
  from collections.abc import Iterable
18
- from typing import Any
19
18
 
20
19
  from layered_config_tree import LayeredConfigTree
21
20
 
@@ -88,6 +87,7 @@ class Step:
88
87
  output_slots: Iterable[OutputSlot] = (),
89
88
  input_slot_mappings: Iterable[InputSlotMapping] = (),
90
89
  output_slot_mappings: Iterable[OutputSlotMapping] = (),
90
+ is_embarrassingly_parallel: bool = False,
91
91
  ) -> None:
92
92
  self.step_name = step_name
93
93
  """The name of the pipeline step in the ``PipelineSchema``. It must also match
@@ -108,6 +108,8 @@ class Step:
108
108
  }
109
109
  """A combined dictionary containing both the ``InputSlotMappings`` and
110
110
  ``OutputSlotMappings`` of this ``Step``."""
111
+ self.is_embarrassingly_parallel = is_embarrassingly_parallel
112
+ """Whether or not this ``Step`` is to be run in an embarrassingly parallel manner."""
111
113
  self.parent_step = None
112
114
  """This ``Step's`` parent ``Step``, if applicable."""
113
115
  self._configuration_state = None
@@ -250,18 +252,25 @@ class Step:
250
252
  ]
251
253
  return errors
252
254
 
253
- def get_implementation_graph(self) -> ImplementationGraph:
254
- """Gets this ``Step's`` :class:`~easylink.graph_components.ImplementationGraph`.
255
+ def add_nodes_to_implementation_graph(
256
+ self, implementation_graph: ImplementationGraph
257
+ ) -> None:
258
+ """Adds the ``Implementations`` related to this ``Step`` as nodes to the :class:`~easylink.graph_components.ImplementationGraph`.
255
259
 
256
- The ``ImplementationGraph`` and how it is determined depends on whether
257
- this ``Step`` is a leaf or a non-leaf, i.e. what its :attr:`configuration_state`
258
- is.
260
+ How the nodes get added depends on whether this ``Step`` is a leaf or a non-leaf,
261
+ i.e. what its :attr:`configuration_state` is.
262
+ """
263
+ self.configuration_state.add_nodes_to_implementation_graph(implementation_graph)
259
264
 
260
- Returns
261
- -------
262
- The ``ImplementationGraph`` of this ``Step`` based on its ``configuration_state``.
265
+ def add_edges_to_implementation_graph(
266
+ self, implementation_graph: ImplementationGraph
267
+ ) -> None:
268
+ """Adds the edges of this ``Step's`` ``Implementation(s)`` to the :class:`~easylink.graph_components.ImplementationGraph`.
269
+
270
+ How the edges get added depends on whether this ``Step`` is a leaf or a non-leaf,
271
+ i.e. what its :attr:`configuration_state` is.
263
272
  """
264
- return self.configuration_state.get_implementation_graph()
273
+ self.configuration_state.add_edges_to_implementation_graph(implementation_graph)
265
274
 
266
275
  def get_implementation_edges(self, edge: EdgeParams) -> list[EdgeParams]:
267
276
  """Gets the edge information for the ``Implementation`` related to this ``Step``.
@@ -387,7 +396,7 @@ class IOStep(Step):
387
396
  The internal configuration of this ``Step``, i.e. it should not include
388
397
  the ``Step's`` name.
389
398
  combined_implementations
390
- The configuration for any implementations to be combined.
399
+ The configuration for any ``Implementations`` to be combined.
391
400
  input_data_config
392
401
  The input data configuration for the entire pipeline.
393
402
  """
@@ -395,8 +404,10 @@ class IOStep(Step):
395
404
  self, step_config, combined_implementations, input_data_config
396
405
  )
397
406
 
398
- def get_implementation_graph(self) -> ImplementationGraph:
399
- """Gets this ``Step's`` :class:`~easylink.graph_components.ImplementationGraph`.
407
+ def add_nodes_to_implementation_graph(
408
+ self, implementation_graph: ImplementationGraph
409
+ ) -> None:
410
+ """Adds this ``IOStep's`` ``Implementation`` as a node to the :class:`~easylink.graph_components.ImplementationGraph`.
400
411
 
401
412
  Notes
402
413
  -----
@@ -404,19 +415,22 @@ class IOStep(Step):
404
415
  via an :class:`~easylink.implementation.Implementation`. As such, we
405
416
  leverage the :class:`~easylink.implementation.NullImplementation` class
406
417
  to generate the graph node.
407
-
408
- Returns
409
- -------
410
- The ``ImplementationGraph`` of this ``Step``.
411
418
  """
412
- implementation_graph = ImplementationGraph()
413
419
  implementation_graph.add_node_from_implementation(
414
420
  self.name,
415
421
  implementation=NullImplementation(
416
422
  self.name, self.input_slots.values(), self.output_slots.values()
417
423
  ),
418
424
  )
419
- return implementation_graph
425
+
426
+ def add_edges_to_implementation_graph(self, implementation_graph):
427
+ """Adds the edges of this ``Step's`` ``Implementation`` to the ``ImplementationGraph``.
428
+
429
+ ``IOSteps`` do not have edges within them in the ``ImplementationGraph``,
430
+ since they are represented by a single ``NullImplementation`` node, and so we
431
+ simply pass.
432
+ """
433
+ pass
420
434
 
421
435
 
422
436
  class InputStep(IOStep):
@@ -873,7 +887,7 @@ class TemplatedStep(Step, ABC):
873
887
  self.step_graph = StepGraph()
874
888
  self.template_step.name = self.name
875
889
  self.step_graph.add_node_from_step(self.template_step)
876
- # Special handle the slot_mappings update
890
+ # Update the slot mappings with renamed children
877
891
  input_mappings = [
878
892
  InputSlotMapping(slot, self.name, slot) for slot in self.input_slots
879
893
  ]
@@ -888,10 +902,8 @@ class TemplatedStep(Step, ABC):
888
902
  num_repeats = len(expanded_config)
889
903
  self.step_graph = self._update_step_graph(num_repeats)
890
904
  self.slot_mappings = self._update_slot_mappings(num_repeats)
891
- # Manually set the configuration state to non-leaf instead of relying
892
- # on super().get_configuration_state() because that method will erroneously
893
- # set to leaf state in the event the user didn't include the config_key
894
- # in the pipeline specification.
905
+
906
+ # TemplatedSteps are by definition non-leaf steps.
895
907
  self._configuration_state = NonLeafConfigurationState(
896
908
  self, expanded_config, combined_implementations, input_data_config
897
909
  )
@@ -1153,10 +1165,22 @@ class EmbarrassinglyParallelStep(Step):
1153
1165
  def __init__(
1154
1166
  self,
1155
1167
  step: Step,
1168
+ input_slots: Iterable[InputSlot],
1169
+ output_slots: Iterable[OutputSlot],
1170
+ input_slot_mappings: Iterable[InputSlotMapping],
1171
+ output_slot_mappings: Iterable[OutputSlotMapping],
1156
1172
  ) -> None:
1157
1173
  super().__init__(
1158
- step.step_name, step.name, step.input_slots.values(), step.output_slots.values()
1174
+ step.step_name,
1175
+ step.name,
1176
+ input_slots,
1177
+ output_slots,
1178
+ input_slot_mappings,
1179
+ output_slot_mappings,
1180
+ is_embarrassingly_parallel=True,
1159
1181
  )
1182
+ self.step_graph = None
1183
+ self.step = step
1160
1184
  self._validate()
1161
1185
 
1162
1186
  def _validate(self) -> None:
@@ -1199,6 +1223,50 @@ class EmbarrassinglyParallelStep(Step):
1199
1223
  if errors:
1200
1224
  raise ValueError("\n".join(errors))
1201
1225
 
1226
+ def set_configuration_state(
1227
+ self,
1228
+ step_config: LayeredConfigTree,
1229
+ combined_implementations: LayeredConfigTree,
1230
+ input_data_config: LayeredConfigTree,
1231
+ ):
1232
+ """Sets the configuration state to 'non-leaf'.
1233
+
1234
+ In addition to setting the configuration state, this also updates the
1235
+ :class:`~easylink.graph_components.StepGraph` and
1236
+ :class:`SlotMappings<easylink.graph_components.SlotMapping>`.
1237
+
1238
+ Parameters
1239
+ ----------
1240
+ step_config
1241
+ The internal configuration of this ``Step``, i.e. it should not include
1242
+ the ``Step's`` name.
1243
+ combined_implementations
1244
+ The configuration for any implementations to be combined.
1245
+ input_data_config
1246
+ The input data configuration for the entire pipeline.
1247
+ """
1248
+ if self.step.name != self.name:
1249
+ # Update the step name if the parent got renamed, e.g. a parent LoopStep
1250
+ # 'step_1' that got expanded to 'step_1_loop_1', etc.
1251
+ self.step.name = self.name
1252
+ input_mappings = [
1253
+ InputSlotMapping(slot, self.name, slot) for slot in self.input_slots
1254
+ ]
1255
+ output_mappings = [
1256
+ OutputSlotMapping(slot, self.name, slot) for slot in self.output_slots
1257
+ ]
1258
+ self.slot_mappings = {"input": input_mappings, "output": output_mappings}
1259
+ # Generate step graph from the single ``step`` attr
1260
+ self.step_graph = StepGraph()
1261
+ self.step_graph.add_node_from_step(self.step)
1262
+ # Add the key back to the expanded config
1263
+ expanded_config = LayeredConfigTree({self.name: step_config})
1264
+
1265
+ # EmbarrassinglyParallelSteps are by definition non-leaf steps
1266
+ self._configuration_state = NonLeafConfigurationState(
1267
+ self, expanded_config, combined_implementations, input_data_config
1268
+ )
1269
+
1202
1270
 
1203
1271
  class ChoiceStep(Step):
1204
1272
  """A type of :class:`Step` that allows for choosing from a set of options.
@@ -1394,8 +1462,17 @@ class ConfigurationState(ABC):
1394
1462
  """The input data configuration for the entire pipeline."""
1395
1463
 
1396
1464
  @abstractmethod
1397
- def get_implementation_graph(self) -> ImplementationGraph:
1398
- """Resolves the graph composed of ``Steps`` into one composed of ``Implementations``."""
1465
+ def add_nodes_to_implementation_graph(
1466
+ self, implementation_graph: ImplementationGraph
1467
+ ) -> None:
1468
+ """Adds this ``Step's`` ``Implementation(s)`` as nodes to the :class:`~easylink.graph_components.ImplementationGraph`."""
1469
+ pass
1470
+
1471
+ @abstractmethod
1472
+ def add_edges_to_implementation_graph(
1473
+ self, implementation_graph: ImplementationGraph
1474
+ ) -> None:
1475
+ """Adds the edges of this ``Step's`` ``Implementation(s)`` to the :class:`~easylink.graph_components.ImplementationGraph`."""
1399
1476
  pass
1400
1477
 
1401
1478
  @abstractmethod
@@ -1438,24 +1515,21 @@ class LeafConfigurationState(ConfigurationState):
1438
1515
  else self.step_config.implementation
1439
1516
  )
1440
1517
 
1441
- def get_implementation_graph(self) -> ImplementationGraph:
1442
- """Gets this ``Step's`` :class:`~easylink.graph_components.ImplementationGraph`.
1518
+ def add_nodes_to_implementation_graph(
1519
+ self, implementation_graph: ImplementationGraph
1520
+ ) -> None:
1521
+ """Adds this ``Step's`` :class:`~easylink.implementation.Implementation` as a node to the :class:`~easylink.graph_components.ImplementationGraph`.
1443
1522
 
1444
1523
  A ``Step`` in a leaf configuration state by definition has no sub-``Steps``
1445
1524
  to unravel; we are able to directly instantiate an :class:`~easylink.implementation.Implementation`
1446
- and generate an ``ImplementationGraph`` from it.
1447
-
1448
- Returns
1449
- -------
1450
- The ``ImplementationGraph`` related to this ``Step``.
1525
+ and add it to the ``ImplementationGraph``.
1451
1526
  """
1452
1527
  step = self._step
1453
- implementation_graph = ImplementationGraph()
1454
1528
  if self.is_combined:
1455
- if isinstance(step, EmbarrassinglyParallelStep):
1529
+ if step.is_embarrassingly_parallel:
1456
1530
  raise NotImplementedError(
1457
1531
  "Combining implementations with embarrassingly parallel steps "
1458
- "is not yet supported."
1532
+ "is not supported."
1459
1533
  )
1460
1534
  implementation = PartialImplementation(
1461
1535
  combined_name=self.step_config[COMBINED_IMPLEMENTATION_KEY],
@@ -1469,13 +1543,21 @@ class LeafConfigurationState(ConfigurationState):
1469
1543
  implementation_config=self.implementation_config,
1470
1544
  input_slots=step.input_slots.values(),
1471
1545
  output_slots=step.output_slots.values(),
1472
- is_embarrassingly_parallel=isinstance(step, EmbarrassinglyParallelStep),
1546
+ is_embarrassingly_parallel=step.is_embarrassingly_parallel,
1473
1547
  )
1474
1548
  implementation_graph.add_node_from_implementation(
1475
1549
  step.implementation_node_name,
1476
1550
  implementation=implementation,
1477
1551
  )
1478
- return implementation_graph
1552
+
1553
+ def add_edges_to_implementation_graph(self, implementation_graph) -> None:
1554
+ """Adds the edges for this ``Step's`` ``Implementation`` to the ``ImplementationGraph``.
1555
+
1556
+ ``Steps`` in a ``LeafConfigurationState`` do not actually have edges within them
1557
+ (they are represented by a single node in the ``ImplementationGraph``) and so
1558
+ we simply pass.
1559
+ """
1560
+ pass
1479
1561
 
1480
1562
  def get_implementation_edges(self, edge: EdgeParams) -> list[EdgeParams]:
1481
1563
  """Gets the edge information for the ``Implementation`` related to this ``Step``.
@@ -1543,7 +1625,8 @@ class NonLeafConfigurationState(ConfigurationState):
1543
1625
  for; it should not include the ``Step's`` name (though it must include
1544
1626
  the sub-step names).
1545
1627
  combined_implementations
1546
- The configuration for any implementations to be combined.
1628
+ The configuration for any :class:`Implementations<easylink.implementation.Implementation>`
1629
+ to be combined.
1547
1630
  input_data_config
1548
1631
  The input data configuration for the entire pipeline.
1549
1632
 
@@ -1582,64 +1665,115 @@ class NonLeafConfigurationState(ConfigurationState):
1582
1665
  "NonLeafConfigurationState requires a subgraph upon which to operate, "
1583
1666
  f"but Step {step.name} has no step graph."
1584
1667
  )
1585
- self._nodes = step.step_graph.nodes
1586
1668
  self._configure_subgraph_steps()
1587
1669
 
1588
- def get_implementation_graph(self) -> ImplementationGraph:
1589
- """Gets this ``Step's`` :class:`~easylink.graph_components.ImplementationGraph`.
1670
+ def add_nodes_to_implementation_graph(
1671
+ self, implementation_graph: ImplementationGraph
1672
+ ) -> None:
1673
+ """Adds this ``Step's`` ``Implementations`` as nodes to the ``ImplementationGraph``.
1590
1674
 
1591
- A ``Step`` in a non-leaf configuration state by definition has a ``StepGraph``
1592
- containing sub-``Steps`` that need to be unrolled. This method recursively
1593
- traverses that ``StepGraph`` and its childrens' ``StepGraphs`` until all
1594
- sub-``Steps`` are in a :class:`LeafConfigurationState`, i.e. all ``Steps``
1595
- are implemented by a single ``Implementation`` and we have our desired
1596
- ``ImplementationGraph``.
1675
+ This is a recursive function; it calls itself until all sub-``Steps``
1676
+ are of a ``LeafConfigurationState`` and have had their corresponding
1677
+ ``Implementations`` added as nodes to the ``ImplementationGraph``.
1678
+ """
1679
+ for node in self._step.step_graph.nodes:
1680
+ substep = self._step.step_graph.nodes[node]["step"]
1681
+ if self._step.is_embarrassingly_parallel:
1682
+ substep.is_embarrassingly_parallel = True
1683
+ self._propagate_splitter_aggregators(self._step, substep)
1684
+ substep.add_nodes_to_implementation_graph(implementation_graph)
1685
+
1686
+ @staticmethod
1687
+ def _propagate_splitter_aggregators(parent: Step, child: Step):
1688
+ """Propagates splitters and aggregators to child ``Steps``.
1689
+
1690
+ This method adds the :meth:`~easylink.graph_components.InputSlot.splitter`
1691
+ and :meth:`~easylink.graph_components.OutputSlot.aggregator` methods from a
1692
+ parent ``Step's`` :class:`~easylink.graph_components.InputSlot` and
1693
+ :class:`OutputSlots<easylink.graph_components.OutputSlot>` to the corresponding
1694
+ child steps' slots.
1597
1695
 
1598
- Returns
1599
- -------
1600
- The ``ImplementationGraph`` of this ``Step``.
1696
+ Parameters
1697
+ ----------
1698
+ parent
1699
+ The parent ``Step`` whose ``splitter`` and ``aggregator`` methods are
1700
+ to be propagated to the appropriate child ``Step``.
1701
+ child
1702
+ A child ``Step`` to potentially have its parent's ``splitter`` and
1703
+ ``aggregators`` assigned to its ``InputSlot`` and ``OutputSlots``,
1704
+ respectively.
1705
+ """
1706
+ for parent_input_slot_name, parent_input_slot in parent.input_slots.items():
1707
+ if parent_input_slot.splitter:
1708
+ # Extract the appropriate child slot name from the mapping
1709
+ mappings_with_splitter = [
1710
+ mapping
1711
+ for mapping in parent.slot_mappings["input"]
1712
+ if mapping.parent_slot == parent_input_slot_name
1713
+ ]
1714
+ for mapping in mappings_with_splitter:
1715
+ child_node = mapping.child_node
1716
+ child_slot = mapping.child_slot
1717
+ # Assign the splitter to the appropriate child slot
1718
+ if child_slot in child.input_slots and child_node == child.name:
1719
+ child.input_slots[child_slot].splitter = parent_input_slot.splitter
1720
+ for parent_output_slot_name, parent_output_slot in parent.output_slots.items():
1721
+ # Extract the appropriate child slot name from the mapping
1722
+ mappings_from_parent = [
1723
+ mapping
1724
+ for mapping in parent.slot_mappings["output"]
1725
+ if mapping.parent_slot == parent_output_slot_name
1726
+ ]
1727
+ for mapping in mappings_from_parent:
1728
+ child_node = mapping.child_node
1729
+ child_slot = mapping.child_slot
1730
+ # Assign the aggregator to the appropriate child slot
1731
+ if child_slot in child.output_slots and child_node == child.name:
1732
+ child.output_slots[child_slot].aggregator = parent_output_slot.aggregator
1733
+
1734
+ def add_edges_to_implementation_graph(
1735
+ self, implementation_graph: ImplementationGraph
1736
+ ) -> None:
1737
+ """Adds the edges of this ``Step's`` ``Implementations`` to the ``ImplementationGraph``.
1601
1738
 
1602
- Notes
1603
- -----
1604
- This method is first called on the entire :class:`~easylink.pipeline_schema.PipelineSchema`
1605
- when constructing the :class:`~easylink.pipeline_graph.PipelineGraph`
1606
- to run.
1739
+ This method does two things:
1740
+ 1. Adds the edges *at this level* (i.e. at the ``Step`` tied to this
1741
+ ``NonLeafConfigurationState``) to the ``ImplementationGraph``.
1742
+ 2. Recursively traverses all sub-steps and adds their edges to the
1743
+ ``ImplementationGraph``.
1607
1744
 
1745
+ Note that to achieve (1), edges must be mapped from being between steps at
1746
+ this level of the hierarchy, all the way down to being between concrete implementations.
1747
+ Mapping each edge down to the implementation level is *itself* a recursive
1748
+ operation (see ``get_implementation_edges``).
1608
1749
  """
1609
- implementation_graph = ImplementationGraph()
1610
- self.add_nodes(implementation_graph)
1611
- self.add_edges(implementation_graph)
1612
- return implementation_graph
1613
-
1614
- def add_nodes(self, implementation_graph: ImplementationGraph) -> None:
1615
- """Adds nodes for each ``Step`` to the ``ImplementationGraph``."""
1616
- for node in self._nodes:
1617
- step = self._nodes[node]["step"]
1618
- implementation_graph.update(step.get_implementation_graph())
1619
-
1620
- def add_edges(self, implementation_graph: ImplementationGraph) -> None:
1621
- """Adds the edges to the ``ImplementationGraph``."""
1750
+ # Add the edges at this level (i.e. the edges at this `self._step`)
1622
1751
  for source, target, edge_attrs in self._step.step_graph.edges(data=True):
1623
- all_edges = []
1624
1752
  edge = EdgeParams.from_graph_edge(source, target, edge_attrs)
1625
- parent_source_step = self._nodes[source]["step"]
1626
- parent_target_step = self._nodes[target]["step"]
1753
+ source_step = self._step.step_graph.nodes[source]["step"]
1754
+ target_step = self._step.step_graph.nodes[target]["step"]
1627
1755
 
1628
- source_edges = parent_source_step.get_implementation_edges(edge)
1756
+ source_edges = source_step.get_implementation_edges(edge)
1629
1757
  for source_edge in source_edges:
1630
- for target_edge in parent_target_step.get_implementation_edges(source_edge):
1631
- all_edges.append(target_edge)
1758
+ for target_edge in target_step.get_implementation_edges(source_edge):
1759
+ implementation_graph.add_edge_from_params(target_edge)
1632
1760
 
1633
- for edge in all_edges:
1634
- implementation_graph.add_edge_from_params(edge)
1761
+ # Recurse through all sub-steps and add the edges between them
1762
+ for node in self._step.step_graph.nodes:
1763
+ substep = self._step.step_graph.nodes[node]["step"]
1764
+ substep.add_edges_to_implementation_graph(implementation_graph)
1635
1765
 
1636
1766
  def get_implementation_edges(self, edge: EdgeParams) -> list[EdgeParams]:
1637
- """Gets the edge information for the ``Implementation`` related to this ``Step``.
1767
+ """Gets the edges for the ``Implementation`` related to this ``Step``.
1768
+
1769
+ This method maps an edge between ``Steps`` in this ``Step's`` ``StepGraph``
1770
+ to one or more edges between ``Implementations`` by applying ``SlotMappings``.
1638
1771
 
1639
1772
  Parameters
1640
1773
  ----------
1641
1774
  edge
1642
- The ``Step's`` edge information to be propagated to the ``ImplementationGraph``.
1775
+ The edge information of the edge in the ``StepGraph`` to be mapped to
1776
+ the ``Implementation`` level.
1643
1777
 
1644
1778
  Raises
1645
1779
  ------
@@ -1648,7 +1782,28 @@ class NonLeafConfigurationState(ConfigurationState):
1648
1782
 
1649
1783
  Returns
1650
1784
  -------
1651
- The ``Implementation's`` edge information.
1785
+ A list of edges between ``Implementations`` which are ready to add to
1786
+ the ``ImplementationGraph``.
1787
+
1788
+ Notes
1789
+ -----
1790
+ In EasyLink, an edge (in either a ``StepGraph`` or ``ImplementationGraph``)
1791
+ sconnects two ``Slot``.
1792
+
1793
+ The core of this method is to map the ``Slots`` on the ``StepGraph`` edge
1794
+ to the corresponding ``Slots`` on ``Implementations``.
1795
+
1796
+ At each level in the step hierarchy, ``SlotMappings`` indicate how to map
1797
+ a ``Slot`` to the level below in the hierarchy.
1798
+
1799
+ This method recurses through the step hierarchy until it reaches the leaf
1800
+ ``Steps`` relevant to this edge in order to compose all the ``SlotMappings``
1801
+ that should apply to it.
1802
+
1803
+ Because a single ``Step`` can become multiple nodes in the ``ImplementationGraph``
1804
+ (e.g. a :class:`TemplatedStep`), a single edge between ``Steps`` may actually
1805
+ become multiple edges between ``Implementations``, which is why this method
1806
+ can return a list.
1652
1807
  """
1653
1808
  implementation_edges = []
1654
1809
  if edge.source_node == self._step.name:
@@ -1659,7 +1814,7 @@ class NonLeafConfigurationState(ConfigurationState):
1659
1814
  ]
1660
1815
  for mapping in mappings:
1661
1816
  new_edge = mapping.remap_edge(edge)
1662
- new_step = self._nodes[mapping.child_node]["step"]
1817
+ new_step = self._step.step_graph.nodes[mapping.child_node]["step"]
1663
1818
  imp_edges = new_step.get_implementation_edges(new_edge)
1664
1819
  implementation_edges.extend(imp_edges)
1665
1820
  elif edge.target_node == self._step.name:
@@ -1670,7 +1825,7 @@ class NonLeafConfigurationState(ConfigurationState):
1670
1825
  ]
1671
1826
  for mapping in mappings:
1672
1827
  new_edge = mapping.remap_edge(edge)
1673
- new_step = self._nodes[mapping.child_node]["step"]
1828
+ new_step = self._step.step_graph.nodes[mapping.child_node]["step"]
1674
1829
  imp_edges = new_step.get_implementation_edges(new_edge)
1675
1830
  implementation_edges.extend(imp_edges)
1676
1831
  else:
@@ -1685,12 +1840,14 @@ class NonLeafConfigurationState(ConfigurationState):
1685
1840
  This method recursively traverses the ``StepGraph`` and sets the configuration
1686
1841
  state for each ``Step`` until reaching all leaf nodes.
1687
1842
  """
1688
- for node in self._nodes:
1689
- step = self._nodes[node]["step"]
1843
+ for sub_node in self._step.step_graph.nodes:
1844
+ sub_step = self._step.step_graph.nodes[sub_node]["step"]
1690
1845
  # IOStep names never appear in configuration
1691
1846
  step_config = (
1692
- self.step_config if isinstance(step, IOStep) else self.step_config[step.name]
1847
+ self.step_config
1848
+ if isinstance(sub_step, IOStep)
1849
+ else self.step_config[sub_step.name]
1693
1850
  )
1694
- step.set_configuration_state(
1851
+ sub_step.set_configuration_state(
1695
1852
  step_config, self.combined_implementations, self.input_data_config
1696
1853
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easylink
3
- Version: 0.1.10
3
+ Version: 0.1.12
4
4
  Summary: Research repository for the EasyLink ER ecosystem project.
5
5
  Home-page: https://github.com/ihmeuw/easylink
6
6
  Author: The EasyLink developers
@@ -1,6 +1,6 @@
1
1
  easylink/__about__.py,sha256=2-oxCfu9t9yUJouLDwqYRZ0eii8kN25SxRzsawjWjho,440
2
2
  easylink/__init__.py,sha256=gGMcIVfiVnHtlDw5mZwhevcDb2wt-kuP6F64gnkFack,159
3
- easylink/_version.py,sha256=z0zCHFTcKSR0tJ6h5qrpNmRVP21QIPP8N0p7quCnnm0,23
3
+ easylink/_version.py,sha256=LcIlFjHZFfiF9Rd4UHoakmombOFkxIYk00I181frGBM,23
4
4
  easylink/cli.py,sha256=ARSKAljepNOEYd1VCS_QqBJQIBLzE3IgKiOb5-OROdY,6380
5
5
  easylink/configuration.py,sha256=Ire2pMZNZ6wtSwhcWnQpYa-snX4KrhXgovlQwQ2Wxf4,12530
6
6
  easylink/graph_components.py,sha256=PhMKxpgZjorhubS7vcta1pgXgXSGplmPulQpV0YZhqo,14811
@@ -8,14 +8,14 @@ easylink/implementation.py,sha256=AwGl5YCKCSQo91owWj-gg9_5lBz7H_4q2z7jF0BhXs4,89
8
8
  easylink/implementation_metadata.yaml,sha256=VvlEu3Dvlmeh1MpzeYx91j22GiV-9mu3hZP5yVuW04o,6763
9
9
  easylink/pipeline.py,sha256=EyCXv5p9WzTqcndXK6ukBJE6jY_fWIP_DGZQUl1wRcY,12284
10
10
  easylink/pipeline_graph.py,sha256=vsY6nW_iEwZCNf_N_3CsixsKBUy_5JxGEi61-1Q-KAw,22842
11
- easylink/pipeline_schema.py,sha256=kINpvy2Fl2S3FBqgdgZCCFHEk237_36X4ltLOtk5-dE,5862
11
+ easylink/pipeline_schema.py,sha256=Q2sCpsC-F2W0yxVP7ufunowDepOBrRVENXOdap9J5iY,6921
12
12
  easylink/rule.py,sha256=W97LMI-vkEPipJbnSZLn2BxfYfFtvzGTKzq6YgDVri0,19913
13
13
  easylink/runner.py,sha256=k9ICTToHj2xr6MGIuvlWf6YMeZ47UGgseaMByMgUGac,6271
14
- easylink/step.py,sha256=8EhoFOXBLWgDfb3OhmQu5g03fqElIJCWg8-Y_5azKEA,67100
14
+ easylink/step.py,sha256=Hweg1OAGcmrNAt95C-M4ksOAtc_db0oeibbF3cnqhq0,74951
15
15
  easylink/images/spark_cluster/Dockerfile,sha256=3PHotbR4jdjVYRHOJ0VQW55b5Qd4tQ1pLLQMrTKWVA0,576
16
16
  easylink/images/spark_cluster/README.md,sha256=KdgSttZRplNNWqHn4K1GTsTIab3dTOSG4V99QPLxSp8,569
17
17
  easylink/pipeline_schema_constants/__init__.py,sha256=uRVjQw7_Ff5IBQw0_Jc93Fzfa-MnbPVPKsy18CCaW7E,1021
18
- easylink/pipeline_schema_constants/development.py,sha256=kOTEqfZD5pWqP9gu7E6r9Cubf3ILtWEUxCfJfrN8znc,11547
18
+ easylink/pipeline_schema_constants/development.py,sha256=0fc6xWRBr5e_xDaldR9sY2vMQJU1wnlhDQS_-yUOT6g,12339
19
19
  easylink/pipeline_schema_constants/testing.py,sha256=ohcTlT_viZYxS1GkO46mjkb8IzXo6yIOqvBbb4YrOhA,10897
20
20
  easylink/steps/dev/README.md,sha256=u9dZUggpY2Lf2qb-xkDLWWgHjcmi4osbQtzSNo4uklE,4549
21
21
  easylink/steps/dev/build-containers-local.sh,sha256=Wy3pfcyt7I-BNvHcr7ZXDe0g5Ihd00BIPqt9YuRbLeA,259
@@ -43,8 +43,8 @@ easylink/utilities/paths.py,sha256=KM1GlnsAcKbUJrC4LZKpeJfPljxe_aXP1ZhVp43TYRA,9
43
43
  easylink/utilities/spark.smk,sha256=tQ7RArNQzhjbaBQQcRORB4IxxkuDx4gPHUBcWHDYJ_U,5795
44
44
  easylink/utilities/splitter_utils.py,sha256=y4CbbTBgRaoXFxy-9Eu5eWx4lA4ZEcbrYpxgLIzG_kc,2602
45
45
  easylink/utilities/validation_utils.py,sha256=W9r_RXcivJjfpioLhONirfwdByYttxNsVY489_sbrYQ,1683
46
- easylink-0.1.10.dist-info/METADATA,sha256=wPKznMaCPytiFoECbqY0jcFra-Hl28FMQJLo4XQcwdo,2805
47
- easylink-0.1.10.dist-info/WHEEL,sha256=DK49LOLCYiurdXXOXwGJm6U4DkHkg4lcxjhqwRa0CP4,91
48
- easylink-0.1.10.dist-info/entry_points.txt,sha256=OGMZDFltg3yMboT7XjJt3joiPhRfV_7jnREVtrAIQNU,51
49
- easylink-0.1.10.dist-info/top_level.txt,sha256=oHcOpcF_jDMWFiJRzfGQvuskENGDjSPC_Agu9Z_Xvik,9
50
- easylink-0.1.10.dist-info/RECORD,,
46
+ easylink-0.1.12.dist-info/METADATA,sha256=jBuzMoJJREYmGnXNbuUEmHs_SgqcibeIcKiTxjZ1Ky8,2805
47
+ easylink-0.1.12.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
+ easylink-0.1.12.dist-info/entry_points.txt,sha256=OGMZDFltg3yMboT7XjJt3joiPhRfV_7jnREVtrAIQNU,51
49
+ easylink-0.1.12.dist-info/top_level.txt,sha256=oHcOpcF_jDMWFiJRzfGQvuskENGDjSPC_Agu9Z_Xvik,9
50
+ easylink-0.1.12.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.0.2)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5