From 58278636fd5ad2c2982facf1471d3db11db8a6be Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:33:28 -0700 Subject: [PATCH 1/8] Add transformer module Co-authored-by: Claude Signed-off-by: Liam Huber --- src/flowrep/api/schemas.py | 2 + src/flowrep/transformers.py | 116 ++++++++++++++++++++++++++++++++ tests/unit/test_transformers.py | 52 ++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 src/flowrep/transformers.py create mode 100644 tests/unit/test_transformers.py diff --git a/src/flowrep/api/schemas.py b/src/flowrep/api/schemas.py index 7edeead7..e6225910 100644 --- a/src/flowrep/api/schemas.py +++ b/src/flowrep/api/schemas.py @@ -51,3 +51,5 @@ from flowrep.retrospective.datastructures import TryData as TryData from flowrep.retrospective.datastructures import WhileData as WhileData from flowrep.subgraph_validation import ProspectiveOutputEdges as ProspectiveOutputEdges +from flowrep.transformers import Transform1toN as Transform1toN +from flowrep.transformers import TransformNto1 as TransformNto1 diff --git a/src/flowrep/transformers.py b/src/flowrep/transformers.py new file mode 100644 index 00000000..ff8dfaaa --- /dev/null +++ b/src/flowrep/transformers.py @@ -0,0 +1,116 @@ +""" +Atomic recipes that materialize fan-out and fan-in as real nodes. + +A for-node scatters one input collection across many body instances and gathers their +outputs back into one list. The edge models describe strictly 1:1 connections -- +:data:`~flowrep.edge_models.OutputEdges` admits exactly one source per output target -- +so a WfMS records that fan-out and fan-in honestly by building nodes for it, rather +than leaving it implicit and unrecordable. + +These mirror ``pyiron_workflow.transformers`` deliberately, down to the port labels and +function names, so that records produced by different WfMS describe the same shape. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, ClassVar + +from pyiron_snippets import versions + +from flowrep import base_models +from flowrep.prospective import atomic_recipe + + +class Transform1toN: + """ + Scatter one iterable input across ``n`` output ports. + + Attributes: + input_label: The name of the sole input port. + + Args: + n: How many output ports to scatter into. Must be at least 1. + """ + + input_label: ClassVar[base_models.Label] = "items" + + @staticmethod + def output_label(i: int) -> base_models.Label: + return f"output_{i}" + + @staticmethod + def iterable_to_outputs(items, /): + return tuple(items) + + @staticmethod + def iterable_to_output(items, /): + return items[0] + + def __init__(self, n: int): + if n < 1: + raise ValueError(f"Cannot scatter into {n} outputs; need at least 1.") + self.n = n + + @property + def _function(self) -> Callable[[Any], Any]: + """ + A recipe declaring exactly one output receives the whole return value, so a + 1-wide scatter must return the element rather than a 1-tuple. + """ + return self.iterable_to_output if self.n == 1 else self.iterable_to_outputs + + @property + def recipe(self) -> atomic_recipe.AtomicRecipe: + return atomic_recipe.AtomicRecipe( + reference=base_models.PythonReference( + info=versions.VersionInfo.of(self._function), + restricted_input_kinds={ + self.input_label: base_models.RestrictedParamKind.POSITIONAL_ONLY + }, + ), + inputs=[self.input_label], + outputs=[self.output_label(i) for i in range(self.n)], + ) + + +class TransformNto1: + """ + Gather ``n`` input ports into one list output. + + Attributes: + output_label: The name of the sole output port. + + Args: + n: How many input ports to gather. Zero is legal and yields an empty list, + which is what an empty iterated input needs. + """ + + output_label: ClassVar[base_models.Label] = "output_0" + + @staticmethod + def input_label(i: int) -> base_models.Label: + return f"item_{i}" + + @staticmethod + def inputs_to_list(*items): + return list(items) + + def __init__(self, n: int): + if n < 0: + raise ValueError(f"Cannot gather {n} inputs; need at least 0.") + self.n = n + + @property + def recipe(self) -> atomic_recipe.AtomicRecipe: + return atomic_recipe.AtomicRecipe( + reference=base_models.PythonReference( + info=versions.VersionInfo.of(self.inputs_to_list), + restricted_input_kinds={ + self.input_label(i): base_models.RestrictedParamKind.POSITIONAL_ONLY + for i in range(self.n) + }, + ), + inputs=[self.input_label(i) for i in range(self.n)], + outputs=[self.output_label], + ) diff --git a/tests/unit/test_transformers.py b/tests/unit/test_transformers.py new file mode 100644 index 00000000..eede6a56 --- /dev/null +++ b/tests/unit/test_transformers.py @@ -0,0 +1,52 @@ +"""Tests for the scatter/gather transformer recipes.""" + +from __future__ import annotations + +import unittest + +from flowrep import transformers, wfms + + +class TestTransform1toN(unittest.TestCase): + def test_recipe_shape(self): + recipe = transformers.Transform1toN(3).recipe + self.assertEqual(recipe.inputs, ["items"]) + self.assertEqual(recipe.outputs, ["output_0", "output_1", "output_2"]) + + def test_scatters(self): + data = wfms.run_recipe(transformers.Transform1toN(3).recipe, items=[10, 20, 30]) + self.assertEqual( + [port.value for port in data.output_ports.values()], [10, 20, 30] + ) + + def test_single_output_receives_the_element_not_a_tuple(self): + """A recipe declaring exactly one output receives the whole return value, so a + 1-wide scatter must return the element itself.""" + data = wfms.run_recipe(transformers.Transform1toN(1).recipe, items=[42]) + self.assertEqual(data.output_ports["output_0"].value, 42) + + def test_zero_outputs_raises(self): + with self.assertRaisesRegex(ValueError, "at least 1"): + transformers.Transform1toN(0) + + +class TestTransformNto1(unittest.TestCase): + def test_recipe_shape(self): + recipe = transformers.TransformNto1(2).recipe + self.assertEqual(recipe.inputs, ["item_0", "item_1"]) + self.assertEqual(recipe.outputs, ["output_0"]) + + def test_gathers(self): + data = wfms.run_recipe( + transformers.TransformNto1(3).recipe, item_0=1, item_1=2, item_2=3 + ) + self.assertEqual(data.output_ports["output_0"].value, [1, 2, 3]) + + def test_zero_inputs_gathers_an_empty_list(self): + """The empty-iterable for-loop builds aggregators with nothing to gather.""" + data = wfms.run_recipe(transformers.TransformNto1(0).recipe) + self.assertEqual(data.output_ports["output_0"].value, []) + + def test_negative_raises(self): + with self.assertRaisesRegex(ValueError, "at least 0"): + transformers.TransformNto1(-1) From c254bd0631982e7ec8ee5d2b6535db64b7007ddb Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:41:45 -0700 Subject: [PATCH 2/8] Update wfms flow controllers to add edges And run the workflows by actually leveraging those edges in order to keep the claims and the results in lock-step Co-authored-by: Claude Signed-off-by: Liam Huber --- src/flowrep/drawing/retrospective.py | 7 +- src/flowrep/wfms.py | 458 ++++++++++++++++++----- tests/unit/drawing/test_render.py | 15 +- tests/unit/drawing/test_retrospective.py | 58 ++- tests/unit/test_retrospective_wfms.py | 306 ++++++++++++++- 5 files changed, 736 insertions(+), 108 deletions(-) diff --git a/src/flowrep/drawing/retrospective.py b/src/flowrep/drawing/retrospective.py index fc38387a..3be26c0f 100644 --- a/src/flowrep/drawing/retrospective.py +++ b/src/flowrep/drawing/retrospective.py @@ -67,9 +67,10 @@ def _note( ) -> str | None: """A user-facing note when a composite expanded with children but no edges. - The toy WfMS never records actualized edges for flow-control instances - (only ``DagData`` copies edges from its recipe), so a run flow-control node - otherwise draws as a set of unconnected boxes. + The toy WfMS records actualized edges for the flow-control nodes it runs, but the + retrospective format does not oblige every WfMS to, so a composite can still + arrive with children and no wiring. Say so, rather than drawing a set of + unconnected boxes that reads as a rendering fault. """ if children and not edges: return "(no recorded edges)" diff --git a/src/flowrep/wfms.py b/src/flowrep/wfms.py index 882b0dc4..0ca802ab 100644 --- a/src/flowrep/wfms.py +++ b/src/flowrep/wfms.py @@ -8,13 +8,13 @@ from __future__ import annotations -import itertools +import math from collections.abc import Collection from typing import Any, cast from pyiron_snippets import retrieve -from flowrep import base_models, edge_models, subgraph_validation +from flowrep import base_models, edge_models, subgraph_validation, transformers from flowrep.parsers import label_helpers from flowrep.prospective import ( atomic_recipe, @@ -144,12 +144,18 @@ def _run_workflow( _populate_input_ports(node, kwargs) for child_label in _topo_sort_children(recipe): - child_inputs = _gather_child_inputs(child_label, recipe, node) child_recipe = recipe.nodes[child_label] + child_inputs = _gather_child_inputs( + child_label, + child_recipe.inputs, + recipe.input_edges, + recipe.edges, + node, + ) child_node = run_recipe(child_recipe, **child_inputs) node.nodes[child_label] = child_node # Overwrite with _executed_ child - _populate_workflow_outputs(node, recipe) + _populate_outputs_from_edges(node, recipe.output_edges) return node @@ -168,44 +174,49 @@ def _topo_sort_children(recipe: workflow_recipe.WorkflowRecipe) -> list[str]: def _gather_child_inputs( - child_label: str, - recipe: workflow_recipe.WorkflowRecipe, - workflow_node: datastructures.DagData, + child_label: base_models.Label, + child_inputs: base_models.Labels, + input_edges: edge_models.InputEdges, + edges: edge_models.Edges, + parent: datastructures.CompositeData, ) -> dict[str, Any]: """ - Resolve input values for a child node from workflow input ports and sibling - output ports according to the recipe edges. + Resolve input values for a child node from the parent's input ports and sibling + output ports, according to the given edges. - Ports not covered by any edge are omitted — the child's own defaults (if any) + Ports not covered by any edge are omitted -- the child's own defaults (if any) will be used downstream. + + Taking the edges explicitly rather than reading them off a recipe is what lets a + for-node, whose edges only exist at runtime, share this resolution path with a + static workflow. """ - child_recipe = recipe.nodes[child_label] inputs: dict[str, Any] = {} - for port in child_recipe.inputs: + for port in child_inputs: th = edge_models.TargetHandle(node=child_label, port=port) - if th in recipe.input_edges: - parent_source = recipe.input_edges[th] - inputs[port] = workflow_node.input_ports[parent_source.port].get_data() - elif th in recipe.edges: - sibling_source = recipe.edges[th] - sibling = workflow_node.nodes[sibling_source.node] + if th in input_edges: + inputs[port] = parent.input_ports[input_edges[th].port].get_data() + elif th in edges: + sibling_source = edges[th] + sibling = parent.nodes[sibling_source.node] inputs[port] = sibling.output_ports[sibling_source.port].value # else: port has a default on the child, _call_atomic will handle it return inputs -def _populate_workflow_outputs( - node: datastructures.DagData, recipe: workflow_recipe.WorkflowRecipe +def _populate_outputs_from_edges( + node: datastructures.CompositeData, + output_edges: edge_models.OutputEdges, ) -> None: - for target, source in recipe.output_edges.items(): - if isinstance(source, edge_models.InputSource): + """Fill the composite's own output ports by following its output edges.""" + for target, source in output_edges.items(): + if source.node is None: val = node.input_ports[source.port].get_data() else: - child = node.nodes[source.node] - val = child.output_ports[source.port].value + val = node.nodes[source.node].output_ports[source.port].value node.output_ports[target.port].value = val @@ -233,96 +244,346 @@ def _iterated_value( return cast(Collection, value) +def _scatter_label(parent_port: base_models.Label) -> base_models.Label: + return f"scatter_{parent_port}" + + +def _aggregate_label(output_port: base_models.Label) -> base_models.Label: + return f"aggregate_{output_port}" + + +def _body_to_parent_ports( + recipe: for_recipe.ForEachRecipe, body_ports: base_models.Labels +) -> dict[base_models.Label, base_models.Label]: + """Map iterated body port names to the for-node input ports that feed them.""" + body_label = recipe.body_node.label + return { + port: recipe.input_edges[ + edge_models.TargetHandle(node=body_label, port=port) + ].port + for port in body_ports + } + + +def _nested_strides( + total_steps: int, nested_lengths: dict[base_models.Label, int] +) -> dict[base_models.Label, int]: + """ + Per-port strides for mixed-radix decomposition of the body index. + + Nested ports are the outer dimensions, in ``nested_ports`` order; zipped ports + occupy the innermost dimension (stride 1) and are not represented here. + """ + if total_steps == 0: + return {} # No body indices exist, so there is nothing to decompose + + strides: dict[base_models.Label, int] = {} + running = total_steps + for parent_port, length in nested_lengths.items(): + running //= length + strides[parent_port] = running + return strides + + +def _guard_generated_labels( + recipe: for_recipe.ForEachRecipe, + scattered: dict[base_models.Label, int], + total_steps: int, +) -> None: + """ + The for-node invents labels for its scatter, body and aggregate instances, and + nothing in the recipe validators knows about them. Two generated labels coinciding + would silently drop a node from the record; a body node named ``aggregate_ys`` + sitting beside a generated ``aggregate_ys_0`` would merely be unreadable. Refuse + both, rather than corrupting the record or handing back a graph nobody can follow. + """ + body_label = recipe.body_node.label + generated = ( + [_scatter_label(port) for port in scattered] + + [body_label] + + [label_helpers.index_label(body_label, i) for i in range(total_steps)] + + [_aggregate_label(port) for port in recipe.outputs] + ) + seen: set[base_models.Label] = set() + for label in generated: + if label in seen: + raise ValueError( + f"For-node label collision: '{label}' would name two different " + f"instances. Rename the body node or the colliding port." + ) + seen.add(label) + + +def _guard_distinct_iterated_sources( + nested_map: dict[base_models.Label, base_models.Label], + zipped_map: dict[base_models.Label, base_models.Label], +) -> None: + """ + One scatter node is built per *parent* port, so two iterated body ports fed by the + same parent port would share a scatter and therefore an index -- collapsing what + used to be two independent axes into one. Refuse rather than silently change what + the loop iterates over. + """ + parents = list(nested_map.values()) + list(zipped_map.values()) + duplicated = {port for port in parents if parents.count(port) > 1} + if duplicated: + raise ValueError( + f"Iterated body ports must draw from distinct for-node inputs, but " + f"{sorted(duplicated)} feeds more than one. Duplicate the input port to " + f"iterate over the same data on two axes." + ) + + def _run_for( recipe: for_recipe.ForEachRecipe, **kwargs: Any ) -> datastructures.ForEachData: """ - Execute a for-node by scattering iterated inputs across body instances and - collecting outputs into lists. + Execute a for-node as a real DAG: scatter nodes fan the iterated inputs out across + body instances, and aggregate nodes gather their outputs back into lists. Nested ports drive a Cartesian product; zipped ports are iterated in lockstep. Broadcast (non-iterated) inputs are passed unchanged to every body instance. - Transferred outputs collect the per-iteration value of a scattered input, - preserving the link between input element and body output element. + Transferred outputs collect the per-iteration value of a scattered input, so they + are sourced from the scatter node rather than from a body output. + + Execution follows the recorded edges rather than running alongside them, so the + record cannot drift from what actually happened. """ node = datastructures.ForEachData.from_recipe(recipe) _populate_input_ports(node, kwargs) body_label = recipe.body_node.label body_recipe = recipe.body_node.recipe - iterated_ports = recipe.iterated_ports - - # body iterated port -> for-node input name - body_to_for: dict[str, str] = {} - for port in iterated_ports: - th = edge_models.TargetHandle(node=body_label, port=port) - body_to_for[port] = recipe.input_edges[th].port - - # Reverse mapping for transferred outputs - for_to_body: dict[str, str] = {v: k for k, v in body_to_for.items()} - - # Broadcast inputs (non-iterated body ports sourced from for-node inputs) - broadcast: dict[str, Any] = {} - for port in body_recipe.inputs: - if port not in iterated_ports: - th = edge_models.TargetHandle(node=body_label, port=port) - if th in recipe.input_edges: - src = recipe.input_edges[th] - broadcast[port] = node.input_ports[src.port].value - - # Build iteration axes - nested_iters = [ - _iterated_value(node, body_to_for[p], p) for p in recipe.nested_ports - ] - zipped_iters = [ - _iterated_value(node, body_to_for[p], p) for p in recipe.zipped_ports - ] - # Note that beyond insisting the value arrived at all, we simply cast iterated - # input values to the form we expect, and let the user pay the price if runtime - # data is non-compliant. - - nested_combos = list(itertools.product(*nested_iters)) if nested_iters else [()] - if zipped_iters: - zip_len = len(zipped_iters[0]) - for zi in zipped_iters: - if len(zi) != zip_len: - raise ValueError("Zipped inputs must have equal lengths") - zipped_combos = list(zip(*zipped_iters, strict=True)) - else: - zipped_combos = [()] - accumulators: dict[str, list[Any]] = {port: [] for port in recipe.outputs} + nested_map = _body_to_parent_ports(recipe, recipe.nested_ports) + zipped_map = _body_to_parent_ports(recipe, recipe.zipped_ports) + _guard_distinct_iterated_sources(nested_map, zipped_map) - for nested_vals, zipped_vals in itertools.product(nested_combos, zipped_combos): - body_kwargs = dict(broadcast) - for port, val in zip(recipe.nested_ports, nested_vals, strict=True): - body_kwargs[port] = val - for port, val in zip(recipe.zipped_ports, zipped_vals, strict=True): - body_kwargs[port] = val + # Note that beyond insisting the value arrived at all, we simply take the length of + # iterated input values, and let the user pay the price if runtime data is + # non-compliant. + nested_lengths = { + parent_port: len(_iterated_value(node, parent_port, body_port)) + for body_port, parent_port in nested_map.items() + } + zipped_lengths = { + parent_port: len(_iterated_value(node, parent_port, body_port)) + for body_port, parent_port in zipped_map.items() + } + if len(set(zipped_lengths.values())) > 1: + raise ValueError("Zipped inputs must have equal lengths") + + zip_len = next(iter(zipped_lengths.values()), 1) + total_steps = math.prod(nested_lengths.values()) * zip_len + strides = _nested_strides(total_steps, nested_lengths) + + # An empty iterated port means zero body steps. Scatter nodes would then need zero + # outputs, which no atomic recipe can express -- and nothing would consume them + # anyway -- so only the aggregators get built, and they collect empty lists. + scattered = {**nested_lengths, **zipped_lengths} if total_steps > 0 else {} + _guard_generated_labels(recipe, scattered, total_steps) + + for parent_port, length in scattered.items(): + node.nodes[_scatter_label(parent_port)] = run_recipe( + transformers.Transform1toN(length).recipe, + **{ + transformers.Transform1toN.input_label: node.input_ports[ + parent_port + ].value + }, + ) - child = run_recipe(body_recipe, **body_kwargs) - idx = len(node.nodes) - node.nodes[label_helpers.index_label(body_label, idx)] = child + node.input_edges = _for_input_edges(recipe, scattered, total_steps) + node.edges = _for_edges( + recipe, + {**nested_map, **zipped_map}, + nested_lengths, + strides, + zip_len, + total_steps, + ) + node.output_edges = _for_output_edges(recipe) + + for i in range(total_steps): + instance_label = label_helpers.index_label(body_label, i) + node.nodes[instance_label] = run_recipe( + body_recipe, + **_gather_child_inputs( + instance_label, + body_recipe.inputs, + node.input_edges, + node.edges, + node, + ), + ) - for target, source in recipe.output_edges.items(): - if isinstance(source, edge_models.SourceHandle): - accumulators[target.port].append(child.output_ports[source.port].value) - else: - # Transferred output: collect the scattered input element - body_port = for_to_body[source.port] - accumulators[target.port].append(body_kwargs[body_port]) + for output_port in recipe.outputs: + aggregate_recipe = transformers.TransformNto1(total_steps).recipe + label = _aggregate_label(output_port) + node.nodes[label] = run_recipe( + aggregate_recipe, + **_gather_child_inputs( + label, + aggregate_recipe.inputs, + node.input_edges, + node.edges, + node, + ), + ) - for port, values in accumulators.items(): - node.output_ports[port].value = values + _populate_outputs_from_edges(node, node.output_edges) return node +def _for_input_edges( + recipe: for_recipe.ForEachRecipe, + scattered: dict[base_models.Label, int], + total_steps: int, +) -> edge_models.InputEdges: + """ + Parent inputs feed the scatter nodes, and broadcast inputs feed every body + instance directly. Iterated ports are deliberately absent: they reach the bodies + through a scatter node, which is the whole point of building one. + """ + body_label = recipe.body_node.label + input_edges: edge_models.InputEdges = { + edge_models.TargetHandle( + node=_scatter_label(parent_port), + port=transformers.Transform1toN.input_label, + ): edge_models.InputSource(port=parent_port) + for parent_port in scattered + } + for target, source in recipe.input_edges.items(): + if target.port in recipe.iterated_ports: + continue + for i in range(total_steps): + input_edges[ + edge_models.TargetHandle( + node=label_helpers.index_label(body_label, i), port=target.port + ) + ] = edge_models.InputSource(port=source.port) + return input_edges + + +def _for_edges( + recipe: for_recipe.ForEachRecipe, + iterated_map: dict[base_models.Label, base_models.Label], + nested_lengths: dict[base_models.Label, int], + strides: dict[base_models.Label, int], + zip_len: int, + total_steps: int, +) -> edge_models.Edges: + """Scatters to bodies, and bodies (or scatters) to aggregators.""" + body_label = recipe.body_node.label + + def scatter_source( + parent_port: base_models.Label, i: int + ) -> edge_models.SourceHandle: + if parent_port in nested_lengths: + index = (i // strides[parent_port]) % nested_lengths[parent_port] + else: + index = i % zip_len + return edge_models.SourceHandle( + node=_scatter_label(parent_port), + port=transformers.Transform1toN.output_label(index), + ) + + edges: edge_models.Edges = { + edge_models.TargetHandle( + node=label_helpers.index_label(body_label, i), port=body_port + ): scatter_source(parent_port, i) + for body_port, parent_port in iterated_map.items() + for i in range(total_steps) + } + + for target, source in recipe.output_edges.items(): + aggregate = _aggregate_label(target.port) + for i in range(total_steps): + handle = edge_models.TargetHandle( + node=aggregate, port=transformers.TransformNto1.input_label(i) + ) + if source.node is None: + # Transferred output: the scattered input element itself + edges[handle] = scatter_source(source.port, i) + else: + edges[handle] = edge_models.SourceHandle( + node=label_helpers.index_label(body_label, i), port=source.port + ) + return edges + + +def _for_output_edges(recipe: for_recipe.ForEachRecipe) -> edge_models.OutputEdges: + return { + edge_models.OutputTarget(port=port): edge_models.SourceHandle( + node=_aggregate_label(port), + port=transformers.TransformNto1.output_label, + ) + for port in recipe.outputs + } + + # --------------------------------------------------------------------------- # While # --------------------------------------------------------------------------- +def _record_while_child_edges( + node: datastructures.WhileData, + recipe: while_recipe.WhileRecipe, + child_label: base_models.Label, + iteration: int, +) -> None: + """ + Record where one condition or body instance's inputs actually came from. + + Iteration 0 draws everything from the while-node's own inputs. Later iterations + draw the looped ports from the previous body instance, and the rest -- ports the + body never writes back -- from the while-node's inputs, still. + """ + body_label = recipe.case.body.label + looped = ( + recipe.body_condition_edges + if child_label == recipe.case.condition.label + else recipe.body_body_edges + ) + instance = label_helpers.index_label(child_label, iteration) + + for target, source in recipe.input_edges.items(): + if target.node != child_label: + continue + retargeted = edge_models.TargetHandle(node=instance, port=target.port) + if iteration > 0 and target in looped: + node.edges[retargeted] = edge_models.SourceHandle( + node=label_helpers.index_label(body_label, iteration - 1), + port=looped[target].port, + ) + else: + node.input_edges[retargeted] = edge_models.InputSource(port=source.port) + + +def _record_while_output_edges( + node: datastructures.WhileData, + recipe: while_recipe.WhileRecipe, + body_runs: int, +) -> None: + """ + Record where the while-node's outputs actually came from: the last body instance, + or -- if the body never ran -- the while-node's own inputs passed straight + through, which validation guarantees is possible since outputs are a subset of + inputs. + """ + body_label = recipe.case.body.label + for target, source in recipe.output_edges.items(): + if body_runs > 0: + node.output_edges[target] = edge_models.SourceHandle( + node=label_helpers.index_label(body_label, body_runs - 1), + port=source.port, + ) + else: + node.output_edges[target] = edge_models.InputSource(port=target.port) + + def _run_while( recipe: while_recipe.WhileRecipe, **kwargs: Any ) -> datastructures.WhileData: @@ -349,6 +610,7 @@ def _run_while( iteration = 0 while True: # --- condition --- + _record_while_child_edges(node, recipe, cond_label, iteration) cond_kwargs = _gather_dynamic_child_inputs( cond_label, recipe.input_edges, current ) @@ -359,6 +621,7 @@ def _run_while( break # --- body --- + _record_while_child_edges(node, recipe, body_label, iteration) body_kwargs = _gather_dynamic_child_inputs( body_label, recipe.input_edges, current ) @@ -371,6 +634,8 @@ def _run_while( iteration += 1 + _record_while_output_edges(node, recipe, iteration) + for name in recipe.outputs: node.output_ports[name].value = current[name] @@ -402,12 +667,14 @@ def _run_if(recipe: if_recipe.IfRecipe, **kwargs: Any) -> datastructures.IfData: if _evaluate_condition(case, cond_node): _execute_if_branch(node, recipe, case.body) + _record_executed_input_edges(node, recipe.input_edges) return node # No case matched — try else if recipe.else_case is not None: _execute_if_branch(node, recipe, recipe.else_case) + _record_executed_input_edges(node, recipe.input_edges) return node @@ -446,6 +713,7 @@ def _run_try(recipe: try_recipe.TryRecipe, **kwargs: Any) -> datastructures.TryD _populate_prospective_outputs( node, recipe.prospective_output_edges, recipe.try_node.label ) + _record_executed_input_edges(node, recipe.input_edges) return node except BaseException as exc: for case in recipe.exception_cases: @@ -462,6 +730,7 @@ def _run_try(recipe: try_recipe.TryRecipe, **kwargs: Any) -> datastructures.TryD _populate_prospective_outputs( node, recipe.prospective_output_edges, case.body.label ) + _record_executed_input_edges(node, recipe.input_edges) return node raise @@ -515,6 +784,20 @@ def _evaluate_condition( return bool(cond_node.output_ports[output_name].value) +def _record_executed_input_edges( + node: datastructures.IfData | datastructures.TryData, + input_edges: edge_models.InputEdges, +) -> None: + """ + Record input edges for the children that actually ran -- the conditions evaluated + plus the branch or handler chosen. The recipe describes every branch that might + have run; the record describes the one that did. + """ + for target, source in input_edges.items(): + if target.node in node.nodes: + node.input_edges[target] = source + + def _populate_prospective_outputs( node: datastructures.IfData | datastructures.TryData, prospective_output_edges: dict[ @@ -530,6 +813,7 @@ def _populate_prospective_outputs( node.output_ports[target.port].value = child.output_ports[ source.port ].value + node.output_edges[target] = source break diff --git a/tests/unit/drawing/test_render.py b/tests/unit/drawing/test_render.py index b839e8ca..26d65fbb 100644 --- a/tests/unit/drawing/test_render.py +++ b/tests/unit/drawing/test_render.py @@ -277,12 +277,25 @@ def test_badge_wins_when_both_set(self): @unittest.skipUnless(_has_graphviz, "graphviz not installed") class TestRetrospectiveNote(unittest.TestCase): def test_no_recorded_edges_note_appears_in_cluster_label(self): - """The known WfMS limitation surfaces as an italic note on the cluster.""" + """A composite with children but no wiring surfaces an italic note. + + The toy WfMS records actualized edges, so this is not reachable through it. + The retrospective format does not oblige every WfMS to record them, though, + so stripping the edges back off a run node stands in for one that doesn't. + """ data = wfms.run_recipe(_for_recipe(), xs=[1, 2, 3]) + data.input_edges = {} + data.edges = {} + data.output_edges = {} graph = retrospective.build(data, depth=0) source = render.render(graph).source self.assertIn("no recorded edges", source) + def test_no_note_when_the_wfms_records_its_edges(self): + data = wfms.run_recipe(_for_recipe(), xs=[1, 2, 3]) + graph = retrospective.build(data, depth=0) + self.assertNotIn("no recorded edges", render.render(graph).source) + @unittest.skipUnless(_has_graphviz, "graphviz not installed") class TestActuallyRenders(unittest.TestCase): diff --git a/tests/unit/drawing/test_retrospective.py b/tests/unit/drawing/test_retrospective.py index 56bc9930..8143b09d 100644 --- a/tests/unit/drawing/test_retrospective.py +++ b/tests/unit/drawing/test_retrospective.py @@ -201,35 +201,61 @@ def test_passthrough_source_is_the_parent_input(self): class TestExecutedFlowControl(unittest.TestCase): - """A run for-node has instance children but, per the known limitation, no edges.""" + """A run for-node draws its materialized scatter/body/aggregate DAG.""" def setUp(self): self.data = wfms.run_recipe(_for_recipe(), xs=[1, 2, 3]) self.graph = retrospective.build(self.data, depth=0) - def test_instance_children_present(self): - self.assertEqual(len(self.graph.children), 3) - - def test_instance_paths_are_lexical(self): + def test_children_include_the_transformer_nodes(self): self.assertEqual( - sorted(c.path for c in self.graph.children), ["body_0", "body_1", "body_2"] + sorted(c.path for c in self.graph.children), + ["aggregate_ys", "body_0", "body_1", "body_2", "scatter_xs"], ) - def test_note_explains_the_missing_edges(self): - self.assertEqual(self.graph.note, "(no recorded edges)") + def test_edges_are_drawn(self): + self.assertNotEqual(self.graph.edges, ()) - def test_no_edges_drawn(self): - self.assertEqual(self.graph.edges, ()) + def test_no_note_now_that_edges_are_recorded(self): + self.assertIsNone(self.graph.note) class TestUnexpandedCompositeHasNoNote(unittest.TestCase): - """A composite that never expands (depth < 0 branch skipped) must not get the note.""" + """A composite that never expands (depth < 0 branch skipped) must not get the note, + even though its own ``.nodes`` is non-empty -- exercising the ``depth >= 0`` guard + in ``_build`` rather than an accident of the composite having nothing inside it.""" + + def setUp(self): + from flowrep import edge_models + from flowrep.prospective import workflow_recipe + + inner = library.simple_workflow.flowrep_recipe + recipe = workflow_recipe.WorkflowRecipe( + inputs=["p", "q"], + outputs=["r"], + nodes={"inner": inner}, + input_edges={ + edge_models.TargetHandle( + node="inner", port="a" + ): edge_models.InputSource(port="p"), + edge_models.TargetHandle( + node="inner", port="b" + ): edge_models.InputSource(port="q"), + }, + edges={}, + output_edges={ + edge_models.OutputTarget(port="r"): edge_models.SourceHandle( + node="inner", port=inner.outputs[0] + ) + }, + ) + self.data = wfms.run_recipe(recipe, p=1, q=2) - def test_depth_zero_but_no_children_no_note(self): - data = wfms.run_recipe(_for_recipe(), xs=[]) - graph = retrospective.build(data, depth=0) - self.assertEqual(graph.children, ()) - self.assertIsNone(graph.note) + def test_depth_zero_leaves_the_inner_composite_unexpanded(self): + graph = retrospective.build(self.data, depth=0) + inner = _by_path(graph, "inner") + self.assertEqual(inner.children, ()) + self.assertIsNone(inner.note) def _for_recipe(): diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index ee004e21..ef28c882 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -217,6 +217,31 @@ def _for_add_zipped() -> for_recipe.ForEachRecipe: ) +def _for_add_nested_2x3() -> for_recipe.ForEachRecipe: + """Cartesian product of `xs` (outer) and `ys` (inner), computing `add(a, b)`.""" + return for_recipe.ForEachRecipe( + inputs=["xs", "ys"], + outputs=["sums"], + body_node=helper_models.LabeledRecipe( + label="body", recipe=std.add.flowrep_recipe + ), + input_edges={ + edge_models.TargetHandle(node="body", port="a"): edge_models.InputSource( + port="xs" + ), + edge_models.TargetHandle(node="body", port="b"): edge_models.InputSource( + port="ys" + ), + }, + output_edges={ + edge_models.OutputTarget(port="sums"): edge_models.SourceHandle( + node="body", port="added" + ), + }, + nested_ports=["a", "b"], + ) + + def _decrement_body_workflow() -> workflow_recipe.WorkflowRecipe: """`n -> decrement(n) -> n` — single-step body for while loops.""" return _single_node_workflow( @@ -1164,7 +1189,9 @@ def test_broadcast_and_transfer(self): def test_body_instances_stored(self): node = wfms.run_recipe(_for_negate(), xs=[1, 2]) - self.assertEqual(len(node.nodes), 2) + self.assertEqual( + list(node.nodes), ["scatter_xs", "body_0", "body_1", "aggregate_ys"] + ) def test_zipped_ports(self): node = wfms.run_recipe(_for_add_zipped(), xs=[1, 2, 3], ys=[10, 20, 30]) @@ -1198,6 +1225,164 @@ def test_unfilled_zipped_port_raises(self): ) +class TestRunForRecordsEdges(unittest.TestCase): + """The for-node materializes its fan-out and fan-in as real nodes, so every + recorded edge is 1:1 and the record describes what actually ran.""" + + def test_scatter_and_aggregate_nodes_exist(self): + node = wfms.run_recipe(_for_negate(), xs=[1, 2, 3]) + self.assertEqual( + list(node.nodes), + ["scatter_xs", "body_0", "body_1", "body_2", "aggregate_ys"], + ) + + def test_parent_input_feeds_the_scatter(self): + node = wfms.run_recipe(_for_negate(), xs=[1, 2, 3]) + self.assertEqual( + node.input_edges[edge_models.TargetHandle(node="scatter_xs", port="items")], + edge_models.InputSource(port="xs"), + ) + + def test_scatter_feeds_each_body_instance(self): + node = wfms.run_recipe(_for_negate(), xs=[1, 2, 3]) + for i in range(3): + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"body_{i}", port="a")], + edge_models.SourceHandle(node="scatter_xs", port=f"output_{i}"), + msg=f"body_{i} should draw element {i}", + ) + + def test_each_body_instance_feeds_the_aggregator(self): + node = wfms.run_recipe(_for_negate(), xs=[1, 2, 3]) + for i in range(3): + self.assertEqual( + node.edges[ + edge_models.TargetHandle(node="aggregate_ys", port=f"item_{i}") + ], + edge_models.SourceHandle(node=f"body_{i}", port="negative"), + ) + + def test_aggregator_feeds_the_parent_output(self): + node = wfms.run_recipe(_for_negate(), xs=[1, 2, 3]) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="ys")], + edge_models.SourceHandle(node="aggregate_ys", port="output_0"), + ) + + def test_broadcast_input_goes_to_every_instance_directly(self): + node = wfms.run_recipe(_for_add_broadcast(), xs=[10, 20, 30], offset=5) + for i in range(3): + self.assertEqual( + node.input_edges[edge_models.TargetHandle(node=f"body_{i}", port="b")], + edge_models.InputSource(port="offset"), + ) + + def test_transferred_output_is_sourced_from_the_scatter(self): + """`inputs_used` collects the scattered element itself, not a body output, so + it must be recorded as coming from the scatter node.""" + node = wfms.run_recipe(_for_add_broadcast(), xs=[10, 20, 30], offset=5) + for i in range(3): + self.assertEqual( + node.edges[ + edge_models.TargetHandle( + node="aggregate_inputs_used", port=f"item_{i}" + ) + ], + edge_models.SourceHandle(node="scatter_xs", port=f"output_{i}"), + ) + + def test_zipped_ports_share_an_index(self): + node = wfms.run_recipe(_for_add_zipped(), xs=[1, 2, 3], ys=[10, 20, 30]) + for i in range(3): + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"body_{i}", port="a")], + edge_models.SourceHandle(node="scatter_xs", port=f"output_{i}"), + ) + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"body_{i}", port="b")], + edge_models.SourceHandle(node="scatter_ys", port=f"output_{i}"), + ) + + def test_nested_strides(self): + """`xs` is the outer axis and `ys` the inner, matching the instance ordering + `itertools.product` produced before this rewrite.""" + node = wfms.run_recipe(_for_add_nested_2x3(), xs=[1, 2], ys=[10, 20, 30]) + expected = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] + for i, (xi, yi) in enumerate(expected): + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"body_{i}", port="a")], + edge_models.SourceHandle(node="scatter_xs", port=f"output_{xi}"), + msg=f"body_{i} outer axis", + ) + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"body_{i}", port="b")], + edge_models.SourceHandle(node="scatter_ys", port=f"output_{yi}"), + msg=f"body_{i} inner axis", + ) + + def test_nested_values_match_the_recorded_edges(self): + node = wfms.run_recipe(_for_add_nested_2x3(), xs=[1, 2], ys=[10, 20, 30]) + self.assertEqual(node.output_ports["sums"].value, [11, 21, 31, 12, 22, 32]) + + def test_empty_input_builds_aggregators_but_no_scatters(self): + """A zero-length input means zero body steps, so a scatter would need zero + outputs -- which no atomic recipe can express -- and nothing would consume.""" + node = wfms.run_recipe(_for_negate(), xs=[]) + self.assertEqual(list(node.nodes), ["aggregate_ys"]) + self.assertEqual(node.input_edges, {}) + self.assertEqual(node.edges, {}) + self.assertEqual(node.output_ports["ys"].value, []) + + def test_label_collision_raises(self): + recipe = _for_negate().model_copy( + update={ + "body_node": helper_models.LabeledRecipe( + label="aggregate_ys", recipe=std.neg.flowrep_recipe + ), + "input_edges": { + edge_models.TargetHandle( + node="aggregate_ys", port="a" + ): edge_models.InputSource(port="xs") + }, + "output_edges": { + edge_models.OutputTarget(port="ys"): edge_models.SourceHandle( + node="aggregate_ys", port="negative" + ) + }, + } + ) + with self.assertRaisesRegex(ValueError, "label collision"): + wfms.run_recipe(recipe, xs=[1]) + + def test_two_iterated_ports_sharing_one_input_raises(self): + """One scatter is built per for-node input port, so two iterated body ports + fed by the same input would share a scatter -- and an index -- collapsing two + independent axes into one. Refuse rather than quietly iterate over less.""" + recipe = for_recipe.ForEachRecipe( + inputs=["xs"], + outputs=["sums"], + body_node=helper_models.LabeledRecipe( + label="body", recipe=std.add.flowrep_recipe + ), + input_edges={ + edge_models.TargetHandle( + node="body", port="a" + ): edge_models.InputSource(port="xs"), + edge_models.TargetHandle( + node="body", port="b" + ): edge_models.InputSource(port="xs"), + }, + output_edges={ + edge_models.OutputTarget(port="sums"): edge_models.SourceHandle( + node="body", port="added" + ) + }, + nested_ports=["a", "b"], + ) + with self.assertRaisesRegex(ValueError, "distinct for-node inputs"): + wfms.run_recipe(recipe, xs=[1, 2]) + + # ═══════════════════════════════════════════════════════════════════════════ # wfms.py tests — while # ═══════════════════════════════════════════════════════════════════════════ @@ -1222,6 +1407,64 @@ def test_body_instances_counted(self): self.assertEqual(len(node.nodes), 3 + 4) +class TestRunWhileRecordsEdges(unittest.TestCase): + def test_first_iteration_comes_from_the_while_inputs(self): + node = wfms.run_recipe(_while_countdown(), n=3) + for child in ("condition_0", "body_0"): + self.assertEqual( + node.input_edges[edge_models.TargetHandle(node=child, port="n")], + edge_models.InputSource(port="n"), + msg=f"{child} should read the while-node's own input", + ) + + def test_later_conditions_come_from_the_previous_body(self): + node = wfms.run_recipe(_while_countdown(), n=3) + for k in range(1, 4): # condition_1 … condition_3 + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"condition_{k}", port="n")], + edge_models.SourceHandle(node=f"body_{k - 1}", port="n"), + ) + + def test_later_bodies_come_from_the_previous_body(self): + node = wfms.run_recipe(_while_countdown(), n=3) + for k in range(1, 3): # body_1, body_2 -- there is no body_3 + self.assertEqual( + node.edges[edge_models.TargetHandle(node=f"body_{k}", port="n")], + edge_models.SourceHandle(node=f"body_{k - 1}", port="n"), + ) + + def test_no_edge_for_a_body_that_never_ran(self): + node = wfms.run_recipe(_while_countdown(), n=3) + self.assertNotIn(edge_models.TargetHandle(node="body_3", port="n"), node.edges) + + def test_output_comes_from_the_last_body(self): + node = wfms.run_recipe(_while_countdown(), n=3) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="n")], + edge_models.SourceHandle(node="body_2", port="n"), + ) + + def test_output_falls_back_to_the_input_when_the_body_never_ran(self): + node = wfms.run_recipe(_while_countdown(), n=0) + self.assertEqual(list(node.nodes), ["condition_0"]) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="n")], + edge_models.InputSource(port="n"), + ) + self.assertEqual(node.edges, {}) + + def test_single_iteration(self): + node = wfms.run_recipe(_while_countdown(), n=1) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="n")], + edge_models.SourceHandle(node="body_0", port="n"), + ) + self.assertEqual( + node.edges[edge_models.TargetHandle(node="condition_1", port="n")], + edge_models.SourceHandle(node="body_0", port="n"), + ) + + # ═══════════════════════════════════════════════════════════════════════════ # wfms.py tests — if # ═══════════════════════════════════════════════════════════════════════════ @@ -1286,6 +1529,67 @@ def test_unhandled_exception_propagates(self): wfms.run_recipe(_failing_try.flowrep_recipe, x=42) +class TestRunIfRecordsEdges(unittest.TestCase): + def test_positive_branch_records_only_what_ran(self): + node = wfms.run_recipe(_if_abs(), x=5) + self.assertEqual( + set(node.input_edges), + { + edge_models.TargetHandle(node="condition_0", port="n"), + edge_models.TargetHandle(node="body_0", port="x"), + }, + ) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="y")], + edge_models.SourceHandle(node="body_0", port="y"), + ) + + def test_else_branch_records_only_what_ran(self): + node = wfms.run_recipe(_if_abs(), x=-5) + self.assertEqual( + set(node.input_edges), + { + edge_models.TargetHandle(node="condition_0", port="n"), + edge_models.TargetHandle(node="else_body", port="x"), + }, + ) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="y")], + edge_models.SourceHandle(node="else_body", port="y"), + ) + + def test_no_sibling_edges(self): + node = wfms.run_recipe(_if_abs(), x=5) + self.assertEqual(node.edges, {}) + + +class TestRunTryRecordsEdges(unittest.TestCase): + def test_success_records_the_try_body(self): + node = wfms.run_recipe(_try_safe_divide(), a=10, b=2) + self.assertEqual( + set(node.input_edges), + { + edge_models.TargetHandle(node="try_body", port="a"), + edge_models.TargetHandle(node="try_body", port="b"), + }, + ) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="result")], + edge_models.SourceHandle(node="try_body", port="result"), + ) + + def test_handled_exception_records_the_handler(self): + node = wfms.run_recipe(_try_safe_divide(), a=10, b=0) + self.assertIn( + edge_models.TargetHandle(node="except_body_0", port="a"), + node.input_edges, + ) + self.assertEqual( + node.output_edges[edge_models.OutputTarget(port="result")], + edge_models.SourceHandle(node="except_body_0", port="result"), + ) + + # ═══════════════════════════════════════════════════════════════════════════ # wfms.py tests — edge case # ═══════════════════════════════════════════════════════════════════════════ From d879870e239f78a59ac90b7890810b12184863d3 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:42:11 -0700 Subject: [PATCH 3/8] Update user guide To reflect new for-loop behaviour in the toy WfMS Co-authored-by: Claude Signed-off-by: Liam Huber --- notebooks/user-guide.ipynb | 4406 ++++++++++++++++++++++++++---------- 1 file changed, 3223 insertions(+), 1183 deletions(-) diff --git a/notebooks/user-guide.ipynb b/notebooks/user-guide.ipynb index f0dfcce4..70a2f4a7 100644 --- a/notebooks/user-guide.ipynb +++ b/notebooks/user-guide.ipynb @@ -35,20 +35,21 @@ ] }, { + "cell_type": "code", + "execution_count": 1, + "id": "9505e25f9b266276", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.548577Z", "start_time": "2026-09-08T00:49:22.246134Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:03.798252Z", + "iopub.status.busy": "2026-09-07T17:47:03.798044Z", + "iopub.status.idle": "2026-09-07T17:47:04.115623Z", + "shell.execute_reply": "2026-09-07T17:47:04.115248Z" } }, - "cell_type": "code", - "source": [ - "import flowrep as fr\n", - "\n", - "# Quick check that the four main entry points are available:\n", - "fr.parse_atomic, fr.atomic, fr.workflow, fr.dataclass" - ], - "id": "9505e25f9b266276", "outputs": [ { "data": { @@ -64,11 +65,17 @@ "output_type": "execute_result" } ], - "execution_count": 1 + "source": [ + "import flowrep as fr\n", + "\n", + "# Quick check that the four main entry points are available:\n", + "fr.parse_atomic, fr.atomic, fr.workflow, fr.dataclass" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "f39b6c2d0ce77d0f", + "metadata": {}, "source": [ "We also supply a standard library of nodes for common use patterns, e.g. python operators, under `flowrep.std`.\n", "\n", @@ -76,12 +83,12 @@ "\n", "- `flowrep.schemas` — Pydantic models and classes for nodes, edges, etc., as well as enums and constants\n", "- `flowrep.tools` — Functions for creating and modifying flowrep data formats" - ], - "id": "f39b6c2d0ce77d0f" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "8ae7bf4cb3a7e58", + "metadata": {}, "source": [ "---\n", "## 1. Atomic Nodes\n", @@ -95,30 +102,24 @@ "### 1.1 The `@atomic` decorator and `parse_atomic`\n", "\n", "The simplest way to create a recipe is with the `@atomic` decorator:" - ], - "id": "8ae7bf4cb3a7e58" + ] }, { + "cell_type": "code", + "execution_count": 2, + "id": "c66106e9842c6a0b", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.583265Z", "start_time": "2026-09-08T00:49:22.564678Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.116784Z", + "iopub.status.busy": "2026-09-07T17:47:04.116635Z", + "iopub.status.idle": "2026-09-07T17:47:04.120853Z", + "shell.execute_reply": "2026-09-07T17:47:04.120449Z" } }, - "cell_type": "code", - "source": [ - "@fr.atomic\n", - "def add(a, b):\n", - " result = a + b\n", - " return result\n", - "\n", - "\n", - "# The decorator attaches a recipe to the function — it's still callable as usual:\n", - "print(f\"add(2, 3) = {add(2, 3)}\")\n", - "print(f\"Recipe type: {type(add.flowrep_recipe).__name__}\")\n", - "add.flowrep_recipe" - ], - "id": "c66106e9842c6a0b", "outputs": [ { "name": "stdout", @@ -139,7 +140,18 @@ "output_type": "execute_result" } ], - "execution_count": 2 + "source": [ + "@fr.atomic\n", + "def add(a, b):\n", + " result = a + b\n", + " return result\n", + "\n", + "\n", + "# The decorator attaches a recipe to the function — it's still callable as usual:\n", + "print(f\"add(2, 3) = {add(2, 3)}\")\n", + "print(f\"Recipe type: {type(add.flowrep_recipe).__name__}\")\n", + "add.flowrep_recipe" + ] }, { "cell_type": "markdown", @@ -160,22 +172,20 @@ }, { "cell_type": "code", + "execution_count": 3, "id": "fbb2ac82", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.637275Z", "start_time": "2026-09-08T00:49:22.602013Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.121636Z", + "iopub.status.busy": "2026-09-07T17:47:04.121553Z", + "iopub.status.idle": "2026-09-07T17:47:04.124637Z", + "shell.execute_reply": "2026-09-07T17:47:04.124229Z" } }, - "source": [ - "def multiply(x, y):\n", - " product = x * y\n", - " return product\n", - "\n", - "\n", - "recipe = fr.parse_atomic(multiply)\n", - "recipe" - ], "outputs": [ { "data": { @@ -188,7 +198,15 @@ "output_type": "execute_result" } ], - "execution_count": 3 + "source": [ + "def multiply(x, y):\n", + " product = x * y\n", + " return product\n", + "\n", + "\n", + "recipe = fr.parse_atomic(multiply)\n", + "recipe" + ] }, { "cell_type": "markdown", @@ -202,18 +220,21 @@ ] }, { + "cell_type": "code", + "execution_count": 4, + "id": "d652b49c0740e517", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.656137Z", "start_time": "2026-09-08T00:49:22.639351Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.125312Z", + "iopub.status.busy": "2026-09-07T17:47:04.125246Z", + "iopub.status.idle": "2026-09-07T17:47:04.127649Z", + "shell.execute_reply": "2026-09-07T17:47:04.127415Z" } }, - "cell_type": "code", - "source": [ - "for reserved in sorted(fr.schemas.RESERVED_NAMES):\n", - " print(reserved)" - ], - "id": "d652b49c0740e517", "outputs": [ { "name": "stdout", @@ -224,26 +245,48 @@ ] } ], - "execution_count": 4 + "source": [ + "for reserved in sorted(fr.schemas.RESERVED_NAMES):\n", + " print(reserved)" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "d10c644017d2bac6", + "metadata": {}, "source": [ "This keeps recipes unambiguous and compatible with\n", "attribute-style access." - ], - "id": "d10c644017d2bac6" + ] }, { "cell_type": "code", + "execution_count": 5, "id": "4ce6917d", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.689278Z", "start_time": "2026-09-08T00:49:22.670350Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.128408Z", + "iopub.status.busy": "2026-09-07T17:47:04.128352Z", + "iopub.status.idle": "2026-09-07T17:47:04.132924Z", + "shell.execute_reply": "2026-09-07T17:47:04.132660Z" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 'inputs' rejected: ValidationError\n", + " '0port' rejected: ValidationError\n", + " 'class' rejected: ValidationError\n", + " 'my-port' rejected: ValidationError\n" + ] + } + ], "source": [ "# These will fail:\n", "from pydantic import TypeAdapter\n", @@ -260,20 +303,7 @@ " label_adapter.validate_python(bad_name)\n", " except Exception as e:\n", " print(f\" '{bad_name}' rejected: {type(e).__name__}\")" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 'inputs' rejected: ValidationError\n", - " '0port' rejected: ValidationError\n", - " 'class' rejected: ValidationError\n", - " 'my-port' rejected: ValidationError\n" - ] - } - ], - "execution_count": 5 + ] }, { "cell_type": "markdown", @@ -299,13 +329,29 @@ }, { "cell_type": "code", + "execution_count": 6, "id": "cac0a1bd", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.726585Z", "start_time": "2026-09-08T00:49:22.705208Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.133723Z", + "iopub.status.busy": "2026-09-07T17:47:04.133652Z", + "iopub.status.idle": "2026-09-07T17:47:04.136916Z", + "shell.execute_reply": "2026-09-07T17:47:04.136487Z" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outputs: ['quotient', 'remainder']\n" + ] + } + ], "source": [ "@fr.atomic\n", "def divide(a, b):\n", @@ -316,17 +362,7 @@ "\n", "print(f\"Outputs: {divide.flowrep_recipe.outputs}\")\n", "assert(divide.flowrep_recipe.outputs == [\"quotient\", \"remainder\"])" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Outputs: ['quotient', 'remainder']\n" - ] - } - ], - "execution_count": 6 + ] }, { "cell_type": "markdown", @@ -340,22 +376,20 @@ }, { "cell_type": "code", + "execution_count": 7, "id": "9f659bd7", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.761232Z", "start_time": "2026-09-08T00:49:22.743329Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.137577Z", + "iopub.status.busy": "2026-09-07T17:47:04.137512Z", + "iopub.status.idle": "2026-09-07T17:47:04.141043Z", + "shell.execute_reply": "2026-09-07T17:47:04.140691Z" } }, - "source": [ - "@fr.atomic\n", - "def increment(x):\n", - " return x + 1\n", - "\n", - "\n", - "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", - "assert(increment.flowrep_recipe.outputs == [\"output_0\"])" - ], "outputs": [ { "name": "stdout", @@ -365,31 +399,40 @@ ] } ], - "execution_count": 7 + "source": [ + "@fr.atomic\n", + "def increment(x):\n", + " return x + 1\n", + "\n", + "\n", + "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", + "assert(increment.flowrep_recipe.outputs == [\"output_0\"])" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "You can override this by providing explicit labels to the decorator", - "id": "e9bab152b3f98e5d" + "id": "e9bab152b3f98e5d", + "metadata": {}, + "source": [ + "You can override this by providing explicit labels to the decorator" + ] }, { + "cell_type": "code", + "execution_count": 8, + "id": "d5a7b4f2477b326", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.795874Z", "start_time": "2026-09-08T00:49:22.776489Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.141665Z", + "iopub.status.busy": "2026-09-07T17:47:04.141607Z", + "iopub.status.idle": "2026-09-07T17:47:04.145011Z", + "shell.execute_reply": "2026-09-07T17:47:04.144627Z" } }, - "cell_type": "code", - "source": [ - "@fr.atomic(\"stepped\")\n", - "def increment(x):\n", - " return x + 1\n", - "\n", - "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", - "assert(increment.flowrep_recipe.outputs == [\"stepped\"])" - ], - "id": "d5a7b4f2477b326", "outputs": [ { "name": "stdout", @@ -399,7 +442,14 @@ ] } ], - "execution_count": 8 + "source": [ + "@fr.atomic(\"stepped\")\n", + "def increment(x):\n", + " return x + 1\n", + "\n", + "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", + "assert(increment.flowrep_recipe.outputs == [\"stepped\"])" + ] }, { "cell_type": "markdown", @@ -413,22 +463,20 @@ }, { "cell_type": "code", + "execution_count": 9, "id": "467bd6b8", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.827586Z", "start_time": "2026-09-08T00:49:22.810176Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.145597Z", + "iopub.status.busy": "2026-09-07T17:47:04.145543Z", + "iopub.status.idle": "2026-09-07T17:47:04.149040Z", + "shell.execute_reply": "2026-09-07T17:47:04.148543Z" } }, - "source": [ - "@fr.atomic(\"tuple_return\")\n", - "def righthanded2lefthanded(x, y, z):\n", - " return (x, z, y)\n", - "\n", - "\n", - "print(f\"Outputs: {righthanded2lefthanded.flowrep_recipe.outputs}\")\n", - "assert(righthanded2lefthanded.flowrep_recipe.outputs == [\"tuple_return\"])" - ], "outputs": [ { "name": "stdout", @@ -438,23 +486,49 @@ ] } ], - "execution_count": 9 + "source": [ + "@fr.atomic(\"tuple_return\")\n", + "def righthanded2lefthanded(x, y, z):\n", + " return (x, z, y)\n", + "\n", + "\n", + "print(f\"Outputs: {righthanded2lefthanded.flowrep_recipe.outputs}\")\n", + "assert(righthanded2lefthanded.flowrep_recipe.outputs == [\"tuple_return\"])" + ] }, { "cell_type": "markdown", "id": "1c301250", "metadata": {}, - "source": "These labels take the highest priority, but if you happen to have annotated returns, flowrep will use any \"label\" term there before falling back to scraping the returned symbols themselves. These annotations won't control how many output ports are produced though -- the only way to force a single-output port is to provide a single output label." + "source": [ + "These labels take the highest priority, but if you happen to have annotated returns, flowrep will use any \"label\" term there before falling back to scraping the returned symbols themselves. These annotations won't control how many output ports are produced though -- the only way to force a single-output port is to provide a single output label." + ] }, { "cell_type": "code", + "execution_count": 10, "id": "0d46e437", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.857797Z", "start_time": "2026-09-08T00:49:22.840823Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.149853Z", + "iopub.status.busy": "2026-09-07T17:47:04.149784Z", + "iopub.status.idle": "2026-09-07T17:47:04.152598Z", + "shell.execute_reply": "2026-09-07T17:47:04.152238Z" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outputs: ['magnitude', 'angle']\n" + ] + } + ], "source": [ "from typing import Annotated\n", "\n", @@ -473,17 +547,7 @@ "\n", "print(f\"Outputs: {norm.flowrep_recipe.outputs}\")\n", "assert(norm.flowrep_recipe.outputs == ['magnitude', 'angle'])" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Outputs: ['magnitude', 'angle']\n" - ] - } - ], - "execution_count": 10 + ] }, { "cell_type": "markdown", @@ -502,23 +566,20 @@ }, { "cell_type": "code", + "execution_count": 11, "id": "77276c40", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.889028Z", "start_time": "2026-09-08T00:49:22.872367Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.153512Z", + "iopub.status.busy": "2026-09-07T17:47:04.153421Z", + "iopub.status.idle": "2026-09-07T17:47:04.156842Z", + "shell.execute_reply": "2026-09-07T17:47:04.156408Z" } }, - "source": [ - "recipe = add.flowrep_recipe\n", - "ref = recipe.reference\n", - "print(f\"Module: {ref.info.module}\")\n", - "print(f\"Qualname: {ref.info.qualname}\")\n", - "print(f\"Version: {ref.info.version}\")\n", - "print(f\"FQN: {recipe.fully_qualified_name}\")\n", - "print(f\"Defaults: {ref.inputs_with_defaults}\")\n", - "print(f\"Restricted kinds: {ref.restricted_input_kinds}\")" - ], "outputs": [ { "name": "stdout", @@ -533,7 +594,16 @@ ] } ], - "execution_count": 11 + "source": [ + "recipe = add.flowrep_recipe\n", + "ref = recipe.reference\n", + "print(f\"Module: {ref.info.module}\")\n", + "print(f\"Qualname: {ref.info.qualname}\")\n", + "print(f\"Version: {ref.info.version}\")\n", + "print(f\"FQN: {recipe.fully_qualified_name}\")\n", + "print(f\"Defaults: {ref.inputs_with_defaults}\")\n", + "print(f\"Restricted kinds: {ref.restricted_input_kinds}\")" + ] }, { "cell_type": "markdown", @@ -582,23 +652,41 @@ }, { "cell_type": "code", + "execution_count": 12, "id": "b8f03ee2", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.920459Z", "start_time": "2026-09-08T00:49:22.904409Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.157905Z", + "iopub.status.busy": "2026-09-07T17:47:04.157811Z", + "iopub.status.idle": "2026-09-07T17:47:04.161856Z", + "shell.execute_reply": "2026-09-07T17:47:04.161367Z" } }, - "source": [ - "import flowrep as fr\n", - "\n", - "@fr.atomic\n", - "class MyClass:\n", - " def __init__(self, *, x: int = 100):\n", - " self.x = x\n", - "\n", - " @fr.atomic\n", - " def some_method(self, y):\n", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "__main__.MyClass ['x'] ['instance']\n", + "__main__.MyClass.some_method ['self', 'y'] ['output_0']\n", + "__main__.MyClass.some_static ['a'] ['triple', 'single']\n" + ] + } + ], + "source": [ + "import flowrep as fr\n", + "\n", + "@fr.atomic\n", + "class MyClass:\n", + " def __init__(self, *, x: int = 100):\n", + " self.x = x\n", + "\n", + " @fr.atomic\n", + " def some_method(self, y):\n", " self.x += y\n", " return self.x\n", "\n", @@ -617,47 +705,32 @@ " decorated.flowrep_recipe.inputs,\n", " decorated.flowrep_recipe.outputs\n", " )" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "__main__.MyClass ['x'] ['instance']\n", - "__main__.MyClass.some_method ['self', 'y'] ['output_0']\n", - "__main__.MyClass.some_static ['a'] ['triple', 'single']\n" - ] - } - ], - "execution_count": 12 + ] }, { "cell_type": "markdown", "id": "02ccd920", "metadata": {}, - "source": "This also works trivially for dataclasses -- you can stack the decorators together, or use the `flowrep.dataclass` decorator to do both at once for you. Just like the decorated objects, these stay as usual dataclasses but get their `.flowrep_recipe` attached:" + "source": [ + "This also works trivially for dataclasses -- you can stack the decorators together, or use the `flowrep.dataclass` decorator to do both at once for you. Just like the decorated objects, these stay as usual dataclasses but get their `.flowrep_recipe` attached:" + ] }, { "cell_type": "code", + "execution_count": 13, "id": "66ac7ffa", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:22.952760Z", "start_time": "2026-09-08T00:49:22.934113Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.162630Z", + "iopub.status.busy": "2026-09-07T17:47:04.162562Z", + "iopub.status.idle": "2026-09-07T17:47:04.166819Z", + "shell.execute_reply": "2026-09-07T17:47:04.166373Z" } }, - "source": [ - "import dataclasses\n", - "\n", - "@fr.dataclass\n", - "class MyDataclass:\n", - " a: str\n", - " x: int = 1\n", - "\n", - "print(\"Dataclass?\", dataclasses.is_dataclass(MyDataclass))\n", - "print(\"Recipe?\", type(MyDataclass.flowrep_recipe))\n", - "MyDataclass.flowrep_recipe.inputs, MyDataclass.flowrep_recipe.outputs" - ], "outputs": [ { "name": "stdout", @@ -678,30 +751,43 @@ "output_type": "execute_result" } ], - "execution_count": 13 + "source": [ + "import dataclasses\n", + "\n", + "@fr.dataclass\n", + "class MyDataclass:\n", + " a: str\n", + " x: int = 1\n", + "\n", + "print(\"Dataclass?\", dataclasses.is_dataclass(MyDataclass))\n", + "print(\"Recipe?\", type(MyDataclass.flowrep_recipe))\n", + "MyDataclass.flowrep_recipe.inputs, MyDataclass.flowrep_recipe.outputs" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Dataclasses also get a `.flowrep_recipe_unpacking` -- a recipe going the other direction, unpacking an instance's fields into separate outputs. It's a normal callable, so calling the recipe and calling python agree exactly:", - "id": "bdb1f0becd5134e5" + "id": "bdb1f0becd5134e5", + "metadata": {}, + "source": [ + "Dataclasses also get a `.flowrep_recipe_unpacking` -- a recipe going the other direction, unpacking an instance's fields into separate outputs. It's a normal callable, so calling the recipe and calling python agree exactly:" + ] }, { + "cell_type": "code", + "execution_count": 14, + "id": "915053248a94e896", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.001834Z", "start_time": "2026-09-08T00:49:22.966371Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.167617Z", + "iopub.status.busy": "2026-09-07T17:47:04.167542Z", + "iopub.status.idle": "2026-09-07T17:47:04.170822Z", + "shell.execute_reply": "2026-09-07T17:47:04.170367Z" } }, - "cell_type": "code", - "source": [ - "instance = MyDataclass(a=\"hello\", x=7)\n", - "\n", - "print(\"Unpacking inputs: \", MyDataclass.flowrep_recipe_unpacking.inputs)\n", - "print(\"Unpacking outputs:\", MyDataclass.flowrep_recipe_unpacking.outputs)\n", - "MyDataclass.flowrep_recipe_unpacking(instance)" - ], - "id": "915053248a94e896", "outputs": [ { "name": "stdout", @@ -722,22 +808,31 @@ "output_type": "execute_result" } ], - "execution_count": 14 + "source": [ + "instance = MyDataclass(a=\"hello\", x=7)\n", + "\n", + "print(\"Unpacking inputs: \", MyDataclass.flowrep_recipe_unpacking.inputs)\n", + "print(\"Unpacking outputs:\", MyDataclass.flowrep_recipe_unpacking.outputs)\n", + "MyDataclass.flowrep_recipe_unpacking(instance)" + ] }, { + "cell_type": "markdown", + "id": "e10b7383753d5d86", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.277253Z", "start_time": "2026-07-11T03:59:27.262256Z" } }, - "cell_type": "markdown", - "source": "**Limitations**: `@staticmethod` only works when the decorators are applied in the right order; `@classmethod` doesn't work at all; classes defining a custom `__new__` will fail to parse cleanly; classes defining a class-level `flowrep_recipe` field (classvar or method) will fail cleanly.", - "id": "e10b7383753d5d86" + "source": [ + "**Limitations**: `@staticmethod` only works when the decorators are applied in the right order; `@classmethod` doesn't work at all; classes defining a custom `__new__` will fail to parse cleanly; classes defining a class-level `flowrep_recipe` field (classvar or method) will fail cleanly." + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "ec98d006c8259b8e", + "metadata": {}, "source": [ "### 1.7 Power-user: controlling inputs for tricky functions\n", "\n", @@ -749,17 +844,34 @@ "For a workflow node, we need outputs to be predictable. We can manually construct\n", "a recipe that exposes only a safe subset of inputs, relying on Python defaults\n", "for the rest:" - ], - "id": "ec98d006c8259b8e" + ] }, { + "cell_type": "code", + "execution_count": 15, + "id": "cd048745aefa7acc", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.032535Z", "start_time": "2026-09-08T00:49:23.003357Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.171521Z", + "iopub.status.busy": "2026-09-07T17:47:04.171457Z", + "iopub.status.idle": "2026-09-07T17:47:04.175029Z", + "shell.execute_reply": "2026-09-07T17:47:04.174586Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inputs: ['start', 'stop', 'num']\n", + "Outputs: ['samples']\n" + ] + } + ], "source": [ "import numpy as np\n", "from pyiron_snippets import versions\n", @@ -774,46 +886,50 @@ ")\n", "print(f\"Inputs: {linspace_node.inputs}\")\n", "print(f\"Outputs: {linspace_node.outputs}\")" - ], - "id": "cd048745aefa7acc", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inputs: ['start', 'stop', 'num']\n", - "Outputs: ['samples']\n" - ] - } - ], - "execution_count": 15 + ] }, { + "cell_type": "markdown", + "id": "8b5f8794c3dcd24d", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.297043Z", "start_time": "2026-07-11T03:59:27.283303Z" } }, - "cell_type": "markdown", "source": [ "**Forcing consistent argument passing.** `numpy.arange` interprets positional\n", "arguments differently depending on how many you pass: `arange(stop)` vs\n", "`arange(start, stop, step)`. A WfMS needs a single, unambiguous calling convention.\n", "We can use `restricted_input_kinds` to force keyword-only passing, removing all\n", "positional ambiguity:" - ], - "id": "8b5f8794c3dcd24d" + ] }, { "cell_type": "code", + "execution_count": 16, "id": "67cf6163", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.088351Z", "start_time": "2026-09-08T00:49:23.056918Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.175702Z", + "iopub.status.busy": "2026-09-07T17:47:04.175637Z", + "iopub.status.idle": "2026-09-07T17:47:04.178861Z", + "shell.execute_reply": "2026-09-07T17:47:04.178526Z" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted kinds: {'start': , 'stop': , 'step': }\n" + ] + } + ], "source": [ "arange_node = fr.schemas.AtomicRecipe(\n", " reference=fr.schemas.PythonReference(\n", @@ -829,17 +945,7 @@ " outputs=[\"values\"],\n", ")\n", "print(f\"Restricted kinds: {arange_node.reference.restricted_input_kinds}\")" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Restricted kinds: {'start': , 'stop': , 'step': }\n" - ] - } - ], - "execution_count": 16 + ] }, { "cell_type": "markdown", @@ -870,15 +976,21 @@ ] }, { + "cell_type": "code", + "execution_count": 17, + "id": "d0b951260802626c", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.107045Z", "start_time": "2026-09-08T00:49:23.090463Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.179665Z", + "iopub.status.busy": "2026-09-07T17:47:04.179600Z", + "iopub.status.idle": "2026-09-07T17:47:04.182761Z", + "shell.execute_reply": "2026-09-07T17:47:04.182374Z" } }, - "cell_type": "code", - "source": "arange_node(start=1, stop=2, step=3)", - "id": "d0b951260802626c", "outputs": [ { "data": { @@ -891,31 +1003,50 @@ "output_type": "execute_result" } ], - "execution_count": 17 + "source": [ + "arange_node(start=1, stop=2, step=3)" + ] }, { + "cell_type": "markdown", + "id": "f0bac0d25a93b5fa", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.331908Z", "start_time": "2026-07-11T03:59:27.317556Z" } }, - "cell_type": "markdown", "source": [ "**Handling variadics.** The graph-based approach requires that IO all be explicitly labeled, which means that python's variadic signatures pose a problem. If we know something about how those variadics should behave, we can get a workaround by exposing a fixed subset of variadics.\n", "\n", "E.g., if a function author exposes `**kwargs`, but we know a particular set of kwargs that should be used we can specify a particular recipe for this function exposing those kwargs:" - ], - "id": "f0bac0d25a93b5fa" + ] }, { + "cell_type": "code", + "execution_count": 18, + "id": "396728243a8a212e", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.143009Z", "start_time": "2026-09-08T00:49:23.123099Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.183499Z", + "iopub.status.busy": "2026-09-07T17:47:04.183430Z", + "iopub.status.idle": "2026-09-07T17:47:04.186987Z", + "shell.execute_reply": "2026-09-07T17:47:04.186520Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted kinds: {'this': , 'that': , 'the_other': }\n" + ] + } + ], "source": [ "def my_dict_maker(**kwargs):\n", " return dict(**kwargs)\n", @@ -933,29 +1064,24 @@ " outputs=[\"as_a_dict\"],\n", ")\n", "print(f\"Restricted kinds: {my_particular_dict_recipe.reference.restricted_input_kinds}\")" - ], - "id": "396728243a8a212e", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Restricted kinds: {'this': , 'that': , 'the_other': }\n" - ] - } - ], - "execution_count": 18 + ] }, { + "cell_type": "code", + "execution_count": 19, + "id": "5e2b36c02de9cb22", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.163112Z", "start_time": "2026-09-08T00:49:23.143863Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.187684Z", + "iopub.status.busy": "2026-09-07T17:47:04.187612Z", + "iopub.status.idle": "2026-09-07T17:47:04.190805Z", + "shell.execute_reply": "2026-09-07T17:47:04.190519Z" } }, - "cell_type": "code", - "source": "my_particular_dict_recipe(this=1, that=2, the_other=3)", - "id": "5e2b36c02de9cb22", "outputs": [ { "data": { @@ -968,22 +1094,43 @@ "output_type": "execute_result" } ], - "execution_count": 19 + "source": [ + "my_particular_dict_recipe(this=1, that=2, the_other=3)" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "We can play similar tricks to handle positional variadics, e.g.", - "id": "c24743c2e8ff6ddb" + "id": "c24743c2e8ff6ddb", + "metadata": {}, + "source": [ + "We can play similar tricks to handle positional variadics, e.g." + ] }, { + "cell_type": "code", + "execution_count": 20, + "id": "d4026a680946afca", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.208617Z", "start_time": "2026-09-08T00:49:23.176735Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.191546Z", + "iopub.status.busy": "2026-09-07T17:47:04.191482Z", + "iopub.status.idle": "2026-09-07T17:47:04.195022Z", + "shell.execute_reply": "2026-09-07T17:47:04.194494Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted kinds: {'a': , 'b': }\n" + ] + } + ], "source": [ "def my_variadic_add(*terms, offset=0):\n", " sum_ = offset\n", @@ -1005,29 +1152,24 @@ " outputs=[\"total\"],\n", ")\n", "print(f\"Restricted kinds: {my_offset_sum.reference.restricted_input_kinds}\")" - ], - "id": "d4026a680946afca", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Restricted kinds: {'a': , 'b': }\n" - ] - } - ], - "execution_count": 20 + ] }, { + "cell_type": "code", + "execution_count": 21, + "id": "3a6051ff2f2da9e0", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.227060Z", "start_time": "2026-09-08T00:49:23.213254Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.195610Z", + "iopub.status.busy": "2026-09-07T17:47:04.195551Z", + "iopub.status.idle": "2026-09-07T17:47:04.198843Z", + "shell.execute_reply": "2026-09-07T17:47:04.198596Z" } }, - "cell_type": "code", - "source": "my_offset_sum(1, 2, offset=3)", - "id": "3a6051ff2f2da9e0", "outputs": [ { "data": { @@ -1040,22 +1182,27 @@ "output_type": "execute_result" } ], - "execution_count": 21 + "source": [ + "my_offset_sum(1, 2, offset=3)" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "In these cases the recipe winds up more specific than the function, but the function is still usable in a workflow recipe.", - "id": "4ef16bf07dc23b4a" + "id": "4ef16bf07dc23b4a", + "metadata": {}, + "source": [ + "In these cases the recipe winds up more specific than the function, but the function is still usable in a workflow recipe." + ] }, { + "cell_type": "markdown", + "id": "b467569b70654cb6", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.391960Z", "start_time": "2026-07-11T03:59:27.377483Z" } }, - "cell_type": "markdown", "source": [ "---\n", "## 2. Workflows\n", @@ -1072,17 +1219,44 @@ "- A single `return` statement\n", "\n", "(We'll see later that control flow and imports are also supported.)" - ], - "id": "b467569b70654cb6" + ] }, { + "cell_type": "code", + "execution_count": 22, + "id": "ca3cb2598c788de5", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.265325Z", "start_time": "2026-09-08T00:49:23.228107Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.199567Z", + "iopub.status.busy": "2026-09-07T17:47:04.199499Z", + "iopub.status.idle": "2026-09-07T17:47:04.203794Z", + "shell.execute_reply": "2026-09-07T17:47:04.203436Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "linear(3, 2, 1) = 7\n", + "Recipe type: WorkflowRecipe\n" + ] + }, + { + "data": { + "text/plain": [ + "WorkflowRecipe(type=, inputs=['x', 'slope', 'intercept'], outputs=['result'], description='y = slope * x + intercept', nodes={'multiply_0': AtomicRecipe(type=, inputs=['x', 'y'], outputs=['product'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='multiply', version=None), inputs_with_defaults=[], restricted_input_kinds={})), 'add_0': AtomicRecipe(type=, inputs=['a', 'b'], outputs=['result'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='add', version=None), inputs_with_defaults=[], restricted_input_kinds={}))}, input_edges={TargetHandle(node='multiply_0', port='x'): InputSource(node=None, port='x'), TargetHandle(node='multiply_0', port='y'): InputSource(node=None, port='slope'), TargetHandle(node='add_0', port='b'): InputSource(node=None, port='intercept')}, edges={TargetHandle(node='add_0', port='a'): SourceHandle(node='multiply_0', port='product')}, output_edges={OutputTarget(node=None, port='result'): SourceHandle(node='add_0', port='result')}, reference=PythonReference(info=VersionInfo(module='__main__', qualname='linear', version=None), inputs_with_defaults=[], restricted_input_kinds={}))" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@fr.atomic\n", "def add(a, b):\n", @@ -1108,33 +1282,12 @@ "print(f\"linear(3, 2, 1) = {linear(3, 2, 1)}\")\n", "print(f\"Recipe type: {type(linear.flowrep_recipe).__name__}\")\n", "linear.flowrep_recipe" - ], - "id": "ca3cb2598c788de5", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "linear(3, 2, 1) = 7\n", - "Recipe type: WorkflowRecipe\n" - ] - }, - { - "data": { - "text/plain": [ - "WorkflowRecipe(type=, inputs=['x', 'slope', 'intercept'], outputs=['result'], description='y = slope * x + intercept', nodes={'multiply_0': AtomicRecipe(type=, inputs=['x', 'y'], outputs=['product'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='multiply', version=None), inputs_with_defaults=[], restricted_input_kinds={})), 'add_0': AtomicRecipe(type=, inputs=['a', 'b'], outputs=['result'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='add', version=None), inputs_with_defaults=[], restricted_input_kinds={}))}, input_edges={TargetHandle(node='multiply_0', port='x'): InputSource(node=None, port='x'), TargetHandle(node='multiply_0', port='y'): InputSource(node=None, port='slope'), TargetHandle(node='add_0', port='b'): InputSource(node=None, port='intercept')}, edges={TargetHandle(node='add_0', port='a'): SourceHandle(node='multiply_0', port='product')}, output_edges={OutputTarget(node=None, port='result'): SourceHandle(node='add_0', port='result')}, reference=PythonReference(info=VersionInfo(module='__main__', qualname='linear', version=None), inputs_with_defaults=[], restricted_input_kinds={}))" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 22 + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "d24e981330e5f22", + "metadata": {}, "source": [ "The parsed workflow has:\n", "- **inputs**: `[\"x\", \"slope\", \"intercept\"]` — the function parameters.\n", @@ -1145,8 +1298,7 @@ "\n", "The function body is still valid, runnable Python. When a WfMS executes the recipe,\n", "we expect it to produce the same result as calling the function directly." - ], - "id": "d24e981330e5f22" + ] }, { "cell_type": "markdown", @@ -1161,14 +1313,20 @@ }, { "cell_type": "code", + "execution_count": 23, "id": "d39608c2", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.284954Z", "start_time": "2026-09-08T00:49:23.267481Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.204436Z", + "iopub.status.busy": "2026-09-07T17:47:04.204374Z", + "iopub.status.idle": "2026-09-07T17:47:04.207970Z", + "shell.execute_reply": "2026-09-07T17:47:04.207684Z" } }, - "source": "linear.flowrep_recipe(x=3, slope=2, intercept=1)", "outputs": [ { "data": { @@ -1181,35 +1339,151 @@ "output_type": "execute_result" } ], - "execution_count": 23 + "source": [ + "linear.flowrep_recipe(x=3, slope=2, intercept=1)" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "62afc97568577440", + "metadata": {}, "source": [ "A recipe can be visualised directly with `.draw()`:" - ], - "id": "62afc97568577440" + ] }, { "cell_type": "code", + "execution_count": 24, "id": "80ca0e65ffd4f939", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.813950Z", "start_time": "2026-09-08T00:49:23.298183Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.208574Z", + "iopub.status.busy": "2026-09-07T17:47:04.208516Z", + "iopub.status.idle": "2026-09-07T17:47:04.235705Z", + "shell.execute_reply": "2026-09-07T17:47:04.235364Z" } }, - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "linear.flowrep_recipe.draw()" - ], "outputs": [ { "data": { - "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nworkflow\n__main__.linear\n\n\n\ninputs.x\n\nx\n\n\n\n\nmultiply_0\n\nmultiply_0\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\ninputs.x:e->multiply_0:w\n\n\n\n\n\ninputs.slope\n\nslope\n\n\n\n\ninputs.slope:e->multiply_0:w\n\n\n\n\n\ninputs.intercept\n\nintercept\n\n\n\n\nadd_0\n\nadd_0\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\ninputs.intercept:e->add_0:w\n\n\n\n\n\noutputs.result\n\nresult\n\n\n\n\nmultiply_0:e->add_0:w\n\n\n\n\n\nadd_0:e->outputs.result:w\n\n\n\n\n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster_\n", + "\n", + "workflow\n", + "__main__.linear\n", + "\n", + "\n", + "\n", + "inputs.x\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "\n", + "multiply_0\n", + "\n", + "multiply_0\n", + "__main__.multiply\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "product\n", + "\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "\n", + "inputs.x:e->multiply_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.slope\n", + "\n", + "slope\n", + "\n", + "\n", + "\n", + "\n", + "inputs.slope:e->multiply_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.intercept\n", + "\n", + "intercept\n", + "\n", + "\n", + "\n", + "\n", + "add_0\n", + "\n", + "add_0\n", + "__main__.add\n", + "\n", + "\n", + "a\n", + "\n", + "\n", + "result\n", + "\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "inputs.intercept:e->add_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "outputs.result\n", + "\n", + "result\n", + "\n", + "\n", + "\n", + "\n", + "multiply_0:e->add_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "add_0:e->outputs.result:w\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], "text/plain": [ - "" + "" ] }, "execution_count": 24, @@ -1217,7 +1491,10 @@ "output_type": "execute_result" } ], - "execution_count": 24 + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "linear.flowrep_recipe.draw()" + ] }, { "cell_type": "markdown", @@ -1239,23 +1516,42 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "273ce5e9a7bb8ded", + "metadata": {}, "source": [ "### 2.3 Nesting\n", "\n", "Workflows are arbitrarily nestable:" - ], - "id": "273ce5e9a7bb8ded" + ] }, { + "cell_type": "code", + "execution_count": 25, + "id": "ce41773494e520c8", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:23.861383Z", "start_time": "2026-09-08T00:49:23.831547Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.236477Z", + "iopub.status.busy": "2026-09-07T17:47:04.236402Z", + "iopub.status.idle": "2026-09-07T17:47:04.240977Z", + "shell.execute_reply": "2026-09-07T17:47:04.240544Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Top-level nodes: ['linear_0', 'quadratic_0']\n", + "linear_0 type: workflow\n", + "linear_0 children: ['multiply_0', 'add_0']\n" + ] + } + ], "source": [ "@fr.workflow\n", "def quadratic(x, a, b, c):\n", @@ -1279,20 +1575,7 @@ "print(f\"Top-level nodes: {list(recipe.nodes.keys())}\")\n", "print(f\"linear_0 type: {recipe.nodes['linear_0'].type}\")\n", "print(f\"linear_0 children: {list(recipe.nodes['linear_0'].nodes.keys())}\")" - ], - "id": "ce41773494e520c8", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Top-level nodes: ['linear_0', 'quadratic_0']\n", - "linear_0 type: workflow\n", - "linear_0 children: ['multiply_0', 'add_0']\n" - ] - } - ], - "execution_count": 25 + ] }, { "cell_type": "markdown", @@ -1309,33 +1592,208 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "3ea561c5cd649219", + "metadata": {}, "source": [ "Nesting is where `depth` matters: `.draw(depth=0)` leaves nested workflows closed,\n", "`.draw(depth=1)` opens one more level." - ], - "id": "3ea561c5cd649219" + ] }, { "cell_type": "code", + "execution_count": 26, "id": "26ec1c16979aa5db", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.040207Z", "start_time": "2026-09-08T00:49:23.873708Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.241643Z", + "iopub.status.busy": "2026-09-07T17:47:04.241582Z", + "iopub.status.idle": "2026-09-07T17:47:04.251682Z", + "shell.execute_reply": "2026-09-07T17:47:04.251381Z" } }, - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "recipe.draw(depth=0)" - ], "outputs": [ { "data": { - "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nworkflow\n__main__.evaluate_both\n\n\n\ninputs.x\n\nx\n\n\n\n\nlinear_0\n\nlinear_0\n__main__.linear\n\n\nx\n\n\nresult\n\n\nslope\n\n\nintercept\n\n\n\n\ninputs.x:e->linear_0:w\n\n\n\n\n\nquadratic_0\n\nquadratic_0\n__main__.quadratic\n\n\nx\n\n\nresult\n\n\na\n\n\nb\n\n\nc\n\n\n\n\ninputs.x:e->quadratic_0:w\n\n\n\n\n\ninputs.slope\n\nslope\n\n\n\n\ninputs.slope:e->linear_0:w\n\n\n\n\n\ninputs.intercept\n\nintercept\n\n\n\n\ninputs.intercept:e->linear_0:w\n\n\n\n\n\ninputs.a\n\na\n\n\n\n\ninputs.a:e->quadratic_0:w\n\n\n\n\n\ninputs.b\n\nb\n\n\n\n\ninputs.b:e->quadratic_0:w\n\n\n\n\n\ninputs.c\n\nc\n\n\n\n\ninputs.c:e->quadratic_0:w\n\n\n\n\n\noutputs.lin\n\nlin\n\n\n\n\noutputs.quad\n\nquad\n\n\n\n\nlinear_0:e->outputs.lin:w\n\n\n\n\n\nquadratic_0:e->outputs.quad:w\n\n\n\n\n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster_\n", + "\n", + "workflow\n", + "__main__.evaluate_both\n", + "\n", + "\n", + "\n", + "inputs.x\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "\n", + "linear_0\n", + "\n", + "linear_0\n", + "__main__.linear\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "result\n", + "\n", + "\n", + "slope\n", + "\n", + "\n", + "intercept\n", + "\n", + "\n", + "\n", + "\n", + "inputs.x:e->linear_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0\n", + "\n", + "quadratic_0\n", + "__main__.quadratic\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "result\n", + "\n", + "\n", + "a\n", + "\n", + "\n", + "b\n", + "\n", + "\n", + "c\n", + "\n", + "\n", + "\n", + "\n", + "inputs.x:e->quadratic_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.slope\n", + "\n", + "slope\n", + "\n", + "\n", + "\n", + "\n", + "inputs.slope:e->linear_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.intercept\n", + "\n", + "intercept\n", + "\n", + "\n", + "\n", + "\n", + "inputs.intercept:e->linear_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.a\n", + "\n", + "a\n", + "\n", + "\n", + "\n", + "\n", + "inputs.a:e->quadratic_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.b\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "inputs.b:e->quadratic_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.c\n", + "\n", + "c\n", + "\n", + "\n", + "\n", + "\n", + "inputs.c:e->quadratic_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "outputs.lin\n", + "\n", + "lin\n", + "\n", + "\n", + "\n", + "\n", + "outputs.quad\n", + "\n", + "quad\n", + "\n", + "\n", + "\n", + "\n", + "linear_0:e->outputs.lin:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0:e->outputs.quad:w\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], "text/plain": [ - "" + "" ] }, "execution_count": 26, @@ -1343,27 +1801,452 @@ "output_type": "execute_result" } ], - "execution_count": 26 + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "recipe.draw(depth=0)" + ] }, { "cell_type": "code", + "execution_count": 27, "id": "01872b7c31aaedd3", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.208054Z", "start_time": "2026-09-08T00:49:24.049713Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.252364Z", + "iopub.status.busy": "2026-09-07T17:47:04.252306Z", + "iopub.status.idle": "2026-09-07T17:47:04.265118Z", + "shell.execute_reply": "2026-09-07T17:47:04.264452Z" } }, - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "recipe.draw(depth=1)" - ], "outputs": [ { "data": { - "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nworkflow\n__main__.evaluate_both\n\n\ncluster_linear_0\n\nlinear_0\n__main__.linear\n\n\ncluster_quadratic_0\n\nquadratic_0\n__main__.quadratic\n\n\n\ninputs.x\n\nx\n\n\n\n\nlinear_0.inputs.x\n\nx\n\n\n\n\ninputs.x:e->linear_0.inputs.x:w\n\n\n\n\n\nquadratic_0.inputs.x\n\nx\n\n\n\n\ninputs.x:e->quadratic_0.inputs.x:w\n\n\n\n\n\ninputs.slope\n\nslope\n\n\n\n\nlinear_0.inputs.slope\n\nslope\n\n\n\n\ninputs.slope:e->linear_0.inputs.slope:w\n\n\n\n\n\ninputs.intercept\n\nintercept\n\n\n\n\nlinear_0.inputs.intercept\n\nintercept\n\n\n\n\ninputs.intercept:e->linear_0.inputs.intercept:w\n\n\n\n\n\ninputs.a\n\na\n\n\n\n\nquadratic_0.inputs.a\n\na\n\n\n\n\ninputs.a:e->quadratic_0.inputs.a:w\n\n\n\n\n\ninputs.b\n\nb\n\n\n\n\nquadratic_0.inputs.b\n\nb\n\n\n\n\ninputs.b:e->quadratic_0.inputs.b:w\n\n\n\n\n\ninputs.c\n\nc\n\n\n\n\nquadratic_0.inputs.c\n\nc\n\n\n\n\ninputs.c:e->quadratic_0.inputs.c:w\n\n\n\n\n\noutputs.lin\n\nlin\n\n\n\n\noutputs.quad\n\nquad\n\n\n\n\nlinear_0.multiply_0\n\nmultiply_0\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nlinear_0.inputs.x:e->linear_0.multiply_0:w\n\n\n\n\n\nlinear_0.inputs.slope:e->linear_0.multiply_0:w\n\n\n\n\n\nlinear_0.add_0\n\nadd_0\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\nlinear_0.inputs.intercept:e->linear_0.add_0:w\n\n\n\n\n\nlinear_0.outputs.result\n\nresult\n\n\n\n\nlinear_0.outputs.result:e->outputs.lin:w\n\n\n\n\n\nlinear_0.multiply_0:e->linear_0.add_0:w\n\n\n\n\n\nlinear_0.add_0:e->linear_0.outputs.result:w\n\n\n\n\n\nquadratic_0.multiply_0\n\nmultiply_0\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nquadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n\n\n\n\n\nquadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n\n\n\n\n\nquadratic_0.multiply_2\n\nmultiply_2\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nquadratic_0.inputs.x:e->quadratic_0.multiply_2:w\n\n\n\n\n\nquadratic_0.multiply_1\n\nmultiply_1\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nquadratic_0.inputs.a:e->quadratic_0.multiply_1:w\n\n\n\n\n\nquadratic_0.inputs.b:e->quadratic_0.multiply_2:w\n\n\n\n\n\nquadratic_0.add_1\n\nadd_1\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\nquadratic_0.inputs.c:e->quadratic_0.add_1:w\n\n\n\n\n\nquadratic_0.outputs.result\n\nresult\n\n\n\n\nquadratic_0.outputs.result:e->outputs.quad:w\n\n\n\n\n\nquadratic_0.multiply_0:e->quadratic_0.multiply_1:w\n\n\n\n\n\nquadratic_0.add_0\n\nadd_0\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\nquadratic_0.multiply_1:e->quadratic_0.add_0:w\n\n\n\n\n\nquadratic_0.multiply_2:e->quadratic_0.add_0:w\n\n\n\n\n\nquadratic_0.add_0:e->quadratic_0.add_1:w\n\n\n\n\n\nquadratic_0.add_1:e->quadratic_0.outputs.result:w\n\n\n\n\n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster_\n", + "\n", + "workflow\n", + "__main__.evaluate_both\n", + "\n", + "\n", + "cluster_linear_0\n", + "\n", + "linear_0\n", + "__main__.linear\n", + "\n", + "\n", + "cluster_quadratic_0\n", + "\n", + "quadratic_0\n", + "__main__.quadratic\n", + "\n", + "\n", + "\n", + "inputs.x\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.inputs.x\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "\n", + "inputs.x:e->linear_0.inputs.x:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.x\n", + "\n", + "x\n", + "\n", + "\n", + "\n", + "\n", + "inputs.x:e->quadratic_0.inputs.x:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.slope\n", + "\n", + "slope\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.inputs.slope\n", + "\n", + "slope\n", + "\n", + "\n", + "\n", + "\n", + "inputs.slope:e->linear_0.inputs.slope:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.intercept\n", + "\n", + "intercept\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.inputs.intercept\n", + "\n", + "intercept\n", + "\n", + "\n", + "\n", + "\n", + "inputs.intercept:e->linear_0.inputs.intercept:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.a\n", + "\n", + "a\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.a\n", + "\n", + "a\n", + "\n", + "\n", + "\n", + "\n", + "inputs.a:e->quadratic_0.inputs.a:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.b\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.b\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "inputs.b:e->quadratic_0.inputs.b:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.c\n", + "\n", + "c\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.c\n", + "\n", + "c\n", + "\n", + "\n", + "\n", + "\n", + "inputs.c:e->quadratic_0.inputs.c:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "outputs.lin\n", + "\n", + "lin\n", + "\n", + "\n", + "\n", + "\n", + "outputs.quad\n", + "\n", + "quad\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.multiply_0\n", + "\n", + "multiply_0\n", + "__main__.multiply\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "product\n", + "\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.inputs.x:e->linear_0.multiply_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.inputs.slope:e->linear_0.multiply_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.add_0\n", + "\n", + "add_0\n", + "__main__.add\n", + "\n", + "\n", + "a\n", + "\n", + "\n", + "result\n", + "\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.inputs.intercept:e->linear_0.add_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.outputs.result\n", + "\n", + "result\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.outputs.result:e->outputs.lin:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.multiply_0:e->linear_0.add_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "linear_0.add_0:e->linear_0.outputs.result:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.multiply_0\n", + "\n", + "multiply_0\n", + "__main__.multiply\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "product\n", + "\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.multiply_2\n", + "\n", + "multiply_2\n", + "__main__.multiply\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "product\n", + "\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.x:e->quadratic_0.multiply_2:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.multiply_1\n", + "\n", + "multiply_1\n", + "__main__.multiply\n", + "\n", + "\n", + "x\n", + "\n", + "\n", + "product\n", + "\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.a:e->quadratic_0.multiply_1:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.b:e->quadratic_0.multiply_2:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.add_1\n", + "\n", + "add_1\n", + "__main__.add\n", + "\n", + "\n", + "a\n", + "\n", + "\n", + "result\n", + "\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.inputs.c:e->quadratic_0.add_1:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.outputs.result\n", + "\n", + "result\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.outputs.result:e->outputs.quad:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.multiply_0:e->quadratic_0.multiply_1:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.add_0\n", + "\n", + "add_0\n", + "__main__.add\n", + "\n", + "\n", + "a\n", + "\n", + "\n", + "result\n", + "\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.multiply_1:e->quadratic_0.add_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.multiply_2:e->quadratic_0.add_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.add_0:e->quadratic_0.add_1:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "quadratic_0.add_1:e->quadratic_0.outputs.result:w\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], "text/plain": [ - "" + "" ] }, "execution_count": 27, @@ -1371,7 +2254,10 @@ "output_type": "execute_result" } ], - "execution_count": 27 + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "recipe.draw(depth=1)" + ] }, { "cell_type": "markdown", @@ -1388,25 +2274,20 @@ }, { "cell_type": "code", + "execution_count": 28, "id": "55716aa9", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.232809Z", "start_time": "2026-09-08T00:49:24.219164Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.266140Z", + "iopub.status.busy": "2026-09-07T17:47:04.266056Z", + "iopub.status.idle": "2026-09-07T17:47:04.269810Z", + "shell.execute_reply": "2026-09-07T17:47:04.269414Z" } }, - "source": [ - "@fr.workflow\n", - "def add_and_passthrough(a, b, tag):\n", - " result = add(a, b)\n", - " return result, tag\n", - "\n", - "\n", - "recipe = add_and_passthrough.flowrep_recipe\n", - "print(f\"Inputs: {recipe.inputs}\")\n", - "print(f\"Outputs: {recipe.outputs}\")\n", - "print(f\"Output edges: {recipe.output_edges}\")" - ], "outputs": [ { "name": "stdout", @@ -1418,7 +2299,18 @@ ] } ], - "execution_count": 28 + "source": [ + "@fr.workflow\n", + "def add_and_passthrough(a, b, tag):\n", + " result = add(a, b)\n", + " return result, tag\n", + "\n", + "\n", + "recipe = add_and_passthrough.flowrep_recipe\n", + "print(f\"Inputs: {recipe.inputs}\")\n", + "print(f\"Outputs: {recipe.outputs}\")\n", + "print(f\"Output edges: {recipe.output_edges}\")" + ] }, { "cell_type": "markdown", @@ -1433,36 +2325,33 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "258f89f599ca50a9", + "metadata": {}, "source": [ "### 2.5 Constants\n", "\n", "`@workflow` will parse JSONable constants when they are used as call arguments by inserting a \"constant\" node type to provide the value. These are restricted to JSONable elements (bool, int, float, string, and lists and dictionaries of these, recursively), because the data value must be able to appear in the serialized recipe itself!\n", "\n", "Most of the time, you'll want to expose all data as inputs, but there are some cases where the workflow makes the most sense with the value hardcoded. E.g. in this toy example, which we also use to demonstrate leveraging the standard library:" - ], - "id": "258f89f599ca50a9" + ] }, { + "cell_type": "code", + "execution_count": 29, + "id": "3c9e6f41a81f5187", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.250998Z", "start_time": "2026-09-08T00:49:24.241200Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.270542Z", + "iopub.status.busy": "2026-09-07T17:47:04.270467Z", + "iopub.status.idle": "2026-09-07T17:47:04.273717Z", + "shell.execute_reply": "2026-09-07T17:47:04.273316Z" } }, - "cell_type": "code", - "source": [ - "@fr.workflow\n", - "def kinetic_energy(mass, velocity):\n", - " v_sq = multiply(velocity, velocity)\n", - " mv_sq = multiply(mass, v_sq)\n", - " kinetic_energy = multiply(0.5, mv_sq)\n", - " return kinetic_energy\n", - "\n", - "kinetic_energy(mass=1, velocity=2)" - ], - "id": "3c9e6f41a81f5187", "outputs": [ { "data": { @@ -1475,24 +2364,41 @@ "output_type": "execute_result" } ], - "execution_count": 29 + "source": [ + "@fr.workflow\n", + "def kinetic_energy(mass, velocity):\n", + " v_sq = multiply(velocity, velocity)\n", + " mv_sq = multiply(mass, v_sq)\n", + " kinetic_energy = multiply(0.5, mv_sq)\n", + " return kinetic_energy\n", + "\n", + "kinetic_energy(mass=1, velocity=2)" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "The injected constant is just a normal node recipe with a `constant` field", - "id": "d4622385443842df" + "id": "d4622385443842df", + "metadata": {}, + "source": [ + "The injected constant is just a normal node recipe with a `constant` field" + ] }, { + "cell_type": "code", + "execution_count": 30, + "id": "964d6e961fbdc5d7", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.266388Z", "start_time": "2026-09-08T00:49:24.259105Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.274326Z", + "iopub.status.busy": "2026-09-07T17:47:04.274269Z", + "iopub.status.idle": "2026-09-07T17:47:04.277949Z", + "shell.execute_reply": "2026-09-07T17:47:04.277655Z" } }, - "cell_type": "code", - "source": "kinetic_energy.flowrep_recipe.nodes[\"constant_0\"].model_dump(mode=\"json\")", - "id": "964d6e961fbdc5d7", "outputs": [ { "data": { @@ -1509,22 +2415,43 @@ "output_type": "execute_result" } ], - "execution_count": 30 + "source": [ + "kinetic_energy.flowrep_recipe.nodes[\"constant_0\"].model_dump(mode=\"json\")" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "We do not currently allow assigning such constants to symbols, the can _only_ appear as arguments to calls", - "id": "a57e0a1dfe3da4f4" + "id": "a57e0a1dfe3da4f4", + "metadata": {}, + "source": [ + "We do not currently allow assigning such constants to symbols, the can _only_ appear as arguments to calls" + ] }, { + "cell_type": "code", + "execution_count": 31, + "id": "f177977f81a9e15a", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.282612Z", "start_time": "2026-09-08T00:49:24.274482Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.278675Z", + "iopub.status.busy": "2026-09-07T17:47:04.278615Z", + "iopub.status.idle": "2026-09-07T17:47:04.281804Z", + "shell.execute_reply": "2026-09-07T17:47:04.281437Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Workflow python definitions accept constants only as call arguments (e.g. f(x, 5)); a bare literal assignment like 'some_var = 5' is no longer supported.\n" + ] + } + ], "source": [ "try:\n", " @fr.workflow\n", @@ -1536,37 +2463,46 @@ " return kinetic_energy\n", "except ValueError as e:\n", " print(e)" - ], - "id": "f177977f81a9e15a", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Workflow python definitions accept constants only as call arguments (e.g. f(x, 5)); a bare literal assignment like 'some_var = 5' is no longer supported.\n" - ] - } - ], - "execution_count": 31 + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "a37a86a55a142d93", + "metadata": {}, "source": [ "### 2.6 The standard library\n", "\n", "For convenience, `flowrep` includes a standard library of `@atomic`-decorated nodes which we expect to be broadly useful when building . This includes wrapping most of python's builtin `operator` module, as well as item and attribute access, a simple \"identity\" node that passes its input through to its output, and more." - ], - "id": "a37a86a55a142d93" + ] }, { + "cell_type": "code", + "execution_count": 32, + "id": "a72ef46c57a9d311", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.299233Z", "start_time": "2026-09-08T00:49:24.290393Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.282425Z", + "iopub.status.busy": "2026-09-07T17:47:04.282367Z", + "iopub.status.idle": "2026-09-07T17:47:04.285455Z", + "shell.execute_reply": "2026-09-07T17:47:04.285148Z" } }, - "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": [ + "{'scaled': OutputDataPort(value=8, annotation=None)}" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@fr.atomic\n", "class SomeDataStructure:\n", @@ -1582,41 +2518,36 @@ " return scaled\n", "\n", "fr.tools.run_recipe(stdlib_workflow.flowrep_recipe, x=2, y=3, scale=4).output_ports" - ], - "id": "a72ef46c57a9d311", - "outputs": [ - { - "data": { - "text/plain": [ - "{'scaled': OutputDataPort(value=8, annotation=None)}" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 32 + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "c273e5876d5cf8da", + "metadata": {}, "source": [ "### 2.7 Power User: Recipes as workflow steps\n", "\n", "Not only will workflow definitions try to leverage the `.flowrep_recipe` recipe information when processing call steps, they will also accept recipe objects themselves.\n", "To demonstrate this, let's parse a workflow using some recipes we defined for \"awkward\" functions from our last power-user aside:" - ], - "id": "c273e5876d5cf8da" + ] }, { + "cell_type": "code", + "execution_count": 33, + "id": "e092fad543b9bfb9", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.314173Z", "start_time": "2026-09-08T00:49:24.307787Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.286101Z", + "iopub.status.busy": "2026-09-07T17:47:04.286046Z", + "iopub.status.idle": "2026-09-07T17:47:04.288807Z", + "shell.execute_reply": "2026-09-07T17:47:04.288428Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "@fr.workflow\n", "def my_workflow_from_recipes(\n", @@ -1629,10 +2560,7 @@ " this=this, that=that, the_other=numpy_arange\n", " )\n", " return output_dict" - ], - "id": "e092fad543b9bfb9", - "outputs": [], - "execution_count": 33 + ] }, { "cell_type": "markdown", @@ -1644,19 +2572,21 @@ ] }, { + "cell_type": "code", + "execution_count": 34, + "id": "7e7106b145ffbdc", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.321110Z", "start_time": "2026-09-08T00:49:24.315052Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.289393Z", + "iopub.status.busy": "2026-09-07T17:47:04.289338Z", + "iopub.status.idle": "2026-09-07T17:47:04.291662Z", + "shell.execute_reply": "2026-09-07T17:47:04.291380Z" } }, - "cell_type": "code", - "source": [ - "child_labels = list(my_workflow_from_recipes.flowrep_recipe.nodes.keys())\n", - "assert(child_labels == ['arange_0', 'my_dict_maker_0'])\n", - "print(child_labels)" - ], - "id": "7e7106b145ffbdc", "outputs": [ { "name": "stdout", @@ -1666,39 +2596,51 @@ ] } ], - "execution_count": 34 - }, - { - "metadata": { - "ExecuteTime": { + "source": [ + "child_labels = list(my_workflow_from_recipes.flowrep_recipe.nodes.keys())\n", + "assert(child_labels == ['arange_0', 'my_dict_maker_0'])\n", + "print(child_labels)" + ] + }, + { + "cell_type": "markdown", + "id": "de18e624974cf3e2", + "metadata": { + "ExecuteTime": { "end_time": "2026-07-11T03:59:27.481126Z", "start_time": "2026-07-11T03:59:27.475464Z" } }, - "cell_type": "markdown", "source": [ "Workflows will parse _any recipe_ used in this way, even other `WorkflowRecipe` recipes and flow control recipes.\n", "However, just as workflow nodes are only callable when they have an underlying python function, when that python function calls actual recipe objects, will only execute successfully if each of those recipe objects is itself callable.\n", "I.e., leveraging this feature of \"recipes as a workflow steps\" will always parse OK, but may break the call-ability of your workflow function object." - ], - "id": "de18e624974cf3e2" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "In this case, we are calling recipes wrapping python functions, which are themselves callable, and so both the workflow function and its associated recipe remain callable:", - "id": "c24cf47407659e9" + "id": "c24cf47407659e9", + "metadata": {}, + "source": [ + "In this case, we are calling recipes wrapping python functions, which are themselves callable, and so both the workflow function and its associated recipe remain callable:" + ] }, { + "cell_type": "code", + "execution_count": 35, + "id": "6a0c65a9c8b32b63", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.337902Z", "start_time": "2026-09-08T00:49:24.328946Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.292282Z", + "iopub.status.busy": "2026-09-07T17:47:04.292225Z", + "iopub.status.idle": "2026-09-07T17:47:04.294651Z", + "shell.execute_reply": "2026-09-07T17:47:04.294270Z" } }, - "cell_type": "code", - "source": "my_workflow_from_recipes.flowrep_recipe(start=0, stop=3, step=2)", - "id": "6a0c65a9c8b32b63", "outputs": [ { "data": { @@ -1711,11 +2653,14 @@ "output_type": "execute_result" } ], - "execution_count": 35 + "source": [ + "my_workflow_from_recipes.flowrep_recipe(start=0, stop=3, step=2)" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "5dd03eb5e7644e01", + "metadata": {}, "source": [ "---\n", "## 3. Edges: the connective tissue\n", @@ -1755,25 +2700,32 @@ "\n", "Using the target as the dictionary key makes this constraint automatic — you\n", "can't have two sources for the same target in a `dict`." - ], - "id": "5dd03eb5e7644e01" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "> **Aside on attribute access:** `InputSource` and `OutputTarget` do still have a `node` field, but it is locked to `None`:", - "id": "ce5c5407af6ae68a" + "id": "ce5c5407af6ae68a", + "metadata": {}, + "source": [ + "> **Aside on attribute access:** `InputSource` and `OutputTarget` do still have a `node` field, but it is locked to `None`:" + ] }, { + "cell_type": "code", + "execution_count": 36, + "id": "f91eb49fe62486aa", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.354632Z", "start_time": "2026-09-08T00:49:24.345586Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.295260Z", + "iopub.status.busy": "2026-09-07T17:47:04.295203Z", + "iopub.status.idle": "2026-09-07T17:47:04.297625Z", + "shell.execute_reply": "2026-09-07T17:47:04.297250Z" } }, - "cell_type": "code", - "source": "fr.schemas.InputSource(port=\"inp\").node, fr.schemas.OutputTarget(port=\"out\").node", - "id": "f91eb49fe62486aa", "outputs": [ { "data": { @@ -1786,38 +2738,34 @@ "output_type": "execute_result" } ], - "execution_count": 36 + "source": [ + "fr.schemas.InputSource(port=\"inp\").node, fr.schemas.OutputTarget(port=\"out\").node" + ] }, { "cell_type": "markdown", "id": "68ac7e14", "metadata": {}, - "source": "### 3.3 Inspecting edges" + "source": [ + "### 3.3 Inspecting edges" + ] }, { + "cell_type": "code", + "execution_count": 37, + "id": "7144a6706b877f78", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.370898Z", "start_time": "2026-09-08T00:49:24.362493Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.298212Z", + "iopub.status.busy": "2026-09-07T17:47:04.298158Z", + "iopub.status.idle": "2026-09-07T17:47:04.302101Z", + "shell.execute_reply": "2026-09-07T17:47:04.301742Z" } }, - "cell_type": "code", - "source": [ - "recipe = linear.flowrep_recipe\n", - "\n", - "print(\"Input edges (parent input → child input):\")\n", - "for target, source in recipe.input_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")\n", - "\n", - "print(\"\\nSibling edges (child → child):\")\n", - "for target, source in recipe.edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")\n", - "\n", - "print(\"\\nOutput edges (child → parent output):\")\n", - "for target, source in recipe.output_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")" - ], - "id": "7144a6706b877f78", "outputs": [ { "name": "stdout", @@ -1836,39 +2784,55 @@ ] } ], - "execution_count": 37 + "source": [ + "recipe = linear.flowrep_recipe\n", + "\n", + "print(\"Input edges (parent input → child input):\")\n", + "for target, source in recipe.input_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")\n", + "\n", + "print(\"\\nSibling edges (child → child):\")\n", + "for target, source in recipe.edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")\n", + "\n", + "print(\"\\nOutput edges (child → parent output):\")\n", + "for target, source in recipe.output_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")" + ] }, { + "cell_type": "markdown", + "id": "56384e86f385779d", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.527620Z", "start_time": "2026-07-11T03:59:27.515088Z" } }, - "cell_type": "markdown", "source": [ "### 3.4 Serialisation and the dot notation\n", "\n", "Edges serialise to compact dot-separated strings. A `SourceHandle(\"add_0\", \"c\")`\n", "becomes `\"add_0.c\"`, while an `InputSource(\"x\")` (which has no node) becomes\n", "just `\"x\"`. This keeps the JSON representation concise:" - ], - "id": "56384e86f385779d" + ] }, { "cell_type": "code", + "execution_count": 38, "id": "13f799b9", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.388114Z", "start_time": "2026-09-08T00:49:24.379120Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.302680Z", + "iopub.status.busy": "2026-09-07T17:47:04.302626Z", + "iopub.status.idle": "2026-09-07T17:47:04.306054Z", + "shell.execute_reply": "2026-09-07T17:47:04.305708Z" } }, - "source": [ - "import json\n", - "\n", - "print(json.dumps(json.loads(recipe.model_dump_json()), indent=2))" - ], "outputs": [ { "name": "stdout", @@ -1951,7 +2915,11 @@ ] } ], - "execution_count": 38 + "source": [ + "import json\n", + "\n", + "print(json.dumps(json.loads(recipe.model_dump_json()), indent=2))" + ] }, { "cell_type": "markdown", @@ -1970,20 +2938,20 @@ }, { "cell_type": "code", + "execution_count": 39, "id": "e67cac8d", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.405113Z", "start_time": "2026-09-08T00:49:24.396613Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.306626Z", + "iopub.status.busy": "2026-09-07T17:47:04.306572Z", + "iopub.status.idle": "2026-09-07T17:47:04.309966Z", + "shell.execute_reply": "2026-09-07T17:47:04.309571Z" } }, - "source": [ - "# Round-trip demo\n", - "json_str = recipe.model_dump_json()\n", - "reconstructed = fr.schemas.WorkflowRecipe.model_validate_json(json_str)\n", - "assert reconstructed == recipe\n", - "print(\"Round-trip successful!\")" - ], "outputs": [ { "name": "stdout", @@ -1993,7 +2961,13 @@ ] } ], - "execution_count": 39 + "source": [ + "# Round-trip demo\n", + "json_str = recipe.model_dump_json()\n", + "reconstructed = fr.schemas.WorkflowRecipe.model_validate_json(json_str)\n", + "assert reconstructed == recipe\n", + "print(\"Round-trip successful!\")" + ] }, { "cell_type": "markdown", @@ -2027,13 +3001,32 @@ ] }, { + "cell_type": "code", + "execution_count": 40, + "id": "52f01feb2783079d", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.423440Z", "start_time": "2026-09-08T00:49:24.413257Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.310581Z", + "iopub.status.busy": "2026-09-07T17:47:04.310522Z", + "iopub.status.idle": "2026-09-07T17:47:04.314901Z", + "shell.execute_reply": "2026-09-07T17:47:04.314519Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "abs_value(-3) = 3\n", + "abs_value(5) = 5\n", + "Recipe nodes: ['if_0']\n" + ] + } + ], "source": [ "# A preview — we'll dissect each type below\n", "def less_than(a, b):\n", @@ -2058,20 +3051,7 @@ "print(f\"abs_value(-3) = {abs_value(-3)}\")\n", "print(f\"abs_value(5) = {abs_value(5)}\")\n", "print(f\"Recipe nodes: {list(abs_value.flowrep_recipe.nodes.keys())}\")" - ], - "id": "52f01feb2783079d", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "abs_value(-3) = 3\n", - "abs_value(5) = 5\n", - "Recipe nodes: ['if_0']\n" - ] - } - ], - "execution_count": 40 + ] }, { "cell_type": "markdown", @@ -2163,13 +3143,14 @@ ] }, { + "cell_type": "markdown", + "id": "371165167183fdce", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.595280Z", "start_time": "2026-07-11T03:59:27.580498Z" } }, - "cell_type": "markdown", "source": [ "---\n", "### 4.2 ForEach-nodes\n", @@ -2192,17 +3173,41 @@ "- Each step in the loop is independent of other steps, and could be parallelized.\n", "\n", "#### Parsed example" - ], - "id": "371165167183fdce" + ] }, { + "cell_type": "code", + "execution_count": 41, + "id": "bf71d040c5bb5835", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.440071Z", "start_time": "2026-09-08T00:49:24.431292Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.315604Z", + "iopub.status.busy": "2026-09-07T17:47:04.315541Z", + "iopub.status.idle": "2026-09-07T17:47:04.320155Z", + "shell.execute_reply": "2026-09-07T17:47:04.319736Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "double_all([1,2,3]) = [2, 4, 6]\n", + "\n", + "ForEach-node type: for_each\n", + "Inputs: ['items']\n", + "Outputs: ['results']\n", + "Nested ports: ['item']\n", + "Zipped ports: []\n", + "Input edges: {TargetHandle(node='body', port='item'): InputSource(node=None, port='items')}\n", + "Body node: body -> WorkflowRecipe\n" + ] + } + ], "source": [ "@fr.workflow\n", "def double_all(items):\n", @@ -2226,26 +3231,7 @@ "print(\n", " f\"Body node: {for_node.body_node.label} -> {type(for_node.body_node.recipe).__name__}\"\n", ")" - ], - "id": "bf71d040c5bb5835", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "double_all([1,2,3]) = [2, 4, 6]\n", - "\n", - "ForEach-node type: for_each\n", - "Inputs: ['items']\n", - "Outputs: ['results']\n", - "Nested ports: ['item']\n", - "Zipped ports: []\n", - "Input edges: {TargetHandle(node='body', port='item'): InputSource(node=None, port='items')}\n", - "Body node: body -> WorkflowRecipe\n" - ] - } - ], - "execution_count": 41 + ] }, { "cell_type": "markdown", @@ -2259,33 +3245,146 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "3c4ea6eb5f591c86", + "metadata": {}, "source": [ "The `nested` badge on the body's `item` port in the drawing below is information the\n", "printout above doesn't show:" - ], - "id": "3c4ea6eb5f591c86" + ] }, { "cell_type": "code", + "execution_count": 42, "id": "81cce7c2fbd6a1f1", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.619656Z", "start_time": "2026-09-08T00:49:24.447628Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.320857Z", + "iopub.status.busy": "2026-09-07T17:47:04.320790Z", + "iopub.status.idle": "2026-09-07T17:47:04.331825Z", + "shell.execute_reply": "2026-09-07T17:47:04.331416Z" } }, - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "for_node.draw()" - ], "outputs": [ { "data": { - "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nfor_each\n\n\ncluster_body\n\nbody_n\n\n\n\ninputs.items\n\nitems\n\n\n\n\nbody.inputs.item\n\nitem\nnested\n\n\n\n\ninputs.items:e->body.inputs.item:w\n\n\n\n\n\noutputs.results\n\nresults\n\n\n\n\nbody.mul_0\n\nmul_0\nflowrep.std.mul\n\n\na\n\n\nproduct\n\n\nb\n\n\n\n\nbody.inputs.item:e->body.mul_0:w\n\n\n\n\n\nbody.outputs.doubled\n\ndoubled\n\n\n\n\nbody.outputs.doubled:e->outputs.results:w\n\n\n\n\n\nbody.mul_0:e->body.outputs.doubled:w\n\n\n\n\n\nbody.constant_0\n\nconstant_0\n2\n\n\nconstant\n\n\n\n\nbody.constant_0:e->body.mul_0:w\n\n\n\n\n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster_\n", + "\n", + "for_each\n", + "\n", + "\n", + "cluster_body\n", + "\n", + "body_n\n", + "\n", + "\n", + "\n", + "inputs.items\n", + "\n", + "items\n", + "\n", + "\n", + "\n", + "\n", + "body.inputs.item\n", + "\n", + "item\n", + "nested\n", + "\n", + "\n", + "\n", + "\n", + "inputs.items:e->body.inputs.item:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "outputs.results\n", + "\n", + "results\n", + "\n", + "\n", + "\n", + "\n", + "body.mul_0\n", + "\n", + "mul_0\n", + "flowrep.std.mul\n", + "\n", + "\n", + "a\n", + "\n", + "\n", + "product\n", + "\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "\n", + "body.inputs.item:e->body.mul_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body.outputs.doubled\n", + "\n", + "doubled\n", + "\n", + "\n", + "\n", + "\n", + "body.outputs.doubled:e->outputs.results:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body.mul_0:e->body.outputs.doubled:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body.constant_0\n", + "\n", + "constant_0\n", + "2\n", + "\n", + "\n", + "constant\n", + "\n", + "\n", + "\n", + "\n", + "body.constant_0:e->body.mul_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], "text/plain": [ - "" + "" ] }, "execution_count": 42, @@ -2293,11 +3392,15 @@ "output_type": "execute_result" } ], - "execution_count": 42 + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "for_node.draw()" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "e0b3f4ba0331ef9c", + "metadata": {}, "source": [ "#### Broadcasting and scattering\n", "\n", @@ -2305,17 +3408,37 @@ "execute its recipes. For for-nodes, that means inputs that aren't iterated over are\n", "*broadcast* (sent to every body instance), while inputs that are iterated over are\n", "*scattered* (each body instance gets one element):" - ], - "id": "e0b3f4ba0331ef9c" + ] }, { + "cell_type": "code", + "execution_count": 43, + "id": "b74048bfcfa45a5c", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.695899Z", "start_time": "2026-09-08T00:49:24.656627Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.332503Z", + "iopub.status.busy": "2026-09-07T17:47:04.332439Z", + "iopub.status.idle": "2026-09-07T17:47:04.335615Z", + "shell.execute_reply": "2026-09-07T17:47:04.335305Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inputs: ['factor', 'items']\n", + "Nested ports (scattered): ['item']\n", + "Input edges:\n", + " factor → body.factor\n", + " items → body.item\n" + ] + } + ], "source": [ "@fr.workflow\n", "def scale_all(items, factor):\n", @@ -2332,22 +3455,7 @@ "print(\"Input edges:\")\n", "for target, source in for_node.input_edges.items():\n", " print(f\" {source.serialize()} → {target.serialize()}\")" - ], - "id": "b74048bfcfa45a5c", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inputs: ['factor', 'items']\n", - "Nested ports (scattered): ['item']\n", - "Input edges:\n", - " factor → body.factor\n", - " items → body.item\n" - ] - } - ], - "execution_count": 43 + ] }, { "cell_type": "markdown", @@ -2367,16 +3475,36 @@ "metadata": { "lines_to_next_cell": 2 }, - "source": "#### Zipped iteration" + "source": [ + "#### Zipped iteration" + ] }, { + "cell_type": "code", + "execution_count": 44, + "id": "fb336f6a620be411", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.730154Z", "start_time": "2026-09-08T00:49:24.711628Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.336356Z", + "iopub.status.busy": "2026-09-07T17:47:04.336285Z", + "iopub.status.idle": "2026-09-07T17:47:04.339817Z", + "shell.execute_reply": "2026-09-07T17:47:04.339547Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Zipped ports: ['x', 'y']\n", + "Nested ports: []\n" + ] + } + ], "source": [ "@fr.workflow\n", "def add_pairwise(xs, ys):\n", @@ -2390,19 +3518,7 @@ "for_node = add_pairwise.flowrep_recipe.nodes[\"for_each_0\"]\n", "print(f\"Zipped ports: {for_node.zipped_ports}\")\n", "print(f\"Nested ports: {for_node.nested_ports}\")" - ], - "id": "fb336f6a620be411", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Zipped ports: ['x', 'y']\n", - "Nested ports: []\n" - ] - } - ], - "execution_count": 44 + ] }, { "cell_type": "markdown", @@ -2416,13 +3532,32 @@ ] }, { + "cell_type": "code", + "execution_count": 45, + "id": "bc49c138cf8fd040", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.782105Z", "start_time": "2026-09-08T00:49:24.745839Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.340518Z", + "iopub.status.busy": "2026-09-07T17:47:04.340460Z", + "iopub.status.idle": "2026-09-07T17:47:04.343728Z", + "shell.execute_reply": "2026-09-07T17:47:04.343342Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outputs: ['results', 'sources']\n", + "Transferred (input-forwarded) outputs:\n", + " sources sourced from input 'items'\n" + ] + } + ], "source": [ "@fr.workflow\n", "def process_and_track(items):\n", @@ -2440,29 +3575,17 @@ "print(f\"Transferred (input-forwarded) outputs:\")\n", "for target, source in for_node.transferred_outputs.items():\n", " print(f\" {target.serialize()} sourced from input '{source.port}'\")" - ], - "id": "bc49c138cf8fd040", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Outputs: ['results', 'sources']\n", - "Transferred (input-forwarded) outputs:\n", - " sources sourced from input 'items'\n" - ] - } - ], - "execution_count": 45 + ] }, { + "cell_type": "markdown", + "id": "69d06b59a0b7bc26", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.704571Z", "start_time": "2026-07-11T03:59:27.688279Z" } }, - "cell_type": "markdown", "source": [ "#### What's not supported\n", "\n", @@ -2479,12 +3602,12 @@ "- **Iteration sources must be symbols**, not inline expressions.\n", "- **Non-iterated pass-through outputs are forbidden.** Output edges from\n", " `InputSource` are only valid when the input is being iterated on." - ], - "id": "69d06b59a0b7bc26" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "78354acb95e458e6", + "metadata": {}, "source": [ "---\n", "### 4.3 If-nodes\n", @@ -2504,17 +3627,42 @@ "This last point introduces **prospective output edges**: a mapping from each output\n", "port to a *list* of possible sources — one per branch that can produce it. At\n", "runtime, exactly one of these sources will be actualized." - ], - "id": "78354acb95e458e6" + ] }, { + "cell_type": "code", + "execution_count": 46, + "id": "b1ae44ab3952db8b", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.818984Z", "start_time": "2026-09-08T00:49:24.799031Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.344334Z", + "iopub.status.busy": "2026-09-07T17:47:04.344278Z", + "iopub.status.idle": "2026-09-07T17:47:04.347569Z", + "shell.execute_reply": "2026-09-07T17:47:04.347309Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Type: if\n", + "Inputs: ['x']\n", + "Outputs: ['result']\n", + "Number of cases: 1\n", + "Has else: True\n", + "\n", + "Prospective output edges:\n", + " result can come from:\n", + " - body_0.result\n", + " - else_body.result\n" + ] + } + ], "source": [ "@fr.workflow\n", "def sign(x):\n", @@ -2537,27 +3685,7 @@ " print(f\" {target.serialize()} can come from:\")\n", " for s in sources:\n", " print(f\" - {s.serialize()}\")" - ], - "id": "b1ae44ab3952db8b", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Type: if\n", - "Inputs: ['x']\n", - "Outputs: ['result']\n", - "Number of cases: 1\n", - "Has else: True\n", - "\n", - "Prospective output edges:\n", - " result can come from:\n", - " - body_0.result\n", - " - else_body.result\n" - ] - } - ], - "execution_count": 46 + ] }, { "cell_type": "markdown", @@ -2608,34 +3736,21 @@ ] }, { + "cell_type": "code", + "execution_count": 47, + "id": "14cd83686b680b", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.869943Z", "start_time": "2026-09-08T00:49:24.834194Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.348353Z", + "iopub.status.busy": "2026-09-07T17:47:04.348298Z", + "iopub.status.idle": "2026-09-07T17:47:04.351508Z", + "shell.execute_reply": "2026-09-07T17:47:04.351258Z" } }, - "cell_type": "code", - "source": [ - "# Aside example\n", - "import traceback\n", - "\n", - "# Parses acceptably\n", - "@fr.workflow\n", - "def conditional_availability(x):\n", - " if is_positive(x):\n", - " result = fr.std.identity(x)\n", - " downstream = fr.std.identity(result)\n", - " return downstream\n", - "\n", - "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].outputs)\n", - "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].prospective_output_edges)\n", - "\n", - "try:\n", - " conditional_availability(-1)\n", - "except Exception:\n", - " traceback.print_exc()" - ], - "id": "14cd83686b680b", "outputs": [ { "name": "stdout", @@ -2650,16 +3765,36 @@ "output_type": "stream", "text": [ "Traceback (most recent call last):\n", - " File \"/var/folders/nn/6kd6nhmj2rx7610kd9r_8h0m0000gn/T/ipykernel_13569/3523357707.py\", line 16, in \n", + " File \"/tmp/ipykernel_51635/3523357707.py\", line 16, in \n", " conditional_availability(-1)\n", - " File \"/var/folders/nn/6kd6nhmj2rx7610kd9r_8h0m0000gn/T/ipykernel_13569/3523357707.py\", line 9, in conditional_availability\n", + " ~~~~~~~~~~~~~~~~~~~~~~~~^^^^\n", + " File \"/tmp/ipykernel_51635/3523357707.py\", line 9, in conditional_availability\n", " downstream = fr.std.identity(result)\n", " ^^^^^^\n", "UnboundLocalError: cannot access local variable 'result' where it is not associated with a value\n" ] } ], - "execution_count": 47 + "source": [ + "# Aside example\n", + "import traceback\n", + "\n", + "# Parses acceptably\n", + "@fr.workflow\n", + "def conditional_availability(x):\n", + " if is_positive(x):\n", + " result = fr.std.identity(x)\n", + " downstream = fr.std.identity(result)\n", + " return downstream\n", + "\n", + "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].outputs)\n", + "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].prospective_output_edges)\n", + "\n", + "try:\n", + " conditional_availability(-1)\n", + "except Exception:\n", + " traceback.print_exc()" + ] }, { "cell_type": "markdown", @@ -2685,13 +3820,22 @@ ] }, { + "cell_type": "code", + "execution_count": 48, + "id": "f170e946bc3d3e53", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:24.904725Z", "start_time": "2026-09-08T00:49:24.885947Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.352215Z", + "iopub.status.busy": "2026-09-07T17:47:04.352159Z", + "iopub.status.idle": "2026-09-07T17:47:04.354682Z", + "shell.execute_reply": "2026-09-07T17:47:04.354396Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "@fr.atomic\n", "def is_below_threshold(value, threshold):\n", @@ -2703,19 +3847,17 @@ "def increment(x):\n", " result = x + 1\n", " return result" - ], - "id": "f170e946bc3d3e53", - "outputs": [], - "execution_count": 48 + ] }, { + "cell_type": "markdown", + "id": "e88ea8e75fb8b197", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.757249Z", "start_time": "2026-07-11T03:59:27.739975Z" } }, - "cell_type": "markdown", "source": [ "A natural first attempt might be:\n", "\n", @@ -2731,19 +3873,40 @@ "But `value = start` is a bare name-to-name assignment, which the workflow parser\n", "doesn't support (right-hand sides must be function calls or empty lists). Instead,\n", "we work directly with the function parameter, or use an identity function:" - ], - "id": "e88ea8e75fb8b197" + ] }, { "cell_type": "code", + "execution_count": 49, "id": "460686cf", "metadata": { - "lines_to_next_cell": 2, "ExecuteTime": { "end_time": "2026-09-08T00:49:24.937635Z", "start_time": "2026-09-08T00:49:24.917364Z" - } + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.355360Z", + "iopub.status.busy": "2026-09-07T17:47:04.355293Z", + "iopub.status.idle": "2026-09-07T17:47:04.358457Z", + "shell.execute_reply": "2026-09-07T17:47:04.358187Z" + }, + "lines_to_next_cell": 2 }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "count_up(0, 5) = 5\n", + "\n", + "Type: while\n", + "Inputs: ['start', 'threshold']\n", + "Outputs: ['start']\n", + "Condition: condition\n", + "Body: body\n" + ] + } + ], "source": [ "@fr.workflow\n", "def count_up(start, threshold):\n", @@ -2762,43 +3925,25 @@ "print(f\"Body: {while_node.case.body.label}\")\n", "#\n", "# The while-node also exposes inferred iteration edges:" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "count_up(0, 5) = 5\n", - "\n", - "Type: while\n", - "Inputs: ['start', 'threshold']\n", - "Outputs: ['start']\n", - "Condition: condition\n", - "Body: body\n" - ] - } - ], - "execution_count": 49 + ] }, { "cell_type": "code", + "execution_count": 50, "id": "515eae1c", "metadata": { - "lines_to_next_cell": 2, "ExecuteTime": { "end_time": "2026-09-08T00:49:24.972230Z", "start_time": "2026-09-08T00:49:24.951542Z" - } + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.359285Z", + "iopub.status.busy": "2026-09-07T17:47:04.359222Z", + "iopub.status.idle": "2026-09-07T17:47:04.361620Z", + "shell.execute_reply": "2026-09-07T17:47:04.361287Z" + }, + "lines_to_next_cell": 2 }, - "source": [ - "print(\"Body → body (next iteration) edges:\")\n", - "for target, source in while_node.body_body_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")\n", - "\n", - "print(\"\\nBody → condition (next check) edges:\")\n", - "for target, source in while_node.body_condition_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")" - ], "outputs": [ { "name": "stdout", @@ -2812,7 +3957,15 @@ ] } ], - "execution_count": 50 + "source": [ + "print(\"Body → body (next iteration) edges:\")\n", + "for target, source in while_node.body_body_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")\n", + "\n", + "print(\"\\nBody → condition (next check) edges:\")\n", + "for target, source in while_node.body_condition_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")" + ] }, { "cell_type": "markdown", @@ -2861,13 +4014,38 @@ ] }, { + "cell_type": "code", + "execution_count": 51, + "id": "4cbe44877e1af10e", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.097055Z", "start_time": "2026-09-08T00:49:24.986713Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.362292Z", + "iopub.status.busy": "2026-09-07T17:47:04.362231Z", + "iopub.status.idle": "2026-09-07T17:47:04.366828Z", + "shell.execute_reply": "2026-09-07T17:47:04.366397Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "careful_divide(10, 2) = 5.0\n", + "careful_divide(10, 0) = 0\n", + "\n", + "Type: try\n", + "Inputs: ['a', 'b']\n", + "Outputs: ['result']\n", + "Try body: try_body\n", + "Exception cases: 1\n", + " [0] catches ['builtins.ZeroDivisionError'] → body 'except_body_0'\n" + ] + } + ], "source": [ "@fr.atomic\n", "def zero():\n", @@ -2896,26 +4074,7 @@ "for i, case in enumerate(try_node.exception_cases):\n", " exc_names = [e.fully_qualified_name for e in case.exceptions]\n", " print(f\" [{i}] catches {exc_names} → body '{case.body.label}'\")" - ], - "id": "4cbe44877e1af10e", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "careful_divide(10, 2) = 5.0\n", - "careful_divide(10, 0) = 0\n", - "\n", - "Type: try\n", - "Inputs: ['a', 'b']\n", - "Outputs: ['result']\n", - "Try body: try_body\n", - "Exception cases: 1\n", - " [0] catches ['builtins.ZeroDivisionError'] → body 'except_body_0'\n" - ] - } - ], - "execution_count": 51 + ] }, { "cell_type": "markdown", @@ -2949,17 +4108,20 @@ }, { "cell_type": "code", + "execution_count": 52, "id": "d76836d6", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.170604Z", "start_time": "2026-09-08T00:49:25.116830Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.367488Z", + "iopub.status.busy": "2026-09-07T17:47:04.367424Z", + "iopub.status.idle": "2026-09-07T17:47:04.369603Z", + "shell.execute_reply": "2026-09-07T17:47:04.369266Z" } }, - "source": [ - "intermediate = count_up.flowrep_recipe.nodes[\"while_0\"].case.body\n", - "print(f\"The intermediate node {intermediate.label!r} is of type {str(intermediate.recipe.type)!r}\")" - ], "outputs": [ { "name": "stdout", @@ -2969,27 +4131,35 @@ ] } ], - "execution_count": 52 + "source": [ + "intermediate = count_up.flowrep_recipe.nodes[\"while_0\"].case.body\n", + "print(f\"The intermediate node {intermediate.label!r} is of type {str(intermediate.recipe.type)!r}\")" + ] }, { "cell_type": "markdown", "id": "ef63fe36", "metadata": {}, - "source": "But, strictly speaking, this intermediate workflow is not necessary since it anyhow has a single child -- the atomic incrementer:" + "source": [ + "But, strictly speaking, this intermediate workflow is not necessary since it anyhow has a single child -- the atomic incrementer:" + ] }, { + "cell_type": "code", + "execution_count": 53, + "id": "880a7caa20cb48ec", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.214732Z", "start_time": "2026-09-08T00:49:25.172797Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.370226Z", + "iopub.status.busy": "2026-09-07T17:47:04.370162Z", + "iopub.status.idle": "2026-09-07T17:47:04.372832Z", + "shell.execute_reply": "2026-09-07T17:47:04.372456Z" } }, - "cell_type": "code", - "source": [ - "child_label, child_recipe = next(iter(intermediate.recipe.nodes.items()))\n", - "print(f\"This workflow has a single-node body: {child_label!r} of type {str(child_recipe.type)!r}\")" - ], - "id": "880a7caa20cb48ec", "outputs": [ { "name": "stdout", @@ -2999,16 +4169,20 @@ ] } ], - "execution_count": 53 + "source": [ + "child_label, child_recipe = next(iter(intermediate.recipe.nodes.items()))\n", + "print(f\"This workflow has a single-node body: {child_label!r} of type {str(child_recipe.type)!r}\")" + ] }, { + "cell_type": "markdown", + "id": "d9bf38390b916b62", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.823205Z", "start_time": "2026-07-11T03:59:27.810632Z" } }, - "cell_type": "markdown", "source": [ "---\n", "## 5. Aside on Version Provenance\n", @@ -3026,66 +4200,82 @@ "- `forbid_locals=True`: Reject functions defined inside other functions\n", " (whose qualname contains ``).\n", "- `require_version=True`: Reject functions whose package has no version." - ], - "id": "d9bf38390b916b62" + ] }, { + "cell_type": "code", + "execution_count": 54, + "id": "257fa2cc1c78265d", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.253059Z", "start_time": "2026-09-08T00:49:25.216523Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.373477Z", + "iopub.status.busy": "2026-09-07T17:47:04.373411Z", + "iopub.status.idle": "2026-09-07T17:47:04.377943Z", + "shell.execute_reply": "2026-09-07T17:47:04.377537Z" } }, - "cell_type": "code", - "source": [ - "# This will fail because we're in __main__:\n", - "try:\n", - "\n", - " @fr.atomic(forbid_main=True)\n", - " def my_func(x):\n", - " y = x\n", - " return y\n", - "\n", - "except Exception:\n", - " traceback.print_exc()" - ], - "id": "257fa2cc1c78265d", "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", - " File \"/var/folders/nn/6kd6nhmj2rx7610kd9r_8h0m0000gn/T/ipykernel_13569/3041652459.py\", line 4, in \n", + " File \"/tmp/ipykernel_51635/3041652459.py\", line 4, in \n", " @fr.atomic(forbid_main=True)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " ~~~~~~~~~^^^^^^^^^^^^^^^^^^\n", " File \"/Users/liamhuber/dev/pyiron/flowrep/src/flowrep/parsers/parser_helpers.py\", line 64, in deferred\n", " return wrap(f, labels)\n", - " ^^^^^^^^^^^^^^^\n", " File \"/Users/liamhuber/dev/pyiron/flowrep/src/flowrep/parsers/atomic_parser.py\", line 71, in wrap\n", " f.flowrep_recipe = parse_atomic(f, *labels, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^\n", " File \"/Users/liamhuber/dev/pyiron/flowrep/src/flowrep/parsers/atomic_parser.py\", line 147, in parse_atomic\n", " function_info = versions.VersionInfo.of(\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/Users/liamhuber/dev/pyiron/pyiron_snippets/pyiron_snippets/versions.py\", line 125, in of\n", + " func,\n", + " ...<3 lines>...\n", + " require_version=require_version,\n", + " )\n", + " File \"/Users/liamhuber/dev/pyiron/flowrep/.venv/lib/python3.14/site-packages/pyiron_snippets/versions.py\", line 125, in of\n", " info.validate_constraints(\n", - " File \"/Users/liamhuber/dev/pyiron/pyiron_snippets/pyiron_snippets/versions.py\", line 143, in validate_constraints\n", + " ~~~~~~~~~~~~~~~~~~~~~~~~~^\n", + " forbid_main=forbid_main,\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^\n", + " ...<3 lines>...\n", + " strict=strict,\n", + " ^^^^^^^^^^^^^^\n", + " )\n", + " ^\n", + " File \"/Users/liamhuber/dev/pyiron/flowrep/.venv/lib/python3.14/site-packages/pyiron_snippets/versions.py\", line 143, in validate_constraints\n", " raise ValueError(f\"Found forbidden module '__main__' in module for {self}\")\n", "ValueError: Found forbidden module '__main__' in module for VersionInfo(module='__main__', qualname='my_func', version=None)\n" ] } ], - "execution_count": 54 + "source": [ + "# This will fail because we're in __main__:\n", + "try:\n", + "\n", + " @fr.atomic(forbid_main=True)\n", + " def my_func(x):\n", + " y = x\n", + " return y\n", + "\n", + "except Exception:\n", + " traceback.print_exc()" + ] }, { + "cell_type": "markdown", + "id": "1b2d2b33f6e90e59", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.836844Z", "start_time": "2026-07-11T03:59:27.824048Z" } }, - "cell_type": "markdown", "source": [ "These constraints **propagate through the entire parse tree**. When `@workflow`\n", "parses a function body and encounters child function calls, the same\n", @@ -3097,8 +4287,7 @@ "In a real package context, this ensures that a recipe built with\n", "`require_version=True` will fail at parse time if *any* function in the entire\n", "call graph comes from an unversioned package." - ], - "id": "1b2d2b33f6e90e59" + ] }, { "cell_type": "markdown", @@ -3113,21 +4302,20 @@ }, { "cell_type": "code", + "execution_count": 55, "id": "3efa3180", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.290347Z", "start_time": "2026-09-08T00:49:25.254929Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.378601Z", + "iopub.status.busy": "2026-09-07T17:47:04.378534Z", + "iopub.status.idle": "2026-09-07T17:47:04.382029Z", + "shell.execute_reply": "2026-09-07T17:47:04.381642Z" } }, - "source": [ - "# Hypothetical example for a package with non-standard versioning:\n", - "@fr.atomic(version_scraping={\"__main__\": lambda _: \"1.2.3\"})\n", - "def my_func(x):\n", - " return x\n", - "\n", - "print(my_func.flowrep_recipe.reference.info.version)" - ], "outputs": [ { "name": "stdout", @@ -3137,7 +4325,14 @@ ] } ], - "execution_count": 55 + "source": [ + "# Hypothetical example for a package with non-standard versioning:\n", + "@fr.atomic(version_scraping={\"__main__\": lambda _: \"1.2.3\"})\n", + "def my_func(x):\n", + " return x\n", + "\n", + "print(my_func.flowrep_recipe.reference.info.version)" + ] }, { "cell_type": "markdown", @@ -3153,13 +4348,30 @@ ] }, { + "cell_type": "code", + "execution_count": 56, + "id": "369c2717ee847047", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.308539Z", "start_time": "2026-09-08T00:49:25.292292Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.382667Z", + "iopub.status.busy": "2026-09-07T17:47:04.382604Z", + "iopub.status.idle": "2026-09-07T17:47:04.385926Z", + "shell.execute_reply": "2026-09-07T17:47:04.385533Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This recipe already has an underlying Python reference: info=VersionInfo(module='__main__', qualname='my_recipe', version=None) inputs_with_defaults=['y'] restricted_input_kinds={}\n" + ] + } + ], "source": [ "@fr.workflow\n", "def my_recipe(x: int, y: int = 4):\n", @@ -3170,18 +4382,7 @@ " fr.tools.flowrep2python(my_recipe.flowrep_recipe)\n", "except ValueError as e:\n", " print(e)" - ], - "id": "369c2717ee847047", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This recipe already has an underlying Python reference: info=VersionInfo(module='__main__', qualname='my_recipe', version=None) inputs_with_defaults=['y'] restricted_input_kinds={}\n" - ] - } - ], - "execution_count": 56 + ] }, { "cell_type": "markdown", @@ -3199,42 +4400,56 @@ ] }, { + "cell_type": "code", + "execution_count": 57, + "id": "e0ac6cf3086d983a", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.324399Z", "start_time": "2026-09-08T00:49:25.309756Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.386567Z", + "iopub.status.busy": "2026-09-07T17:47:04.386503Z", + "iopub.status.idle": "2026-09-07T17:47:04.388630Z", + "shell.execute_reply": "2026-09-07T17:47:04.388319Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "unreferenced_recipe = my_recipe.flowrep_recipe.model_copy(update={\"reference\": None})\n", "rendered = fr.tools.flowrep2python(unreferenced_recipe)" - ], - "id": "e0ac6cf3086d983a", - "outputs": [], - "execution_count": 57 + ] }, { + "cell_type": "markdown", + "id": "31639b9ce08f4f94", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.881332Z", "start_time": "2026-07-11T03:59:27.867580Z" } }, - "cell_type": "markdown", - "source": "We can look at the python source code generated from with this recipe, and if we wanted we could `.dump(filepath)` it to a `.py` file", - "id": "31639b9ce08f4f94" + "source": [ + "We can look at the python source code generated from with this recipe, and if we wanted we could `.dump(filepath)` it to a `.py` file" + ] }, { + "cell_type": "code", + "execution_count": 58, + "id": "f906e1e3d41e5b3b", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.364514Z", "start_time": "2026-09-08T00:49:25.325354Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.389287Z", + "iopub.status.busy": "2026-09-07T17:47:04.389226Z", + "iopub.status.idle": "2026-09-07T17:47:04.391804Z", + "shell.execute_reply": "2026-09-07T17:47:04.391427Z" } }, - "cell_type": "code", - "source": "print(rendered.source)", - "id": "f906e1e3d41e5b3b", "outputs": [ { "name": "stdout", @@ -3255,32 +4470,39 @@ ] } ], - "execution_count": 58 + "source": [ + "print(rendered.source)" + ] }, { + "cell_type": "markdown", + "id": "664078176aceb6e0", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.886572Z", "start_time": "2026-07-11T03:59:27.882757Z" } }, - "cell_type": "markdown", - "source": "We can also generate an actual living python function object", - "id": "664078176aceb6e0" + "source": [ + "We can also generate an actual living python function object" + ] }, { + "cell_type": "code", + "execution_count": 59, + "id": "612788a582d4680", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.401131Z", "start_time": "2026-09-08T00:49:25.366200Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.392448Z", + "iopub.status.busy": "2026-09-07T17:47:04.392382Z", + "iopub.status.idle": "2026-09-07T17:47:04.395946Z", + "shell.execute_reply": "2026-09-07T17:47:04.395514Z" } }, - "cell_type": "code", - "source": [ - "rebuilt = rendered.build()\n", - "rebuilt(1, 2)" - ], - "id": "612788a582d4680", "outputs": [ { "data": { @@ -3293,47 +4515,42 @@ "output_type": "execute_result" } ], - "execution_count": 59 + "source": [ + "rebuilt = rendered.build()\n", + "rebuilt(1, 2)" + ] }, { + "cell_type": "markdown", + "id": "b05174603f66abe0", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.899934Z", "start_time": "2026-07-11T03:59:27.887114Z" } }, - "cell_type": "markdown", "source": [ "### 6.2 Simple signatures\n", "\n", "Flowrep recipes don't know about annotations or defaults, so you'll notice that our compiled source is missing these. You are free to specify them at call-time as a signature object" - ], - "id": "b05174603f66abe0" + ] }, { + "cell_type": "code", + "execution_count": 60, + "id": "54362e50de84e98d", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.422673Z", "start_time": "2026-09-08T00:49:25.402434Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.396659Z", + "iopub.status.busy": "2026-09-07T17:47:04.396599Z", + "iopub.status.idle": "2026-09-07T17:47:04.399906Z", + "shell.execute_reply": "2026-09-07T17:47:04.399545Z" } }, - "cell_type": "code", - "source": [ - "import inspect\n", - "\n", - "sig = inspect.Signature([\n", - " inspect.Parameter(\"x\", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),\n", - " inspect.Parameter(\"y\", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=4, annotation=int),\n", - "])\n", - "\n", - "rendered_with_annotations = fr.tools.flowrep2python(\n", - " unreferenced_recipe,\n", - " function_name=\"my_function_with_annotations\",\n", - " signature=sig,\n", - ")\n", - "print(rendered_with_annotations.source)" - ], - "id": "54362e50de84e98d", "outputs": [ { "name": "stdout", @@ -3354,31 +4571,73 @@ ] } ], - "execution_count": 60 + "source": [ + "import inspect\n", + "\n", + "sig = inspect.Signature([\n", + " inspect.Parameter(\"x\", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),\n", + " inspect.Parameter(\"y\", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=4, annotation=int),\n", + "])\n", + "\n", + "rendered_with_annotations = fr.tools.flowrep2python(\n", + " unreferenced_recipe,\n", + " function_name=\"my_function_with_annotations\",\n", + " signature=sig,\n", + ")\n", + "print(rendered_with_annotations.source)" + ] }, { + "cell_type": "markdown", + "id": "6c66d3483935a69e", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.914939Z", "start_time": "2026-07-11T03:59:27.900898Z" } }, - "cell_type": "markdown", "source": [ "### 6.3 Complex signatures\n", "\n", "Where the annotations and defaults have reprs that play nicely with `ast.literal_eval`, these signature items inline nicely into the source code. Where they are more complex, we use placeholders" - ], - "id": "6c66d3483935a69e" + ] }, { + "cell_type": "code", + "execution_count": 61, + "id": "281ef568be306e93", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.454244Z", "start_time": "2026-09-08T00:49:25.434370Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.400566Z", + "iopub.status.busy": "2026-09-07T17:47:04.400504Z", + "iopub.status.idle": "2026-09-07T17:47:04.403904Z", + "shell.execute_reply": "2026-09-07T17:47:04.403529Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "from __future__ import annotations\n", + "\n", + "import flowrep\n", + "import flowrep.std\n", + "import typing\n", + "\n", + "@flowrep.workflow(\"total\")\n", + "def wont_dump(x: _ann_x, y: _ann_y = _default_y):\n", + " add = flowrep.std.add(x, y)\n", + " return add\n", + "\n", + "\n" + ] + } + ], "source": [ "class MyCustomThing(int): ...\n", "\n", @@ -3402,55 +4661,39 @@ " signature=complex_sig,\n", ")\n", "print(complex_rendered.source)" - ], - "id": "281ef568be306e93", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "from __future__ import annotations\n", - "\n", - "import flowrep\n", - "import flowrep.std\n", - "import typing\n", - "\n", - "@flowrep.workflow(\"total\")\n", - "def wont_dump(x: _ann_x, y: _ann_y = _default_y):\n", - " add = flowrep.std.add(x, y)\n", - " return add\n", - "\n", - "\n" - ] - } - ], - "execution_count": 61 + ] }, { + "cell_type": "markdown", + "id": "966d2a06e2842a37", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.936649Z", "start_time": "2026-07-11T03:59:27.922782Z" } }, - "cell_type": "markdown", "source": [ "Such code won't dump without forcing the issue with a boolean flag.\n", "\n", "These placeholder symbols are stored in a namespace" - ], - "id": "966d2a06e2842a37" + ] }, { + "cell_type": "code", + "execution_count": 62, + "id": "730469f5b44b6dd8", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.497258Z", "start_time": "2026-09-08T00:49:25.466554Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.404540Z", + "iopub.status.busy": "2026-09-07T17:47:04.404479Z", + "iopub.status.idle": "2026-09-07T17:47:04.408069Z", + "shell.execute_reply": "2026-09-07T17:47:04.407717Z" } }, - "cell_type": "code", - "source": "complex_rendered.namespace", - "id": "730469f5b44b6dd8", "outputs": [ { "data": { @@ -3465,32 +4708,39 @@ "output_type": "execute_result" } ], - "execution_count": 62 + "source": [ + "complex_rendered.namespace" + ] }, { + "cell_type": "markdown", + "id": "938b1fe74c549980", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.953687Z", "start_time": "2026-07-11T03:59:27.937643Z" } }, - "cell_type": "markdown", - "source": "This allows us to build the function just fine, so only dumping the source code is negatively impacted by complex signature values", - "id": "938b1fe74c549980" + "source": [ + "This allows us to build the function just fine, so only dumping the source code is negatively impacted by complex signature values" + ] }, { + "cell_type": "code", + "execution_count": 63, + "id": "578b4f2ccc1ee02e", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.546887Z", "start_time": "2026-09-08T00:49:25.511639Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.408650Z", + "iopub.status.busy": "2026-09-07T17:47:04.408594Z", + "iopub.status.idle": "2026-09-07T17:47:04.411697Z", + "shell.execute_reply": "2026-09-07T17:47:04.411468Z" } }, - "cell_type": "code", - "source": [ - "rebuilt_complex = complex_rendered.build()\n", - "rebuilt(MyCustomThing(1), MyCustomThing(2))" - ], - "id": "578b4f2ccc1ee02e", "outputs": [ { "data": { @@ -3503,26 +4753,30 @@ "output_type": "execute_result" } ], - "execution_count": 63 + "source": [ + "rebuilt_complex = complex_rendered.build()\n", + "rebuilt(MyCustomThing(1), MyCustomThing(2))" + ] }, { + "cell_type": "markdown", + "id": "385466cdcaf0ec6e", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:27.967749Z", "start_time": "2026-07-11T03:59:27.954651Z" } }, - "cell_type": "markdown", "source": [ "The compiler also works on `DagData` retrospective objects, where the port fields are leveraged to build signature information.\n", "\n", "While most recipes will compile this way, there are a few places where the underlying `flowrep` recipe structure is more flexible than our python parser -- e.g., the parser demands that the conditions of an `if` clause be representable as a callable, but in the recipe format we would be free to put a `WhileRecipe` as the condition. The parser is designed to fail cleanly in these edge cases." - ], - "id": "385466cdcaf0ec6e" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "6da55d857f12b75e", + "metadata": {}, "source": [ "---\n", "## 7. Converting to Other Formats\n", @@ -3545,17 +4799,41 @@ " preserved — it becomes PWD's unnamed `sourcePort` and returns as `__result__`.\n", "\n", "### 7.1 flowrep → PWD" - ], - "id": "6da55d857f12b75e" + ] }, { + "cell_type": "code", + "execution_count": 64, + "id": "e05fec796a1c063b", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.598236Z", "start_time": "2026-09-08T00:49:25.549243Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.412493Z", + "iopub.status.busy": "2026-09-07T17:47:04.412436Z", + "iopub.status.idle": "2026-09-07T17:47:04.415846Z", + "shell.execute_reply": "2026-09-07T17:47:04.415488Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PWD version: 0.1.5\n", + "Nodes: 6\n", + " id=0, type=input, name=x\n", + " id=1, type=input, name=slope\n", + " id=2, type=input, name=intercept\n", + " id=3, type=output, name=result\n", + " id=4, type=function, value=__main__.multiply\n", + " id=5, type=function, value=__main__.add\n", + "Edges: 5\n" + ] + } + ], "source": [ "try:\n", "\n", @@ -3577,42 +4855,44 @@ "except ImportError:\n", " print(\"python-workflow-definition is not installed — skipping this section.\")\n", " print(\"Install with: pip install python-workflow-definition\")" - ], - "id": "e05fec796a1c063b", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "PWD version: 0.0.1\n", - "Nodes: 6\n", - " id=0, type=input, name=x\n", - " id=1, type=input, name=slope\n", - " id=2, type=input, name=intercept\n", - " id=3, type=output, name=result\n", - " id=4, type=function, value=__main__.multiply\n", - " id=5, type=function, value=__main__.add\n", - "Edges: 5\n" - ] - } - ], - "execution_count": 64 + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "### 7.2 PWD → flowrep", - "id": "c0dfca6f272c635d" + "id": "c0dfca6f272c635d", + "metadata": {}, + "source": [ + "### 7.2 PWD → flowrep" + ] }, { "cell_type": "code", + "execution_count": 65, "id": "5ccd8c05", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.618360Z", "start_time": "2026-09-08T00:49:25.600192Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.416472Z", + "iopub.status.busy": "2026-09-07T17:47:04.416412Z", + "iopub.status.idle": "2026-09-07T17:47:04.419865Z", + "shell.execute_reply": "2026-09-07T17:47:04.419595Z" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Round-tripped inputs: ['x', 'slope', 'intercept']\n", + "Round-tripped outputs: ['result']\n", + "Round-tripped nodes: ['multiply_0', 'add_0']\n", + "Defaults: {'x': 0.0, 'slope': 1.0, 'intercept': 0.0}\n" + ] + } + ], "source": [ "try:\n", " # Round-trip: flowrep → PWD → flowrep\n", @@ -3628,21 +4908,8 @@ " print(f\"Round-tripped nodes: {list(roundtripped.nodes.keys())}\")\n", " print(f\"Defaults: {defaults}\")\n", "except ImportError:\n", - " print(\"python-workflow-definition is not installed — skipping.\")" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Round-tripped inputs: ['x', 'slope', 'intercept']\n", - "Round-tripped outputs: ['result']\n", - "Round-tripped nodes: ['multiply_0', 'add_0']\n", - "Defaults: {'x': 0.0, 'slope': 1.0, 'intercept': 0.0}\n" - ] - } - ], - "execution_count": 65 + " print(\"python-workflow-definition is not installed — skipping.\")" + ] }, { "cell_type": "markdown", @@ -3680,28 +4947,36 @@ ] }, { + "cell_type": "markdown", + "id": "b4d773a54b386eba", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.011631Z", "start_time": "2026-07-11T03:59:27.997786Z" } }, - "cell_type": "markdown", "source": [ "To explore this, let's revisit our earlier example of scaling values in a for-loop.\n", "Here, we'll add default values (tracked only by a boolean \"are they available\" flag in the recipe) and annotations (not tracked at all in the recipe):" - ], - "id": "b4d773a54b386eba" + ] }, { "cell_type": "code", + "execution_count": 66, "id": "2370bd6b", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.647477Z", "start_time": "2026-09-08T00:49:25.631485Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.420550Z", + "iopub.status.busy": "2026-09-07T17:47:04.420492Z", + "iopub.status.idle": "2026-09-07T17:47:04.424988Z", + "shell.execute_reply": "2026-09-07T17:47:04.424633Z" } }, + "outputs": [], "source": [ "@fr.atomic\n", "def rescale(value: int | float, factor: float = 1.0) -> float:\n", @@ -3728,33 +5003,45 @@ " to_scale = int_list(n)\n", " scaled = rescale_all(to_scale, scale)\n", " return scaled" - ], - "outputs": [], - "execution_count": 66 + ] }, { + "cell_type": "code", + "execution_count": 67, + "id": "833e6b0ceb72e917", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.673022Z", "start_time": "2026-09-08T00:49:25.658428Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.425593Z", + "iopub.status.busy": "2026-09-07T17:47:04.425533Z", + "iopub.status.idle": "2026-09-07T17:47:04.427716Z", + "shell.execute_reply": "2026-09-07T17:47:04.427333Z" } }, - "cell_type": "code", - "source": "node_data = fr.tools.recipe2data(scaled_range.flowrep_recipe)", - "id": "833e6b0ceb72e917", "outputs": [], - "execution_count": 67 + "source": [ + "node_data = fr.tools.recipe2data(scaled_range.flowrep_recipe)" + ] }, { + "cell_type": "code", + "execution_count": 68, + "id": "d8537a11cde237e4", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.709555Z", "start_time": "2026-09-08T00:49:25.673728Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.428325Z", + "iopub.status.busy": "2026-09-07T17:47:04.428268Z", + "iopub.status.idle": "2026-09-07T17:47:04.430665Z", + "shell.execute_reply": "2026-09-07T17:47:04.430298Z" } }, - "cell_type": "code", - "source": "type(node_data.recipe)", - "id": "d8537a11cde237e4", "outputs": [ { "data": { @@ -3767,44 +5054,42 @@ "output_type": "execute_result" } ], - "execution_count": 68 + "source": [ + "type(node_data.recipe)" + ] }, { + "cell_type": "markdown", + "id": "7ca53a662f660901", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.018411Z", "start_time": "2026-07-11T03:59:28.013354Z" } }, - "cell_type": "markdown", "source": [ "### 8.1 Recursive construction\n", "\n", "The first thing to note is that the conversion is recursive.\n", "When converting a workflow to its data instance, the conversion happens to recipes the whole way through the graph and to all subgraphs." - ], - "id": "7ca53a662f660901" + ] }, { + "cell_type": "code", + "execution_count": 69, + "id": "f044f5954e57507c", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.744949Z", "start_time": "2026-09-08T00:49:25.711398Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.431367Z", + "iopub.status.busy": "2026-09-07T17:47:04.431307Z", + "iopub.status.idle": "2026-09-07T17:47:04.434935Z", + "shell.execute_reply": "2026-09-07T17:47:04.434659Z" } }, - "cell_type": "code", - "source": [ - "def recursively_look_at_data_nodes(node: fr.schemas.NodeData, tabs=0):\n", - " for recipe_child_label in node.recipe.nodes:\n", - " live_child = node.nodes[recipe_child_label]\n", - " assert(isinstance(live_child, fr.schemas.NodeData))\n", - " print(f\"{tabs*'\\t'}{recipe_child_label}\")\n", - " if hasattr(live_child.recipe, \"nodes\"):\n", - " recursively_look_at_data_nodes(live_child, tabs+1)\n", - "\n", - "recursively_look_at_data_nodes(node_data)" - ], - "id": "f044f5954e57507c", "outputs": [ { "name": "stdout", @@ -3816,38 +5101,56 @@ ] } ], - "execution_count": 69 + "source": [ + "def recursively_look_at_data_nodes(node: fr.schemas.NodeData, tabs=0):\n", + " for recipe_child_label in node.recipe.nodes:\n", + " live_child = node.nodes[recipe_child_label]\n", + " assert(isinstance(live_child, fr.schemas.NodeData))\n", + " print(f\"{tabs*'\\t'}{recipe_child_label}\")\n", + " if hasattr(live_child.recipe, \"nodes\"):\n", + " recursively_look_at_data_nodes(live_child, tabs+1)\n", + "\n", + "recursively_look_at_data_nodes(node_data)" + ] }, { + "cell_type": "markdown", + "id": "a1bd687271aa5721", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.034478Z", "start_time": "2026-07-11T03:59:28.022277Z" } }, - "cell_type": "markdown", - "source": "### 8.2 IO ports", - "id": "a1bd687271aa5721" + "source": [ + "### 8.2 IO ports" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "1ec4a0ac252961ef", + "metadata": {}, "source": [ "For each data node, we can look at its input and outputs.\n", "To start with, since they represent an un-run recipe all their actual values will be a special \"not data\" object:" - ], - "id": "1ec4a0ac252961ef" + ] }, { + "cell_type": "code", + "execution_count": 70, + "id": "aec301f79141f523", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.765309Z", "start_time": "2026-09-08T00:49:25.746891Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.435651Z", + "iopub.status.busy": "2026-09-07T17:47:04.435594Z", + "iopub.status.idle": "2026-09-07T17:47:04.438897Z", + "shell.execute_reply": "2026-09-07T17:47:04.438623Z" } }, - "cell_type": "code", - "source": "node_data.input_ports[\"n\"].value", - "id": "aec301f79141f523", "outputs": [ { "data": { @@ -3860,24 +5163,34 @@ "output_type": "execute_result" } ], - "execution_count": 70 + "source": [ + "node_data.input_ports[\"n\"].value" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "But default values are parsed for input ports (where available), and hold their actual python values:", - "id": "dfe8a73f0f059be7" + "id": "dfe8a73f0f059be7", + "metadata": {}, + "source": [ + "But default values are parsed for input ports (where available), and hold their actual python values:" + ] }, { + "cell_type": "code", + "execution_count": 71, + "id": "c9e8560bc82a6038", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.807722Z", "start_time": "2026-09-08T00:49:25.777628Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.439550Z", + "iopub.status.busy": "2026-09-07T17:47:04.439493Z", + "iopub.status.idle": "2026-09-07T17:47:04.441381Z", + "shell.execute_reply": "2026-09-07T17:47:04.441135Z" } }, - "cell_type": "code", - "source": "node_data.input_ports[\"scale\"].default", - "id": "c9e8560bc82a6038", "outputs": [ { "data": { @@ -3890,34 +5203,39 @@ "output_type": "execute_result" } ], - "execution_count": 71 + "source": [ + "node_data.input_ports[\"scale\"].default" + ] }, { + "cell_type": "markdown", + "id": "c6545f09fe38cc1b", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.066698Z", "start_time": "2026-07-11T03:59:28.054289Z" } }, - "cell_type": "markdown", - "source": "The same is true of IO annotations:", - "id": "c6545f09fe38cc1b" + "source": [ + "The same is true of IO annotations:" + ] }, { + "cell_type": "code", + "execution_count": 72, + "id": "6d43e80546840ed0", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.838530Z", "start_time": "2026-09-08T00:49:25.823047Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.442098Z", + "iopub.status.busy": "2026-09-07T17:47:04.442038Z", + "iopub.status.idle": "2026-09-07T17:47:04.444716Z", + "shell.execute_reply": "2026-09-07T17:47:04.444356Z" } }, - "cell_type": "code", - "source": [ - "(\n", - " node_data.input_ports[\"scale\"].annotation,\n", - " node_data.output_ports[\"scaled\"].annotation\n", - ")" - ], - "id": "6d43e80546840ed0", "outputs": [ { "data": { @@ -3930,37 +5248,42 @@ "output_type": "execute_result" } ], - "execution_count": 72 + "source": [ + "(\n", + " node_data.input_ports[\"scale\"].annotation,\n", + " node_data.output_ports[\"scaled\"].annotation\n", + ")" + ] }, { + "cell_type": "markdown", + "id": "d9dd1ddaf30c4f90", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.083961Z", "start_time": "2026-07-11T03:59:28.070939Z" } }, - "cell_type": "markdown", - "source": "And of course these are available recusively on the \"live\" children", - "id": "d9dd1ddaf30c4f90" + "source": [ + "And of course these are available recusively on the \"live\" children" + ] }, { + "cell_type": "code", + "execution_count": 73, + "id": "c0c1dc9c6ca8e156", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.867642Z", "start_time": "2026-09-08T00:49:25.849593Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.445338Z", + "iopub.status.busy": "2026-09-07T17:47:04.445278Z", + "iopub.status.idle": "2026-09-07T17:47:04.449045Z", + "shell.execute_reply": "2026-09-07T17:47:04.448695Z" } }, - "cell_type": "code", - "source": [ - "(\n", - " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].value,\n", - " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].default,\n", - " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].annotation,\n", - " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].value,\n", - " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].annotation,\n", - ")" - ], - "id": "c0c1dc9c6ca8e156", "outputs": [ { "data": { @@ -3973,37 +5296,48 @@ "output_type": "execute_result" } ], - "execution_count": 73 + "source": [ + "(\n", + " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].value,\n", + " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].default,\n", + " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].annotation,\n", + " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].value,\n", + " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].annotation,\n", + ")" + ] }, { + "cell_type": "markdown", + "id": "c1ade97f5e96b4ba", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.098845Z", "start_time": "2026-07-11T03:59:28.084866Z" } }, - "cell_type": "markdown", "source": [ "### 8.3 Edges\n", "\n", "Just as the recipes subgraphs are recursively instantiated into data nodes, we also provide copies of the edges to represent instance-view edges between our instance-view nodes.\n", "Since the edge data format is already quite simple, we don't need any new data types here and can use our pydantic edge models directly, including their nice succinct printing format:" - ], - "id": "c1ade97f5e96b4ba" + ] }, { + "cell_type": "code", + "execution_count": 74, + "id": "29bb5cf64f1b2ea2", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.898443Z", "start_time": "2026-09-08T00:49:25.879124Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.449665Z", + "iopub.status.busy": "2026-09-07T17:47:04.449605Z", + "iopub.status.idle": "2026-09-07T17:47:04.453045Z", + "shell.execute_reply": "2026-09-07T17:47:04.452678Z" } }, - "cell_type": "code", - "source": [ - "for target, source in node_data.input_edges.items():\n", - " print(f\"{source.serialize()} → {target.serialize()}\")" - ], - "id": "29bb5cf64f1b2ea2", "outputs": [ { "name": "stdout", @@ -4014,21 +5348,27 @@ ] } ], - "execution_count": 74 + "source": [ + "for target, source in node_data.input_edges.items():\n", + " print(f\"{source.serialize()} → {target.serialize()}\")" + ] }, { + "cell_type": "code", + "execution_count": 75, + "id": "1468f7bd033bb028", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.926976Z", "start_time": "2026-09-08T00:49:25.910261Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.453670Z", + "iopub.status.busy": "2026-09-07T17:47:04.453610Z", + "iopub.status.idle": "2026-09-07T17:47:04.456965Z", + "shell.execute_reply": "2026-09-07T17:47:04.456630Z" } }, - "cell_type": "code", - "source": [ - "for target, source in node_data.edges.items():\n", - " print(f\"{source.serialize()} → {target.serialize()}\")" - ], - "id": "1468f7bd033bb028", "outputs": [ { "name": "stdout", @@ -4038,21 +5378,27 @@ ] } ], - "execution_count": 75 + "source": [ + "for target, source in node_data.edges.items():\n", + " print(f\"{source.serialize()} → {target.serialize()}\")" + ] }, { + "cell_type": "code", + "execution_count": 76, + "id": "df3500015b3a2f5a", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.946146Z", "start_time": "2026-09-08T00:49:25.927642Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.457621Z", + "iopub.status.busy": "2026-09-07T17:47:04.457556Z", + "iopub.status.idle": "2026-09-07T17:47:04.461086Z", + "shell.execute_reply": "2026-09-07T17:47:04.460725Z" } }, - "cell_type": "code", - "source": [ - "for target, source in node_data.output_edges.items():\n", - " print(f\"{source.serialize()} → {target.serialize()}\")" - ], - "id": "df3500015b3a2f5a", "outputs": [ { "name": "stdout", @@ -4062,16 +5408,20 @@ ] } ], - "execution_count": 76 + "source": [ + "for target, source in node_data.output_edges.items():\n", + " print(f\"{source.serialize()} → {target.serialize()}\")" + ] }, { + "cell_type": "markdown", + "id": "8016d71ec8195853", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.197700Z", "start_time": "2026-07-11T03:59:28.116392Z" } }, - "cell_type": "markdown", "source": [ "### 8.4 Flow controls\n", "\n", @@ -4081,29 +5431,24 @@ "The main difference between workflow data and a flow control data node is that the workflow can initialize to a non-empty graph, while for all the flow controls it is the responsibility of the WfMS to follow the recipe and populate the DAG at runtime.\n", "\n", "In our example, we can see that the deeply-nested for-loop node has no subgraph info of its own yet:" - ], - "id": "8016d71ec8195853" + ] }, { + "cell_type": "code", + "execution_count": 77, + "id": "8003b380315257cd", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:25.977517Z", "start_time": "2026-09-08T00:49:25.961257Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.461719Z", + "iopub.status.busy": "2026-09-07T17:47:04.461657Z", + "iopub.status.idle": "2026-09-07T17:47:04.464870Z", + "shell.execute_reply": "2026-09-07T17:47:04.464553Z" } }, - "cell_type": "code", - "source": [ - "for_data = node_data.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", - "\n", - "(\n", - " for_data.recipe.type,\n", - " for_data.nodes,\n", - " for_data.input_edges,\n", - " for_data.edges,\n", - " for_data.output_edges,\n", - ")" - ], - "id": "8003b380315257cd", "outputs": [ { "data": { @@ -4116,33 +5461,52 @@ "output_type": "execute_result" } ], - "execution_count": 77 + "source": [ + "for_data = node_data.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", + "\n", + "(\n", + " for_data.recipe.type,\n", + " for_data.nodes,\n", + " for_data.input_edges,\n", + " for_data.edges,\n", + " for_data.output_edges,\n", + ")" + ] }, { + "cell_type": "markdown", + "id": "56f385796c984111", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.234494Z", "start_time": "2026-07-11T03:59:28.220696Z" } }, - "cell_type": "markdown", "source": [ "### 8.5 Aside on annotations\n", "\n", "The retrospective data objects have space to hold type hints and annotations, which, as with data values, they do so _as real Python objects_.\n", "\n", "It's customary in python to sometimes hide annotations behind a fake import, which never actually executes at runtime:" - ], - "id": "56f385796c984111" + ] }, { + "cell_type": "code", + "execution_count": 78, + "id": "cc1e5c958dd87720", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.005125Z", "start_time": "2026-09-08T00:49:25.991627Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.465499Z", + "iopub.status.busy": "2026-09-07T17:47:04.465437Z", + "iopub.status.idle": "2026-09-07T17:47:04.469033Z", + "shell.execute_reply": "2026-09-07T17:47:04.468753Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "from __future__ import annotations\n", "\n", @@ -4154,37 +5518,37 @@ "@fr.atomic\n", "def get_blue(colors: SeabornColors) -> str:\n", " return colors.blue" - ], - "id": "cc1e5c958dd87720", - "outputs": [], - "execution_count": 78 + ] }, { + "cell_type": "markdown", + "id": "80942d3b934ef105", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.248350Z", "start_time": "2026-07-11T03:59:28.235562Z" } }, - "cell_type": "markdown", - "source": "This is completely fine at a recipe level, but if we try to convert the recipe into data we won't be able to find `SeabornColors` to populate the hint field!", - "id": "80942d3b934ef105" + "source": [ + "This is completely fine at a recipe level, but if we try to convert the recipe into data we won't be able to find `SeabornColors` to populate the hint field!" + ] }, { + "cell_type": "code", + "execution_count": 79, + "id": "5dd2fc6e39e728b4", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.044462Z", "start_time": "2026-09-08T00:49:26.016285Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.469790Z", + "iopub.status.busy": "2026-09-07T17:47:04.469729Z", + "iopub.status.idle": "2026-09-07T17:47:04.473038Z", + "shell.execute_reply": "2026-09-07T17:47:04.472631Z" } }, - "cell_type": "code", - "source": [ - "try:\n", - " fr.tools.recipe2data(get_blue.flowrep_recipe)\n", - "except NameError as e:\n", - " print(e)" - ], - "id": "5dd2fc6e39e728b4", "outputs": [ { "name": "stdout", @@ -4194,35 +5558,42 @@ ] } ], - "execution_count": 79 + "source": [ + "try:\n", + " fr.tools.recipe2data(get_blue.flowrep_recipe)\n", + "except NameError as e:\n", + " print(e)" + ] }, { + "cell_type": "markdown", + "id": "ca0292eb59cf963a", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.256186Z", "start_time": "2026-07-11T03:59:28.249195Z" } }, - "cell_type": "markdown", - "source": "To solve this, prior to data-fying your recipes, you need to locally import the relevant types. We try to make the error message above as helpful as possible, but you may need to go read the source scope where the underlying function was defined and decorated to learn exactly what import to use.", - "id": "ca0292eb59cf963a" + "source": [ + "To solve this, prior to data-fying your recipes, you need to locally import the relevant types. We try to make the error message above as helpful as possible, but you may need to go read the source scope where the underlying function was defined and decorated to learn exactly what import to use." + ] }, { + "cell_type": "code", + "execution_count": 80, + "id": "36045479099ade66", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.087170Z", - "start_time": "2026-09-08T00:49:26.046374Z" - } - }, - "cell_type": "code", - "source": [ - "from pyiron_snippets.colors import SeabornColors\n", - "\n", - "data = fr.tools.recipe2data(get_blue.flowrep_recipe)\n", - "\n", - "data.input_ports[\"colors\"].annotation" - ], - "id": "36045479099ade66", + "start_time": "2026-09-08T00:49:26.046374Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.473699Z", + "iopub.status.busy": "2026-09-07T17:47:04.473637Z", + "iopub.status.idle": "2026-09-07T17:47:04.476602Z", + "shell.execute_reply": "2026-09-07T17:47:04.476290Z" + } + }, "outputs": [ { "data": { @@ -4235,16 +5606,23 @@ "output_type": "execute_result" } ], - "execution_count": 80 + "source": [ + "from pyiron_snippets.colors import SeabornColors\n", + "\n", + "data = fr.tools.recipe2data(get_blue.flowrep_recipe)\n", + "\n", + "data.input_ports[\"colors\"].annotation" + ] }, { + "cell_type": "markdown", + "id": "44fa91cf59e164c7", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.270600Z", "start_time": "2026-07-11T03:59:28.256991Z" } }, - "cell_type": "markdown", "source": [ "### 8.6 A Toy WfMS\n", "\n", @@ -4253,46 +5631,59 @@ "\n", "Since all our recipe ports must be named, the engine is accessed by a simple runner function that demands all input data be provided with keyword arguments.\n", "Default values are exploited where available, as demonstrated by the fact we will omit a `scale=...` argument to our call:" - ], - "id": "44fa91cf59e164c7" + ] }, { + "cell_type": "code", + "execution_count": 81, + "id": "8fc856912a06a735", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.109193Z", "start_time": "2026-09-08T00:49:26.090219Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.477268Z", + "iopub.status.busy": "2026-09-07T17:47:04.477205Z", + "iopub.status.idle": "2026-09-07T17:47:04.481014Z", + "shell.execute_reply": "2026-09-07T17:47:04.480670Z" } }, - "cell_type": "code", - "source": "ran_node = fr.tools.run_recipe(scaled_range.flowrep_recipe, n=3)", - "id": "8fc856912a06a735", "outputs": [], - "execution_count": 81 + "source": [ + "ran_node = fr.tools.run_recipe(scaled_range.flowrep_recipe, n=3)" + ] }, { + "cell_type": "markdown", + "id": "1fc5221b5f62a0da", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.285169Z", "start_time": "2026-07-11T03:59:28.271440Z" } }, - "cell_type": "markdown", "source": [ "Post-facto, the values of each IO port are fully populated.\n", "We can look at the results of the workflow:" - ], - "id": "1fc5221b5f62a0da" + ] }, { + "cell_type": "code", + "execution_count": 82, + "id": "47a7f46ae249d2f6", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.126195Z", "start_time": "2026-09-08T00:49:26.110203Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.481675Z", + "iopub.status.busy": "2026-09-07T17:47:04.481610Z", + "iopub.status.idle": "2026-09-07T17:47:04.485011Z", + "shell.execute_reply": "2026-09-07T17:47:04.484630Z" } }, - "cell_type": "code", - "source": "ran_node.output_ports[\"scaled\"].value", - "id": "47a7f46ae249d2f6", "outputs": [ { "data": { @@ -4305,80 +5696,295 @@ "output_type": "execute_result" } ], - "execution_count": 82 + "source": [ + "ran_node.output_ports[\"scaled\"].value" + ] }, { + "cell_type": "markdown", + "id": "54c1c113638190b8", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.290343Z", "start_time": "2026-07-11T03:59:28.286066Z" } }, - "cell_type": "markdown", - "source": "And/or use the graph-structure of the workflow to investigate the data provenance of each step, even deep inside a subgraph:", - "id": "54c1c113638190b8" + "source": [ + "And/or use the graph-structure of the workflow to investigate the data provenance of each step, even deep inside a subgraph:" + ] }, { + "cell_type": "code", + "execution_count": 83, + "id": "72982d6646db5e03", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.142502Z", "start_time": "2026-09-08T00:49:26.127102Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.485676Z", + "iopub.status.busy": "2026-09-07T17:47:04.485614Z", + "iopub.status.idle": "2026-09-07T17:47:04.488771Z", + "shell.execute_reply": "2026-09-07T17:47:04.488491Z" } }, - "cell_type": "code", - "source": [ - "ran_for = ran_node.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", - "\n", - "for body_label, body_node in ran_for.nodes.items():\n", - " print(\n", - " f\"For-loop child {body_label} transformed \"\n", - " f\"{body_node.input_ports['item'].value}*{body_node.input_ports['factor'].value} \"\n", - " f\"→ {body_node.output_ports['scaled'].value}\"\n", - " )" - ], - "id": "72982d6646db5e03", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "All recorded nodes: ['scatter_items', 'body_0', 'body_1', 'body_2', 'aggregate_results']\n", "For-loop child body_0 transformed 0*1.0 → 0.0\n", "For-loop child body_1 transformed 1*1.0 → 1.0\n", "For-loop child body_2 transformed 2*1.0 → 2.0\n" ] } ], - "execution_count": 83 + "source": [ + "ran_for = ran_node.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", + "\n", + "print(f\"All recorded nodes: {list(ran_for.nodes)}\")\n", + "\n", + "for body_label, body_node in ran_for.nodes.items():\n", + " if \"item\" not in body_node.input_ports:\n", + " continue # the scatter/aggregate nodes recording fan-out and fan-in\n", + " print(\n", + " f\"For-loop child {body_label} transformed \"\n", + " f\"{body_node.input_ports['item'].value}*{body_node.input_ports['factor'].value} \"\n", + " f\"→ {body_node.output_ports['scaled'].value}\"\n", + " )" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "7e7886e38e56cf6f", + "metadata": {}, "source": [ "Retrospective data draws too, with the same method. Where printing shows the values,\n", - "drawing shows the shape: the for-node has collapsed from one representative `body_n`\n", - "prospectively to one concrete node per iteration retrospectively." - ], - "id": "7e7886e38e56cf6f" + "drawing shows the shape: a for-loop fans one input collection out across its iterations\n", + "and gathers the results back together, and the recorded graph now makes both halves of\n", + "that explicit as `scatter_*` and `aggregate_*` nodes. Every edge into or out of them still\n", + "connects exactly one port to exactly one port, so the record can say precisely which input\n", + "element produced which output element, instead of collapsing the whole collection into a\n", + "single N-to-1 relationship." + ] }, { "cell_type": "code", + "execution_count": 84, "id": "838c4719f577a6a3", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.305789Z", "start_time": "2026-09-08T00:49:26.143499Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.489459Z", + "iopub.status.busy": "2026-09-07T17:47:04.489394Z", + "iopub.status.idle": "2026-09-07T17:47:04.501821Z", + "shell.execute_reply": "2026-09-07T17:47:04.501446Z" } }, - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "ran_for.draw()" - ], "outputs": [ { "data": { - "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nfor_each\n(no recorded edges)\n\n\n\ninputs.factor\n\nfactor\n\n\n\n\ninputs.items\n\nitems\n\n\n\n\noutputs.results\n\nresults\n\n\n\n\nbody_0\n\nbody_0\n\n\nitem\n\n\nscaled\n\n\nfactor\n\n\n\n\nbody_1\n\nbody_1\n\n\nitem\n\n\nscaled\n\n\nfactor\n\n\n\n\nbody_2\n\nbody_2\n\n\nitem\n\n\nscaled\n\n\nfactor\n\n\n\n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster_\n", + "\n", + "for_each\n", + "\n", + "\n", + "\n", + "inputs.factor\n", + "\n", + "factor\n", + "\n", + "\n", + "\n", + "\n", + "body_0\n", + "\n", + "body_0\n", + "\n", + "\n", + "item\n", + "\n", + "\n", + "scaled\n", + "\n", + "\n", + "factor\n", + "\n", + "\n", + "\n", + "\n", + "inputs.factor:e->body_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body_1\n", + "\n", + "body_1\n", + "\n", + "\n", + "item\n", + "\n", + "\n", + "scaled\n", + "\n", + "\n", + "factor\n", + "\n", + "\n", + "\n", + "\n", + "inputs.factor:e->body_1:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body_2\n", + "\n", + "body_2\n", + "\n", + "\n", + "item\n", + "\n", + "\n", + "scaled\n", + "\n", + "\n", + "factor\n", + "\n", + "\n", + "\n", + "\n", + "inputs.factor:e->body_2:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "inputs.items\n", + "\n", + "items\n", + "\n", + "\n", + "\n", + "\n", + "scatter_items\n", + "\n", + "scatter_items\n", + "…orm1toN.iterable_to_outputs\n", + "\n", + "\n", + "items\n", + "\n", + "\n", + "output_0\n", + "\n", + "\n", + "output_1\n", + "\n", + "\n", + "output_2\n", + "\n", + "\n", + "\n", + "\n", + "inputs.items:e->scatter_items:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "outputs.results\n", + "\n", + "results\n", + "\n", + "\n", + "\n", + "\n", + "scatter_items:e->body_0:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "scatter_items:e->body_1:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "scatter_items:e->body_2:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "aggregate_results\n", + "\n", + "aggregate_results\n", + "…ransformNto1.inputs_to_list\n", + "\n", + "\n", + "item_0\n", + "\n", + "\n", + "output_0\n", + "\n", + "\n", + "item_1\n", + "\n", + "\n", + "item_2\n", + "\n", + "\n", + "\n", + "\n", + "body_0:e->aggregate_results:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body_1:e->aggregate_results:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body_2:e->aggregate_results:w\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "aggregate_results:e->outputs.results:w\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], "text/plain": [ - "" + "" ] }, "execution_count": 84, @@ -4386,16 +5992,20 @@ "output_type": "execute_result" } ], - "execution_count": 84 + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "ran_for.draw()" + ] }, { + "cell_type": "markdown", + "id": "65a9e2c8660174e", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.305512Z", "start_time": "2026-07-11T03:59:28.291868Z" } }, - "cell_type": "markdown", "source": [ "### 8.7 Convenient storage and retrieval\n", "\n", @@ -4403,17 +6013,36 @@ "In this section, we'll review how you can save completed workflows to file with `bagofholding`, and then demonstrate a `flowrep` convenience tool for reviewing and reloading your results.\n", "\n", "Let's start by running a simple for-loop -- something easy to understand, but with non-trivial structure:" - ], - "id": "65a9e2c8660174e" + ] }, { + "cell_type": "code", + "execution_count": 85, + "id": "1264d43293ffbd43", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.327352Z", "start_time": "2026-09-08T00:49:26.310319Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.502531Z", + "iopub.status.busy": "2026-09-07T17:47:04.502463Z", + "iopub.status.idle": "2026-09-07T17:47:04.507520Z", + "shell.execute_reply": "2026-09-07T17:47:04.507162Z" } }, - "cell_type": "code", + "outputs": [ + { + "data": { + "text/plain": [ + "{'scaled': OutputDataPort(value=[0.0, 1.0, 2.0, 3.0, 4.0], annotation=list[float])}" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@fr.atomic\n", "def rescale(value: int | float, factor: float = 1.0) -> float:\n", @@ -4443,58 +6072,53 @@ "\n", "executed = fr.tools.run_recipe(scaled_range.flowrep_recipe, n=5)\n", "executed.output_ports" - ], - "id": "1264d43293ffbd43", - "outputs": [ - { - "data": { - "text/plain": [ - "{'scaled': OutputDataPort(value=[0.0, 1.0, 2.0, 3.0, 4.0], annotation=list[float])}" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 85 + ] }, { + "cell_type": "markdown", + "id": "df5a0ed5a28fd5cf", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.318869Z", "start_time": "2026-07-11T03:59:28.306752Z" } }, - "cell_type": "markdown", - "source": "Saving with `bagofholding` is straightforward:", - "id": "df5a0ed5a28fd5cf" + "source": [ + "Saving with `bagofholding` is straightforward:" + ] }, { + "cell_type": "code", + "execution_count": 86, + "id": "8d092cdcf9c0e517", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.677341Z", "start_time": "2026-09-08T00:49:26.340152Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.508185Z", + "iopub.status.busy": "2026-09-07T17:47:04.508118Z", + "iopub.status.idle": "2026-09-07T17:47:04.916081Z", + "shell.execute_reply": "2026-09-07T17:47:04.915679Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "import bagofholding as boh\n", "\n", "boh.H5Bag.save(executed, \"executed.h5\", overwrite_existing=True)" - ], - "id": "8d092cdcf9c0e517", - "outputs": [], - "execution_count": 86 + ] }, { + "cell_type": "markdown", + "id": "f94e046ee2730c68", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.334711Z", "start_time": "2026-07-11T03:59:28.319711Z" } }, - "cell_type": "markdown", "source": [ "One of the benefits of `bagofholding`'s structure is that results are browseable and partially-loadable -- i.e. we don't need to reload the _entire_ python object from disk, rather we can reload just a subset, like a particular subgraph or particular output value.\n", "However, the HDF5 structure captures the entire object, and while it is systematic it is verbose.\n", @@ -4502,22 +6126,24 @@ "Enter, `fr.tools.LexicalBagBrowser`, a `flowrep` tool that lets us parse bagged live workflows using their **Lexical path** -- i.e. a string of node labels (to traverse subgraphs), then \"inputs\" or \"outputs\", and finally a port label.\n", "\n", "For instance, we can browse the entire lexical structure of our stored workflow like this:" - ], - "id": "f94e046ee2730c68" + ] }, { + "cell_type": "code", + "execution_count": 87, + "id": "caeb3c190f954f71", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:26.713353Z", "start_time": "2026-09-08T00:49:26.683278Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.917952Z", + "iopub.status.busy": "2026-09-07T17:47:04.917844Z", + "iopub.status.idle": "2026-09-07T17:47:04.935777Z", + "shell.execute_reply": "2026-09-07T17:47:04.935439Z" } }, - "cell_type": "code", - "source": [ - "browser = fr.tools.LexicalBagBrowser(\"executed.h5\")\n", - "browser.list_paths()" - ], - "id": "caeb3c190f954f71", "outputs": [ { "data": { @@ -4536,6 +6162,13 @@ " 'rescale_all_0.for_each_0.inputs.factor',\n", " 'rescale_all_0.for_each_0.inputs.items',\n", " 'rescale_all_0.for_each_0.outputs.results',\n", + " 'rescale_all_0.for_each_0.aggregate_results',\n", + " 'rescale_all_0.for_each_0.aggregate_results.inputs.item_0',\n", + " 'rescale_all_0.for_each_0.aggregate_results.inputs.item_1',\n", + " 'rescale_all_0.for_each_0.aggregate_results.inputs.item_2',\n", + " 'rescale_all_0.for_each_0.aggregate_results.inputs.item_3',\n", + " 'rescale_all_0.for_each_0.aggregate_results.inputs.item_4',\n", + " 'rescale_all_0.for_each_0.aggregate_results.outputs.output_0',\n", " 'rescale_all_0.for_each_0.body_0',\n", " 'rescale_all_0.for_each_0.body_0.inputs.factor',\n", " 'rescale_all_0.for_each_0.body_0.inputs.item',\n", @@ -4575,7 +6208,14 @@ " 'rescale_all_0.for_each_0.body_4.rescale_0',\n", " 'rescale_all_0.for_each_0.body_4.rescale_0.inputs.factor',\n", " 'rescale_all_0.for_each_0.body_4.rescale_0.inputs.value',\n", - " 'rescale_all_0.for_each_0.body_4.rescale_0.outputs.result']" + " 'rescale_all_0.for_each_0.body_4.rescale_0.outputs.result',\n", + " 'rescale_all_0.for_each_0.scatter_items',\n", + " 'rescale_all_0.for_each_0.scatter_items.inputs.items',\n", + " 'rescale_all_0.for_each_0.scatter_items.outputs.output_0',\n", + " 'rescale_all_0.for_each_0.scatter_items.outputs.output_1',\n", + " 'rescale_all_0.for_each_0.scatter_items.outputs.output_2',\n", + " 'rescale_all_0.for_each_0.scatter_items.outputs.output_3',\n", + " 'rescale_all_0.for_each_0.scatter_items.outputs.output_4']" ] }, "execution_count": 87, @@ -4583,32 +6223,40 @@ "output_type": "execute_result" } ], - "execution_count": 87 + "source": [ + "browser = fr.tools.LexicalBagBrowser(\"executed.h5\")\n", + "browser.list_paths()" + ] }, { + "cell_type": "markdown", + "id": "c4fe9644c6e43a16", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.698253Z", "start_time": "2026-07-11T03:59:28.335655Z" } }, - "cell_type": "markdown", - "source": "We can then re-instantiate from file only a particular node", - "id": "c4fe9644c6e43a16" + "source": [ + "We can then re-instantiate from file only a particular node" + ] }, { + "cell_type": "code", + "execution_count": 88, + "id": "412ddc4424bcefd3", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:27.019082Z", "start_time": "2026-09-08T00:49:26.725883Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.936667Z", + "iopub.status.busy": "2026-09-07T17:47:04.936594Z", + "iopub.status.idle": "2026-09-07T17:47:05.282848Z", + "shell.execute_reply": "2026-09-07T17:47:05.282467Z" } }, - "cell_type": "code", - "source": [ - "reloaded_node = browser.load(\"rescale_all_0\")\n", - "isinstance(reloaded_node, fr.schemas.DagData), reloaded_node.nodes.keys()" - ], - "id": "412ddc4424bcefd3", "outputs": [ { "data": { @@ -4621,32 +6269,40 @@ "output_type": "execute_result" } ], - "execution_count": 88 + "source": [ + "reloaded_node = browser.load(\"rescale_all_0\")\n", + "isinstance(reloaded_node, fr.schemas.DagData), reloaded_node.nodes.keys()" + ] }, { + "cell_type": "markdown", + "id": "4433e29b74f02efe", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:28.726974Z", "start_time": "2026-07-11T03:59:28.703632Z" } }, - "cell_type": "markdown", - "source": "Or a particular port", - "id": "4433e29b74f02efe" + "source": [ + "Or a particular port" + ] }, { + "cell_type": "code", + "execution_count": 89, + "id": "d948155042459cb", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:27.038801Z", "start_time": "2026-09-08T00:49:27.023414Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:05.283568Z", + "iopub.status.busy": "2026-09-07T17:47:05.283501Z", + "iopub.status.idle": "2026-09-07T17:47:05.290753Z", + "shell.execute_reply": "2026-09-07T17:47:05.290317Z" } }, - "cell_type": "code", - "source": [ - "reloaded_port = browser.load(\"rescale_all_0.for_each_0.body_1.inputs.item\")\n", - "isinstance(reloaded_port, fr.schemas.InputDataPort), reloaded_port.value" - ], - "id": "d948155042459cb", "outputs": [ { "data": { @@ -4659,37 +6315,41 @@ "output_type": "execute_result" } ], - "execution_count": 89 + "source": [ + "reloaded_port = browser.load(\"rescale_all_0.for_each_0.body_1.inputs.item\")\n", + "isinstance(reloaded_port, fr.schemas.InputDataPort), reloaded_port.value" + ] }, { + "cell_type": "markdown", + "id": "c3622954ff23bc6f", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:29.067527Z", "start_time": "2026-07-11T03:59:28.727957Z" } }, - "cell_type": "markdown", "source": [ "The input and output port collections are stopping points in a plausible lexical path, but for the sake of return-type simplicity, the browser will _only_ load ports and nodes.\n", "If you try to reload one of the IO collections, you get a clean error:" - ], - "id": "c3622954ff23bc6f" + ] }, { + "cell_type": "code", + "execution_count": 90, + "id": "e0e550efad910586", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:27.081959Z", "start_time": "2026-09-08T00:49:27.049913Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:05.291422Z", + "iopub.status.busy": "2026-09-07T17:47:05.291360Z", + "iopub.status.idle": "2026-09-07T17:47:05.301903Z", + "shell.execute_reply": "2026-09-07T17:47:05.301571Z" } }, - "cell_type": "code", - "source": [ - "try:\n", - " browser.load(\"rescale_all_0.inputs\")\n", - "except ValueError as e:\n", - " print(e)" - ], - "id": "e0e550efad910586", "outputs": [ { "name": "stdout", @@ -4699,76 +6359,94 @@ ] } ], - "execution_count": 90 + "source": [ + "try:\n", + " browser.load(\"rescale_all_0.inputs\")\n", + "except ValueError as e:\n", + " print(e)" + ] }, { + "cell_type": "markdown", + "id": "51463f7d0fa254be", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:29.095206Z", "start_time": "2026-07-11T03:59:29.075064Z" } }, - "cell_type": "markdown", - "source": "In a jupyter environment like this, there is also a browser widget to view the tree structure of the graph:", - "id": "51463f7d0fa254be" + "source": [ + "In a jupyter environment like this, there is also a browser widget to view the tree structure of the graph:" + ] }, { + "cell_type": "code", + "execution_count": 91, + "id": "1b10ad697f945ac5", "metadata": { "ExecuteTime": { "end_time": "2026-09-08T00:49:27.134241Z", "start_time": "2026-09-08T00:49:27.083861Z" + }, + "execution": { + "iopub.execute_input": "2026-09-07T17:47:05.302578Z", + "iopub.status.busy": "2026-09-07T17:47:05.302514Z", + "iopub.status.idle": "2026-09-07T17:47:05.311803Z", + "shell.execute_reply": "2026-09-07T17:47:05.311417Z" } }, - "cell_type": "code", - "source": [ - "widget = browser.browse()\n", - "widget" - ], - "id": "1b10ad697f945ac5", "outputs": [ { "data": { - "text/plain": [ - "LexicalBagTree(multiple_selection=False, nodes=(Node(close_icon_style='danger', name='Workflow', nodes=(Node(c…" - ], "application/vnd.jupyter.widget-view+json": { + "model_id": "ed85d138b9aa4e2f9a830cbb066f494a", "version_major": 2, - "version_minor": 0, - "model_id": "edd9ecaffad14b07bdefdad2f2cae161" - } + "version_minor": 0 + }, + "text/plain": [ + "LexicalBagTree(multiple_selection=False, nodes=(Node(close_icon_style='danger', name='Workflow', nodes=(Node(c…" + ] }, "execution_count": 91, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 91 + "source": [ + "widget = browser.browse()\n", + "widget" + ] }, { + "cell_type": "markdown", + "id": "3fab94fab657aebd", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:29.119470Z", "start_time": "2026-07-11T03:59:29.096217Z" } }, - "cell_type": "markdown", - "source": "In an interactive session, you can select an item and then `widget.load_selected()` to re-instantiate it.", - "id": "3fab94fab657aebd" + "source": [ + "In an interactive session, you can select an item and then `widget.load_selected()` to re-instantiate it." + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "There is no deep magic in this tool, it is just a convenient shortcut for mapping the \"Lexical path\" of a `fr.schemas.LiveWorkflow` to the underlying `bagofholding.H5Bag` file structure.", - "id": "a5d1637cb2072044" + "id": "a5d1637cb2072044", + "metadata": {}, + "source": [ + "There is no deep magic in this tool, it is just a convenient shortcut for mapping the \"Lexical path\" of a `fr.schemas.LiveWorkflow` to the underlying `bagofholding.H5Bag` file structure." + ] }, { + "cell_type": "markdown", + "id": "50bfd15d81eabf32", "metadata": { "ExecuteTime": { "end_time": "2026-07-11T03:59:29.204858Z", "start_time": "2026-07-11T03:59:29.122996Z" } }, - "cell_type": "markdown", "source": [ "---\n", "## Summary\n", @@ -4793,8 +6471,7 @@ "functions. Accompanying retrospective instance-view dataclasses provide a format for associating\n", "recipes and data, and a toy WfMS demonstrates the conversion between\n", "prospective recipes and data." - ], - "id": "50bfd15d81eabf32" + ] } ], "metadata": { @@ -4802,6 +6479,369 @@ "display_name": "Python 3", "language": "python", "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.3" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": { + "61e4684ca5394b6390eb1c3a5b2f84ea": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "5da0aa16-5f01-450f-8caf-3e2fc074d6a1", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "default", + "disabled": true, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "...", + "nodes": [], + "open_icon": "plus", + "open_icon_style": "default", + "opened": true, + "selected": false, + "show_icon": true + } + }, + "907e34b440614d70bed77784006d7c30": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "c4696811-32b7-4cf8-a5c8-2921a2b6d715", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "default", + "disabled": true, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "...", + "nodes": [], + "open_icon": "plus", + "open_icon_style": "default", + "opened": true, + "selected": false, + "show_icon": true + } + }, + "a14be1b22c4140bf8e832460b0ec4f12": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b8f92d7005984a3d998ec418fc6c2273": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "ef5a73eb-57c5-4b08-927c-22c62795800d", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "danger", + "disabled": false, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "rescale_all_0", + "nodes": [ + "IPY_MODEL_cffd73f5787248d88b784f610aa29fbd" + ], + "open_icon": "plus", + "open_icon_style": "success", + "opened": false, + "selected": false, + "show_icon": true + } + }, + "c3ae075c616843509383dbe73f9976e2": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "67828ab0-8a69-4246-9148-b75d1c5b1004", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "danger", + "disabled": false, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "Workflow", + "nodes": [ + "IPY_MODEL_c5aae3110fd247889c2a228ef2ff93cf", + "IPY_MODEL_eb2c16d7adfb46fc8c7339da4506a3cd", + "IPY_MODEL_e8eb0aba5ba84ab584626f63cb553d2e", + "IPY_MODEL_b8f92d7005984a3d998ec418fc6c2273" + ], + "open_icon": "plus", + "open_icon_style": "success", + "opened": true, + "selected": false, + "show_icon": true + } + }, + "c5aae3110fd247889c2a228ef2ff93cf": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "7ab1a251-8d2c-493c-83ca-bf16869377df", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "danger", + "disabled": false, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "inputs", + "nodes": [ + "IPY_MODEL_caaca5600789419a807151f13f68442c" + ], + "open_icon": "plus", + "open_icon_style": "success", + "opened": false, + "selected": false, + "show_icon": true + } + }, + "caaca5600789419a807151f13f68442c": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "20647617-1ad9-4c41-b800-322b06514f08", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "default", + "disabled": true, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "...", + "nodes": [], + "open_icon": "plus", + "open_icon_style": "default", + "opened": true, + "selected": false, + "show_icon": true + } + }, + "cffd73f5787248d88b784f610aa29fbd": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "ff90965a-799e-49fe-965f-46f02709d103", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "default", + "disabled": true, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "...", + "nodes": [], + "open_icon": "plus", + "open_icon_style": "default", + "opened": true, + "selected": false, + "show_icon": true + } + }, + "e8eb0aba5ba84ab584626f63cb553d2e": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "f0c4d877-d9ef-4ccc-86d6-79e0176a6b98", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "danger", + "disabled": false, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "int_list_0", + "nodes": [ + "IPY_MODEL_61e4684ca5394b6390eb1c3a5b2f84ea" + ], + "open_icon": "plus", + "open_icon_style": "success", + "opened": false, + "selected": false, + "show_icon": true + } + }, + "eb2c16d7adfb46fc8c7339da4506a3cd": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "NodeModel", + "state": { + "_id": "f016ad10-a5a3-485e-aae4-888a68403836", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "NodeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "NodeView", + "close_icon": "minus", + "close_icon_style": "danger", + "disabled": false, + "icon": "folder", + "icon_image": "", + "icon_style": "default", + "name": "outputs", + "nodes": [ + "IPY_MODEL_907e34b440614d70bed77784006d7c30" + ], + "open_icon": "plus", + "open_icon_style": "success", + "opened": false, + "selected": false, + "show_icon": true + } + }, + "ed85d138b9aa4e2f9a830cbb066f494a": { + "model_module": "ipytree", + "model_module_version": "^0.2", + "model_name": "TreeModel", + "state": { + "_dom_classes": [], + "_id": "#", + "_model_module": "ipytree", + "_model_module_version": "^0.2", + "_model_name": "TreeModel", + "_view_count": null, + "_view_module": "ipytree", + "_view_module_version": "^0.2", + "_view_name": "TreeView", + "animation": 200, + "layout": "IPY_MODEL_a14be1b22c4140bf8e832460b0ec4f12", + "multiple_selection": false, + "nodes": [ + "IPY_MODEL_c3ae075c616843509383dbe73f9976e2" + ], + "selected_nodes": [], + "tabbable": null, + "tooltip": null + } + } + }, + "version_major": 2, + "version_minor": 0 + } } }, "nbformat": 4, From 5d6e4b93a894a8e80d4c55ec01c434898aee8718 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:46:37 -0700 Subject: [PATCH 4/8] Move transformers Down into the prospective submodule. It's not important enough or independent enough to be in the top-level module. It generates prospective recipes, so let's move it here. Signed-off-by: Liam Huber --- src/flowrep/api/schemas.py | 4 ++-- src/flowrep/{ => prospective}/transformers.py | 0 src/flowrep/wfms.py | 3 ++- tests/unit/test_transformers.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) rename src/flowrep/{ => prospective}/transformers.py (100%) diff --git a/src/flowrep/api/schemas.py b/src/flowrep/api/schemas.py index e6225910..9f08f1cf 100644 --- a/src/flowrep/api/schemas.py +++ b/src/flowrep/api/schemas.py @@ -29,6 +29,8 @@ from flowrep.prospective.helper_models import ExceptionCase as ExceptionCase from flowrep.prospective.helper_models import LabeledRecipe as LabeledRecipe from flowrep.prospective.if_recipe import IfRecipe as IfRecipe +from flowrep.prospective.transformers import Transform1toN as Transform1toN +from flowrep.prospective.transformers import TransformNto1 as TransformNto1 from flowrep.prospective.try_recipe import TryRecipe as TryRecipe from flowrep.prospective.union_types import RecipeDiscrimination as RecipeDiscrimination from flowrep.prospective.union_types import Recipes as Recipes @@ -51,5 +53,3 @@ from flowrep.retrospective.datastructures import TryData as TryData from flowrep.retrospective.datastructures import WhileData as WhileData from flowrep.subgraph_validation import ProspectiveOutputEdges as ProspectiveOutputEdges -from flowrep.transformers import Transform1toN as Transform1toN -from flowrep.transformers import TransformNto1 as TransformNto1 diff --git a/src/flowrep/transformers.py b/src/flowrep/prospective/transformers.py similarity index 100% rename from src/flowrep/transformers.py rename to src/flowrep/prospective/transformers.py diff --git a/src/flowrep/wfms.py b/src/flowrep/wfms.py index 0ca802ab..ea91aa60 100644 --- a/src/flowrep/wfms.py +++ b/src/flowrep/wfms.py @@ -14,7 +14,7 @@ from pyiron_snippets import retrieve -from flowrep import base_models, edge_models, subgraph_validation, transformers +from flowrep import base_models, edge_models, subgraph_validation from flowrep.parsers import label_helpers from flowrep.prospective import ( atomic_recipe, @@ -22,6 +22,7 @@ for_recipe, helper_models, if_recipe, + transformers, try_recipe, union_types, while_recipe, diff --git a/tests/unit/test_transformers.py b/tests/unit/test_transformers.py index eede6a56..e258ea26 100644 --- a/tests/unit/test_transformers.py +++ b/tests/unit/test_transformers.py @@ -4,7 +4,8 @@ import unittest -from flowrep import transformers, wfms +from flowrep import wfms +from flowrep.prospective import transformers class TestTransform1toN(unittest.TestCase): From d763f56a0bb7d91fea8a571a9dbddc52ba89bf49 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:46:51 -0700 Subject: [PATCH 5/8] Remove comment reference to pwf Signed-off-by: Liam Huber --- src/flowrep/prospective/transformers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/flowrep/prospective/transformers.py b/src/flowrep/prospective/transformers.py index ff8dfaaa..b3b0375d 100644 --- a/src/flowrep/prospective/transformers.py +++ b/src/flowrep/prospective/transformers.py @@ -6,9 +6,6 @@ :data:`~flowrep.edge_models.OutputEdges` admits exactly one source per output target -- so a WfMS records that fan-out and fan-in honestly by building nodes for it, rather than leaving it implicit and unrecordable. - -These mirror ``pyiron_workflow.transformers`` deliberately, down to the port labels and -function names, so that records produced by different WfMS describe the same shape. """ from __future__ import annotations From 8da09d19494ed5f3d9ece0c0933bbe3e20d78769 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:47:34 -0700 Subject: [PATCH 6/8] Move test file To reflect location of tested module Signed-off-by: Liam Huber --- tests/unit/{ => prospective}/test_transformers.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{ => prospective}/test_transformers.py (100%) diff --git a/tests/unit/test_transformers.py b/tests/unit/prospective/test_transformers.py similarity index 100% rename from tests/unit/test_transformers.py rename to tests/unit/prospective/test_transformers.py From fd2218925f72dddfb2c609f24597aac21bdebbad Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:56:29 -0700 Subject: [PATCH 7/8] Simplify test The suite was a stop-gap from when the flow controllers completely failed to produce edges. I don't mind the test, but it can be a lot simpler. Signed-off-by: Liam Huber --- tests/unit/drawing/test_render.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unit/drawing/test_render.py b/tests/unit/drawing/test_render.py index 26d65fbb..c6632d8f 100644 --- a/tests/unit/drawing/test_render.py +++ b/tests/unit/drawing/test_render.py @@ -283,19 +283,19 @@ def test_no_recorded_edges_note_appears_in_cluster_label(self): The retrospective format does not oblige every WfMS to record them, though, so stripping the edges back off a run node stands in for one that doesn't. """ - data = wfms.run_recipe(_for_recipe(), xs=[1, 2, 3]) - data.input_edges = {} - data.edges = {} - data.output_edges = {} + recipe = workflow_recipe.WorkflowRecipe( + inputs=[], + outputs=[], + nodes={"n": constant_recipe.ConstantRecipe(constant=1)}, + input_edges={}, + edges={}, + output_edges={}, + ) + data = wfms.run_recipe(recipe) graph = retrospective.build(data, depth=0) source = render.render(graph).source self.assertIn("no recorded edges", source) - def test_no_note_when_the_wfms_records_its_edges(self): - data = wfms.run_recipe(_for_recipe(), xs=[1, 2, 3]) - graph = retrospective.build(data, depth=0) - self.assertNotIn("no recorded edges", render.render(graph).source) - @unittest.skipUnless(_has_graphviz, "graphviz not installed") class TestActuallyRenders(unittest.TestCase): From 9e1786208544c96eab28ab2125f6808dda42daa6 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Mon, 7 Sep 2026 19:57:47 -0700 Subject: [PATCH 8/8] Re-execute notebook Signed-off-by: Liam Huber --- notebooks/user-guide.ipynb | 3470 +++++++++++++----------------------- 1 file changed, 1227 insertions(+), 2243 deletions(-) diff --git a/notebooks/user-guide.ipynb b/notebooks/user-guide.ipynb index 70a2f4a7..858dcf68 100644 --- a/notebooks/user-guide.ipynb +++ b/notebooks/user-guide.ipynb @@ -36,20 +36,25 @@ }, { "cell_type": "code", - "execution_count": 1, "id": "9505e25f9b266276", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.548577Z", - "start_time": "2026-09-08T00:49:22.246134Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:03.798252Z", "iopub.status.busy": "2026-09-07T17:47:03.798044Z", "iopub.status.idle": "2026-09-07T17:47:04.115623Z", "shell.execute_reply": "2026-09-07T17:47:04.115248Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.487523Z", + "start_time": "2026-09-08T02:56:45.144870Z" } }, + "source": [ + "import flowrep as fr\n", + "\n", + "# Quick check that the four main entry points are available:\n", + "fr.parse_atomic, fr.atomic, fr.workflow, fr.dataclass" + ], "outputs": [ { "data": { @@ -65,12 +70,7 @@ "output_type": "execute_result" } ], - "source": [ - "import flowrep as fr\n", - "\n", - "# Quick check that the four main entry points are available:\n", - "fr.parse_atomic, fr.atomic, fr.workflow, fr.dataclass" - ] + "execution_count": 1 }, { "cell_type": "markdown", @@ -106,20 +106,31 @@ }, { "cell_type": "code", - "execution_count": 2, "id": "c66106e9842c6a0b", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.583265Z", - "start_time": "2026-09-08T00:49:22.564678Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.116784Z", "iopub.status.busy": "2026-09-07T17:47:04.116635Z", "iopub.status.idle": "2026-09-07T17:47:04.120853Z", "shell.execute_reply": "2026-09-07T17:47:04.120449Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.560888Z", + "start_time": "2026-09-08T02:56:45.504309Z" } }, + "source": [ + "@fr.atomic\n", + "def add(a, b):\n", + " result = a + b\n", + " return result\n", + "\n", + "\n", + "# The decorator attaches a recipe to the function — it's still callable as usual:\n", + "print(f\"add(2, 3) = {add(2, 3)}\")\n", + "print(f\"Recipe type: {type(add.flowrep_recipe).__name__}\")\n", + "add.flowrep_recipe" + ], "outputs": [ { "name": "stdout", @@ -140,18 +151,7 @@ "output_type": "execute_result" } ], - "source": [ - "@fr.atomic\n", - "def add(a, b):\n", - " result = a + b\n", - " return result\n", - "\n", - "\n", - "# The decorator attaches a recipe to the function — it's still callable as usual:\n", - "print(f\"add(2, 3) = {add(2, 3)}\")\n", - "print(f\"Recipe type: {type(add.flowrep_recipe).__name__}\")\n", - "add.flowrep_recipe" - ] + "execution_count": 2 }, { "cell_type": "markdown", @@ -172,20 +172,28 @@ }, { "cell_type": "code", - "execution_count": 3, "id": "fbb2ac82", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.637275Z", - "start_time": "2026-09-08T00:49:22.602013Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.121636Z", "iopub.status.busy": "2026-09-07T17:47:04.121553Z", "iopub.status.idle": "2026-09-07T17:47:04.124637Z", "shell.execute_reply": "2026-09-07T17:47:04.124229Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.615990Z", + "start_time": "2026-09-08T02:56:45.576099Z" } }, + "source": [ + "def multiply(x, y):\n", + " product = x * y\n", + " return product\n", + "\n", + "\n", + "recipe = fr.parse_atomic(multiply)\n", + "recipe" + ], "outputs": [ { "data": { @@ -198,15 +206,7 @@ "output_type": "execute_result" } ], - "source": [ - "def multiply(x, y):\n", - " product = x * y\n", - " return product\n", - "\n", - "\n", - "recipe = fr.parse_atomic(multiply)\n", - "recipe" - ] + "execution_count": 3 }, { "cell_type": "markdown", @@ -221,20 +221,23 @@ }, { "cell_type": "code", - "execution_count": 4, "id": "d652b49c0740e517", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.656137Z", - "start_time": "2026-09-08T00:49:22.639351Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.125312Z", "iopub.status.busy": "2026-09-07T17:47:04.125246Z", "iopub.status.idle": "2026-09-07T17:47:04.127649Z", "shell.execute_reply": "2026-09-07T17:47:04.127415Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.665291Z", + "start_time": "2026-09-08T02:56:45.631675Z" } }, + "source": [ + "for reserved in sorted(fr.schemas.RESERVED_NAMES):\n", + " print(reserved)" + ], "outputs": [ { "name": "stdout", @@ -245,10 +248,7 @@ ] } ], - "source": [ - "for reserved in sorted(fr.schemas.RESERVED_NAMES):\n", - " print(reserved)" - ] + "execution_count": 4 }, { "cell_type": "markdown", @@ -261,32 +261,19 @@ }, { "cell_type": "code", - "execution_count": 5, "id": "4ce6917d", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.689278Z", - "start_time": "2026-09-08T00:49:22.670350Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.128408Z", "iopub.status.busy": "2026-09-07T17:47:04.128352Z", "iopub.status.idle": "2026-09-07T17:47:04.132924Z", "shell.execute_reply": "2026-09-07T17:47:04.132660Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.716195Z", + "start_time": "2026-09-08T02:56:45.682070Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 'inputs' rejected: ValidationError\n", - " '0port' rejected: ValidationError\n", - " 'class' rejected: ValidationError\n", - " 'my-port' rejected: ValidationError\n" - ] - } - ], "source": [ "# These will fail:\n", "from pydantic import TypeAdapter\n", @@ -303,7 +290,20 @@ " label_adapter.validate_python(bad_name)\n", " except Exception as e:\n", " print(f\" '{bad_name}' rejected: {type(e).__name__}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 'inputs' rejected: ValidationError\n", + " '0port' rejected: ValidationError\n", + " 'class' rejected: ValidationError\n", + " 'my-port' rejected: ValidationError\n" + ] + } + ], + "execution_count": 5 }, { "cell_type": "markdown", @@ -329,29 +329,19 @@ }, { "cell_type": "code", - "execution_count": 6, "id": "cac0a1bd", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.726585Z", - "start_time": "2026-09-08T00:49:22.705208Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.133723Z", "iopub.status.busy": "2026-09-07T17:47:04.133652Z", "iopub.status.idle": "2026-09-07T17:47:04.136916Z", "shell.execute_reply": "2026-09-07T17:47:04.136487Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.764630Z", + "start_time": "2026-09-08T02:56:45.732089Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Outputs: ['quotient', 'remainder']\n" - ] - } - ], "source": [ "@fr.atomic\n", "def divide(a, b):\n", @@ -362,7 +352,17 @@ "\n", "print(f\"Outputs: {divide.flowrep_recipe.outputs}\")\n", "assert(divide.flowrep_recipe.outputs == [\"quotient\", \"remainder\"])" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outputs: ['quotient', 'remainder']\n" + ] + } + ], + "execution_count": 6 }, { "cell_type": "markdown", @@ -376,20 +376,28 @@ }, { "cell_type": "code", - "execution_count": 7, "id": "9f659bd7", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.761232Z", - "start_time": "2026-09-08T00:49:22.743329Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.137577Z", "iopub.status.busy": "2026-09-07T17:47:04.137512Z", "iopub.status.idle": "2026-09-07T17:47:04.141043Z", "shell.execute_reply": "2026-09-07T17:47:04.140691Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.811967Z", + "start_time": "2026-09-08T02:56:45.779907Z" } }, + "source": [ + "@fr.atomic\n", + "def increment(x):\n", + " return x + 1\n", + "\n", + "\n", + "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", + "assert(increment.flowrep_recipe.outputs == [\"output_0\"])" + ], "outputs": [ { "name": "stdout", @@ -399,15 +407,7 @@ ] } ], - "source": [ - "@fr.atomic\n", - "def increment(x):\n", - " return x + 1\n", - "\n", - "\n", - "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", - "assert(increment.flowrep_recipe.outputs == [\"output_0\"])" - ] + "execution_count": 7 }, { "cell_type": "markdown", @@ -419,20 +419,27 @@ }, { "cell_type": "code", - "execution_count": 8, "id": "d5a7b4f2477b326", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.795874Z", - "start_time": "2026-09-08T00:49:22.776489Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.141665Z", "iopub.status.busy": "2026-09-07T17:47:04.141607Z", "iopub.status.idle": "2026-09-07T17:47:04.145011Z", "shell.execute_reply": "2026-09-07T17:47:04.144627Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.870562Z", + "start_time": "2026-09-08T02:56:45.824185Z" } }, + "source": [ + "@fr.atomic(\"stepped\")\n", + "def increment(x):\n", + " return x + 1\n", + "\n", + "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", + "assert(increment.flowrep_recipe.outputs == [\"stepped\"])" + ], "outputs": [ { "name": "stdout", @@ -442,14 +449,7 @@ ] } ], - "source": [ - "@fr.atomic(\"stepped\")\n", - "def increment(x):\n", - " return x + 1\n", - "\n", - "print(f\"Outputs: {increment.flowrep_recipe.outputs}\")\n", - "assert(increment.flowrep_recipe.outputs == [\"stepped\"])" - ] + "execution_count": 8 }, { "cell_type": "markdown", @@ -463,20 +463,28 @@ }, { "cell_type": "code", - "execution_count": 9, "id": "467bd6b8", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.827586Z", - "start_time": "2026-09-08T00:49:22.810176Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.145597Z", "iopub.status.busy": "2026-09-07T17:47:04.145543Z", "iopub.status.idle": "2026-09-07T17:47:04.149040Z", "shell.execute_reply": "2026-09-07T17:47:04.148543Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.920378Z", + "start_time": "2026-09-08T02:56:45.887887Z" } }, + "source": [ + "@fr.atomic(\"tuple_return\")\n", + "def righthanded2lefthanded(x, y, z):\n", + " return (x, z, y)\n", + "\n", + "\n", + "print(f\"Outputs: {righthanded2lefthanded.flowrep_recipe.outputs}\")\n", + "assert(righthanded2lefthanded.flowrep_recipe.outputs == [\"tuple_return\"])" + ], "outputs": [ { "name": "stdout", @@ -486,15 +494,7 @@ ] } ], - "source": [ - "@fr.atomic(\"tuple_return\")\n", - "def righthanded2lefthanded(x, y, z):\n", - " return (x, z, y)\n", - "\n", - "\n", - "print(f\"Outputs: {righthanded2lefthanded.flowrep_recipe.outputs}\")\n", - "assert(righthanded2lefthanded.flowrep_recipe.outputs == [\"tuple_return\"])" - ] + "execution_count": 9 }, { "cell_type": "markdown", @@ -506,29 +506,19 @@ }, { "cell_type": "code", - "execution_count": 10, "id": "0d46e437", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.857797Z", - "start_time": "2026-09-08T00:49:22.840823Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.149853Z", "iopub.status.busy": "2026-09-07T17:47:04.149784Z", "iopub.status.idle": "2026-09-07T17:47:04.152598Z", "shell.execute_reply": "2026-09-07T17:47:04.152238Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:45.962929Z", + "start_time": "2026-09-08T02:56:45.932043Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Outputs: ['magnitude', 'angle']\n" - ] - } - ], "source": [ "from typing import Annotated\n", "\n", @@ -547,7 +537,17 @@ "\n", "print(f\"Outputs: {norm.flowrep_recipe.outputs}\")\n", "assert(norm.flowrep_recipe.outputs == ['magnitude', 'angle'])" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outputs: ['magnitude', 'angle']\n" + ] + } + ], + "execution_count": 10 }, { "cell_type": "markdown", @@ -566,20 +566,29 @@ }, { "cell_type": "code", - "execution_count": 11, "id": "77276c40", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.889028Z", - "start_time": "2026-09-08T00:49:22.872367Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.153512Z", "iopub.status.busy": "2026-09-07T17:47:04.153421Z", "iopub.status.idle": "2026-09-07T17:47:04.156842Z", "shell.execute_reply": "2026-09-07T17:47:04.156408Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.007552Z", + "start_time": "2026-09-08T02:56:45.977343Z" } }, + "source": [ + "recipe = add.flowrep_recipe\n", + "ref = recipe.reference\n", + "print(f\"Module: {ref.info.module}\")\n", + "print(f\"Qualname: {ref.info.qualname}\")\n", + "print(f\"Version: {ref.info.version}\")\n", + "print(f\"FQN: {recipe.fully_qualified_name}\")\n", + "print(f\"Defaults: {ref.inputs_with_defaults}\")\n", + "print(f\"Restricted kinds: {ref.restricted_input_kinds}\")" + ], "outputs": [ { "name": "stdout", @@ -594,16 +603,7 @@ ] } ], - "source": [ - "recipe = add.flowrep_recipe\n", - "ref = recipe.reference\n", - "print(f\"Module: {ref.info.module}\")\n", - "print(f\"Qualname: {ref.info.qualname}\")\n", - "print(f\"Version: {ref.info.version}\")\n", - "print(f\"FQN: {recipe.fully_qualified_name}\")\n", - "print(f\"Defaults: {ref.inputs_with_defaults}\")\n", - "print(f\"Restricted kinds: {ref.restricted_input_kinds}\")" - ] + "execution_count": 11 }, { "cell_type": "markdown", @@ -652,31 +652,19 @@ }, { "cell_type": "code", - "execution_count": 12, "id": "b8f03ee2", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.920459Z", - "start_time": "2026-09-08T00:49:22.904409Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.157905Z", "iopub.status.busy": "2026-09-07T17:47:04.157811Z", "iopub.status.idle": "2026-09-07T17:47:04.161856Z", "shell.execute_reply": "2026-09-07T17:47:04.161367Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.051970Z", + "start_time": "2026-09-08T02:56:46.020118Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "__main__.MyClass ['x'] ['instance']\n", - "__main__.MyClass.some_method ['self', 'y'] ['output_0']\n", - "__main__.MyClass.some_static ['a'] ['triple', 'single']\n" - ] - } - ], "source": [ "import flowrep as fr\n", "\n", @@ -705,7 +693,19 @@ " decorated.flowrep_recipe.inputs,\n", " decorated.flowrep_recipe.outputs\n", " )" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "__main__.MyClass ['x'] ['instance']\n", + "__main__.MyClass.some_method ['self', 'y'] ['output_0']\n", + "__main__.MyClass.some_static ['a'] ['triple', 'single']\n" + ] + } + ], + "execution_count": 12 }, { "cell_type": "markdown", @@ -717,20 +717,31 @@ }, { "cell_type": "code", - "execution_count": 13, "id": "66ac7ffa", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:22.952760Z", - "start_time": "2026-09-08T00:49:22.934113Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.162630Z", "iopub.status.busy": "2026-09-07T17:47:04.162562Z", "iopub.status.idle": "2026-09-07T17:47:04.166819Z", "shell.execute_reply": "2026-09-07T17:47:04.166373Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.111212Z", + "start_time": "2026-09-08T02:56:46.067762Z" } }, + "source": [ + "import dataclasses\n", + "\n", + "@fr.dataclass\n", + "class MyDataclass:\n", + " a: str\n", + " x: int = 1\n", + "\n", + "print(\"Dataclass?\", dataclasses.is_dataclass(MyDataclass))\n", + "print(\"Recipe?\", type(MyDataclass.flowrep_recipe))\n", + "MyDataclass.flowrep_recipe.inputs, MyDataclass.flowrep_recipe.outputs" + ], "outputs": [ { "name": "stdout", @@ -751,18 +762,7 @@ "output_type": "execute_result" } ], - "source": [ - "import dataclasses\n", - "\n", - "@fr.dataclass\n", - "class MyDataclass:\n", - " a: str\n", - " x: int = 1\n", - "\n", - "print(\"Dataclass?\", dataclasses.is_dataclass(MyDataclass))\n", - "print(\"Recipe?\", type(MyDataclass.flowrep_recipe))\n", - "MyDataclass.flowrep_recipe.inputs, MyDataclass.flowrep_recipe.outputs" - ] + "execution_count": 13 }, { "cell_type": "markdown", @@ -774,20 +774,26 @@ }, { "cell_type": "code", - "execution_count": 14, "id": "915053248a94e896", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.001834Z", - "start_time": "2026-09-08T00:49:22.966371Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.167617Z", "iopub.status.busy": "2026-09-07T17:47:04.167542Z", "iopub.status.idle": "2026-09-07T17:47:04.170822Z", "shell.execute_reply": "2026-09-07T17:47:04.170367Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.143201Z", + "start_time": "2026-09-08T02:56:46.114986Z" } }, + "source": [ + "instance = MyDataclass(a=\"hello\", x=7)\n", + "\n", + "print(\"Unpacking inputs: \", MyDataclass.flowrep_recipe_unpacking.inputs)\n", + "print(\"Unpacking outputs:\", MyDataclass.flowrep_recipe_unpacking.outputs)\n", + "MyDataclass.flowrep_recipe_unpacking(instance)" + ], "outputs": [ { "name": "stdout", @@ -808,13 +814,7 @@ "output_type": "execute_result" } ], - "source": [ - "instance = MyDataclass(a=\"hello\", x=7)\n", - "\n", - "print(\"Unpacking inputs: \", MyDataclass.flowrep_recipe_unpacking.inputs)\n", - "print(\"Unpacking outputs:\", MyDataclass.flowrep_recipe_unpacking.outputs)\n", - "MyDataclass.flowrep_recipe_unpacking(instance)" - ] + "execution_count": 14 }, { "cell_type": "markdown", @@ -848,30 +848,19 @@ }, { "cell_type": "code", - "execution_count": 15, "id": "cd048745aefa7acc", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.032535Z", - "start_time": "2026-09-08T00:49:23.003357Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.171521Z", "iopub.status.busy": "2026-09-07T17:47:04.171457Z", "iopub.status.idle": "2026-09-07T17:47:04.175029Z", "shell.execute_reply": "2026-09-07T17:47:04.174586Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.186696Z", + "start_time": "2026-09-08T02:56:46.157416Z" } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inputs: ['start', 'stop', 'num']\n", - "Outputs: ['samples']\n" - ] - } - ], + }, "source": [ "import numpy as np\n", "from pyiron_snippets import versions\n", @@ -886,7 +875,18 @@ ")\n", "print(f\"Inputs: {linspace_node.inputs}\")\n", "print(f\"Outputs: {linspace_node.outputs}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inputs: ['start', 'stop', 'num']\n", + "Outputs: ['samples']\n" + ] + } + ], + "execution_count": 15 }, { "cell_type": "markdown", @@ -907,29 +907,19 @@ }, { "cell_type": "code", - "execution_count": 16, "id": "67cf6163", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.088351Z", - "start_time": "2026-09-08T00:49:23.056918Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.175702Z", "iopub.status.busy": "2026-09-07T17:47:04.175637Z", "iopub.status.idle": "2026-09-07T17:47:04.178861Z", "shell.execute_reply": "2026-09-07T17:47:04.178526Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.204023Z", + "start_time": "2026-09-08T02:56:46.188908Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Restricted kinds: {'start': , 'stop': , 'step': }\n" - ] - } - ], "source": [ "arange_node = fr.schemas.AtomicRecipe(\n", " reference=fr.schemas.PythonReference(\n", @@ -945,7 +935,17 @@ " outputs=[\"values\"],\n", ")\n", "print(f\"Restricted kinds: {arange_node.reference.restricted_input_kinds}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted kinds: {'start': , 'stop': , 'step': }\n" + ] + } + ], + "execution_count": 16 }, { "cell_type": "markdown", @@ -977,20 +977,22 @@ }, { "cell_type": "code", - "execution_count": 17, "id": "d0b951260802626c", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.107045Z", - "start_time": "2026-09-08T00:49:23.090463Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.179665Z", "iopub.status.busy": "2026-09-07T17:47:04.179600Z", "iopub.status.idle": "2026-09-07T17:47:04.182761Z", "shell.execute_reply": "2026-09-07T17:47:04.182374Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.245312Z", + "start_time": "2026-09-08T02:56:46.217563Z" } }, + "source": [ + "arange_node(start=1, stop=2, step=3)" + ], "outputs": [ { "data": { @@ -1003,9 +1005,7 @@ "output_type": "execute_result" } ], - "source": [ - "arange_node(start=1, stop=2, step=3)" - ] + "execution_count": 17 }, { "cell_type": "markdown", @@ -1024,29 +1024,19 @@ }, { "cell_type": "code", - "execution_count": 18, "id": "396728243a8a212e", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.143009Z", - "start_time": "2026-09-08T00:49:23.123099Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.183499Z", "iopub.status.busy": "2026-09-07T17:47:04.183430Z", "iopub.status.idle": "2026-09-07T17:47:04.186987Z", "shell.execute_reply": "2026-09-07T17:47:04.186520Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.261412Z", + "start_time": "2026-09-08T02:56:46.247479Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Restricted kinds: {'this': , 'that': , 'the_other': }\n" - ] - } - ], "source": [ "def my_dict_maker(**kwargs):\n", " return dict(**kwargs)\n", @@ -1064,24 +1054,36 @@ " outputs=[\"as_a_dict\"],\n", ")\n", "print(f\"Restricted kinds: {my_particular_dict_recipe.reference.restricted_input_kinds}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted kinds: {'this': , 'that': , 'the_other': }\n" + ] + } + ], + "execution_count": 18 }, { "cell_type": "code", - "execution_count": 19, "id": "5e2b36c02de9cb22", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.163112Z", - "start_time": "2026-09-08T00:49:23.143863Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.187684Z", "iopub.status.busy": "2026-09-07T17:47:04.187612Z", "iopub.status.idle": "2026-09-07T17:47:04.190805Z", "shell.execute_reply": "2026-09-07T17:47:04.190519Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.276832Z", + "start_time": "2026-09-08T02:56:46.262302Z" } }, + "source": [ + "my_particular_dict_recipe(this=1, that=2, the_other=3)" + ], "outputs": [ { "data": { @@ -1094,9 +1096,7 @@ "output_type": "execute_result" } ], - "source": [ - "my_particular_dict_recipe(this=1, that=2, the_other=3)" - ] + "execution_count": 19 }, { "cell_type": "markdown", @@ -1108,29 +1108,19 @@ }, { "cell_type": "code", - "execution_count": 20, "id": "d4026a680946afca", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.208617Z", - "start_time": "2026-09-08T00:49:23.176735Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.191546Z", "iopub.status.busy": "2026-09-07T17:47:04.191482Z", "iopub.status.idle": "2026-09-07T17:47:04.195022Z", "shell.execute_reply": "2026-09-07T17:47:04.194494Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.305070Z", + "start_time": "2026-09-08T02:56:46.290163Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Restricted kinds: {'a': , 'b': }\n" - ] - } - ], "source": [ "def my_variadic_add(*terms, offset=0):\n", " sum_ = offset\n", @@ -1152,24 +1142,36 @@ " outputs=[\"total\"],\n", ")\n", "print(f\"Restricted kinds: {my_offset_sum.reference.restricted_input_kinds}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted kinds: {'a': , 'b': }\n" + ] + } + ], + "execution_count": 20 }, { "cell_type": "code", - "execution_count": 21, "id": "3a6051ff2f2da9e0", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.227060Z", - "start_time": "2026-09-08T00:49:23.213254Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.195610Z", "iopub.status.busy": "2026-09-07T17:47:04.195551Z", "iopub.status.idle": "2026-09-07T17:47:04.198843Z", "shell.execute_reply": "2026-09-07T17:47:04.198596Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.335267Z", + "start_time": "2026-09-08T02:56:46.305899Z" } }, + "source": [ + "my_offset_sum(1, 2, offset=3)" + ], "outputs": [ { "data": { @@ -1182,9 +1184,7 @@ "output_type": "execute_result" } ], - "source": [ - "my_offset_sum(1, 2, offset=3)" - ] + "execution_count": 21 }, { "cell_type": "markdown", @@ -1223,40 +1223,19 @@ }, { "cell_type": "code", - "execution_count": 22, "id": "ca3cb2598c788de5", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.265325Z", - "start_time": "2026-09-08T00:49:23.228107Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.199567Z", "iopub.status.busy": "2026-09-07T17:47:04.199499Z", "iopub.status.idle": "2026-09-07T17:47:04.203794Z", "shell.execute_reply": "2026-09-07T17:47:04.203436Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "linear(3, 2, 1) = 7\n", - "Recipe type: WorkflowRecipe\n" - ] }, - { - "data": { - "text/plain": [ - "WorkflowRecipe(type=, inputs=['x', 'slope', 'intercept'], outputs=['result'], description='y = slope * x + intercept', nodes={'multiply_0': AtomicRecipe(type=, inputs=['x', 'y'], outputs=['product'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='multiply', version=None), inputs_with_defaults=[], restricted_input_kinds={})), 'add_0': AtomicRecipe(type=, inputs=['a', 'b'], outputs=['result'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='add', version=None), inputs_with_defaults=[], restricted_input_kinds={}))}, input_edges={TargetHandle(node='multiply_0', port='x'): InputSource(node=None, port='x'), TargetHandle(node='multiply_0', port='y'): InputSource(node=None, port='slope'), TargetHandle(node='add_0', port='b'): InputSource(node=None, port='intercept')}, edges={TargetHandle(node='add_0', port='a'): SourceHandle(node='multiply_0', port='product')}, output_edges={OutputTarget(node=None, port='result'): SourceHandle(node='add_0', port='result')}, reference=PythonReference(info=VersionInfo(module='__main__', qualname='linear', version=None), inputs_with_defaults=[], restricted_input_kinds={}))" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.381611Z", + "start_time": "2026-09-08T02:56:46.337893Z" } - ], + }, "source": [ "@fr.atomic\n", "def add(a, b):\n", @@ -1282,7 +1261,28 @@ "print(f\"linear(3, 2, 1) = {linear(3, 2, 1)}\")\n", "print(f\"Recipe type: {type(linear.flowrep_recipe).__name__}\")\n", "linear.flowrep_recipe" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "linear(3, 2, 1) = 7\n", + "Recipe type: WorkflowRecipe\n" + ] + }, + { + "data": { + "text/plain": [ + "WorkflowRecipe(type=, inputs=['x', 'slope', 'intercept'], outputs=['result'], description='y = slope * x + intercept', nodes={'multiply_0': AtomicRecipe(type=, inputs=['x', 'y'], outputs=['product'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='multiply', version=None), inputs_with_defaults=[], restricted_input_kinds={})), 'add_0': AtomicRecipe(type=, inputs=['a', 'b'], outputs=['result'], description=None, reference=PythonReference(info=VersionInfo(module='__main__', qualname='add', version=None), inputs_with_defaults=[], restricted_input_kinds={}))}, input_edges={TargetHandle(node='multiply_0', port='x'): InputSource(node=None, port='x'), TargetHandle(node='multiply_0', port='y'): InputSource(node=None, port='slope'), TargetHandle(node='add_0', port='b'): InputSource(node=None, port='intercept')}, edges={TargetHandle(node='add_0', port='a'): SourceHandle(node='multiply_0', port='product')}, output_edges={OutputTarget(node=None, port='result'): SourceHandle(node='add_0', port='result')}, reference=PythonReference(info=VersionInfo(module='__main__', qualname='linear', version=None), inputs_with_defaults=[], restricted_input_kinds={}))" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 22 }, { "cell_type": "markdown", @@ -1313,20 +1313,22 @@ }, { "cell_type": "code", - "execution_count": 23, "id": "d39608c2", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.284954Z", - "start_time": "2026-09-08T00:49:23.267481Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.204436Z", "iopub.status.busy": "2026-09-07T17:47:04.204374Z", "iopub.status.idle": "2026-09-07T17:47:04.207970Z", "shell.execute_reply": "2026-09-07T17:47:04.207684Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.399287Z", + "start_time": "2026-09-08T02:56:46.385428Z" } }, + "source": [ + "linear.flowrep_recipe(x=3, slope=2, intercept=1)" + ], "outputs": [ { "data": { @@ -1339,9 +1341,7 @@ "output_type": "execute_result" } ], - "source": [ - "linear.flowrep_recipe(x=3, slope=2, intercept=1)" - ] + "execution_count": 23 }, { "cell_type": "markdown", @@ -1353,137 +1353,29 @@ }, { "cell_type": "code", - "execution_count": 24, "id": "80ca0e65ffd4f939", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.813950Z", - "start_time": "2026-09-08T00:49:23.298183Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.208574Z", "iopub.status.busy": "2026-09-07T17:47:04.208516Z", "iopub.status.idle": "2026-09-07T17:47:04.235705Z", "shell.execute_reply": "2026-09-07T17:47:04.235364Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.639309Z", + "start_time": "2026-09-08T02:56:46.410700Z" } }, + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "linear.flowrep_recipe.draw()" + ], "outputs": [ { "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cluster_\n", - "\n", - "workflow\n", - "__main__.linear\n", - "\n", - "\n", - "\n", - "inputs.x\n", - "\n", - "x\n", - "\n", - "\n", - "\n", - "\n", - "multiply_0\n", - "\n", - "multiply_0\n", - "__main__.multiply\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "product\n", - "\n", - "\n", - "y\n", - "\n", - "\n", - "\n", - "\n", - "inputs.x:e->multiply_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.slope\n", - "\n", - "slope\n", - "\n", - "\n", - "\n", - "\n", - "inputs.slope:e->multiply_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.intercept\n", - "\n", - "intercept\n", - "\n", - "\n", - "\n", - "\n", - "add_0\n", - "\n", - "add_0\n", - "__main__.add\n", - "\n", - "\n", - "a\n", - "\n", - "\n", - "result\n", - "\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "inputs.intercept:e->add_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "outputs.result\n", - "\n", - "result\n", - "\n", - "\n", - "\n", - "\n", - "multiply_0:e->add_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "add_0:e->outputs.result:w\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], + "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nworkflow\n__main__.linear\n\n\n\ninputs.x\n\nx\n\n\n\n\nmultiply_0\n\nmultiply_0\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\ninputs.x:e->multiply_0:w\n\n\n\n\n\ninputs.slope\n\nslope\n\n\n\n\ninputs.slope:e->multiply_0:w\n\n\n\n\n\ninputs.intercept\n\nintercept\n\n\n\n\nadd_0\n\nadd_0\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\ninputs.intercept:e->add_0:w\n\n\n\n\n\noutputs.result\n\nresult\n\n\n\n\nmultiply_0:e->add_0:w\n\n\n\n\n\nadd_0:e->outputs.result:w\n\n\n\n\n\n", "text/plain": [ - "" + "" ] }, "execution_count": 24, @@ -1491,10 +1383,7 @@ "output_type": "execute_result" } ], - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "linear.flowrep_recipe.draw()" - ] + "execution_count": 24 }, { "cell_type": "markdown", @@ -1527,31 +1416,19 @@ }, { "cell_type": "code", - "execution_count": 25, "id": "ce41773494e520c8", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:23.861383Z", - "start_time": "2026-09-08T00:49:23.831547Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.236477Z", "iopub.status.busy": "2026-09-07T17:47:04.236402Z", "iopub.status.idle": "2026-09-07T17:47:04.240977Z", "shell.execute_reply": "2026-09-07T17:47:04.240544Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.664825Z", + "start_time": "2026-09-08T02:56:46.651259Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Top-level nodes: ['linear_0', 'quadratic_0']\n", - "linear_0 type: workflow\n", - "linear_0 children: ['multiply_0', 'add_0']\n" - ] - } - ], "source": [ "@fr.workflow\n", "def quadratic(x, a, b, c):\n", @@ -1575,7 +1452,19 @@ "print(f\"Top-level nodes: {list(recipe.nodes.keys())}\")\n", "print(f\"linear_0 type: {recipe.nodes['linear_0'].type}\")\n", "print(f\"linear_0 children: {list(recipe.nodes['linear_0'].nodes.keys())}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Top-level nodes: ['linear_0', 'quadratic_0']\n", + "linear_0 type: workflow\n", + "linear_0 children: ['multiply_0', 'add_0']\n" + ] + } + ], + "execution_count": 25 }, { "cell_type": "markdown", @@ -1602,198 +1491,29 @@ }, { "cell_type": "code", - "execution_count": 26, "id": "26ec1c16979aa5db", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.040207Z", - "start_time": "2026-09-08T00:49:23.873708Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.241643Z", "iopub.status.busy": "2026-09-07T17:47:04.241582Z", "iopub.status.idle": "2026-09-07T17:47:04.251682Z", "shell.execute_reply": "2026-09-07T17:47:04.251381Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:46.849690Z", + "start_time": "2026-09-08T02:56:46.674277Z" } }, + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "recipe.draw(depth=0)" + ], "outputs": [ { "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cluster_\n", - "\n", - "workflow\n", - "__main__.evaluate_both\n", - "\n", - "\n", - "\n", - "inputs.x\n", - "\n", - "x\n", - "\n", - "\n", - "\n", - "\n", - "linear_0\n", - "\n", - "linear_0\n", - "__main__.linear\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "result\n", - "\n", - "\n", - "slope\n", - "\n", - "\n", - "intercept\n", - "\n", - "\n", - "\n", - "\n", - "inputs.x:e->linear_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0\n", - "\n", - "quadratic_0\n", - "__main__.quadratic\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "result\n", - "\n", - "\n", - "a\n", - "\n", - "\n", - "b\n", - "\n", - "\n", - "c\n", - "\n", - "\n", - "\n", - "\n", - "inputs.x:e->quadratic_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.slope\n", - "\n", - "slope\n", - "\n", - "\n", - "\n", - "\n", - "inputs.slope:e->linear_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.intercept\n", - "\n", - "intercept\n", - "\n", - "\n", - "\n", - "\n", - "inputs.intercept:e->linear_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.a\n", - "\n", - "a\n", - "\n", - "\n", - "\n", - "\n", - "inputs.a:e->quadratic_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.b\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "inputs.b:e->quadratic_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.c\n", - "\n", - "c\n", - "\n", - "\n", - "\n", - "\n", - "inputs.c:e->quadratic_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "outputs.lin\n", - "\n", - "lin\n", - "\n", - "\n", - "\n", - "\n", - "outputs.quad\n", - "\n", - "quad\n", - "\n", - "\n", - "\n", - "\n", - "linear_0:e->outputs.lin:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0:e->outputs.quad:w\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], + "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nworkflow\n__main__.evaluate_both\n\n\n\ninputs.x\n\nx\n\n\n\n\nlinear_0\n\nlinear_0\n__main__.linear\n\n\nx\n\n\nresult\n\n\nslope\n\n\nintercept\n\n\n\n\ninputs.x:e->linear_0:w\n\n\n\n\n\nquadratic_0\n\nquadratic_0\n__main__.quadratic\n\n\nx\n\n\nresult\n\n\na\n\n\nb\n\n\nc\n\n\n\n\ninputs.x:e->quadratic_0:w\n\n\n\n\n\ninputs.slope\n\nslope\n\n\n\n\ninputs.slope:e->linear_0:w\n\n\n\n\n\ninputs.intercept\n\nintercept\n\n\n\n\ninputs.intercept:e->linear_0:w\n\n\n\n\n\ninputs.a\n\na\n\n\n\n\ninputs.a:e->quadratic_0:w\n\n\n\n\n\ninputs.b\n\nb\n\n\n\n\ninputs.b:e->quadratic_0:w\n\n\n\n\n\ninputs.c\n\nc\n\n\n\n\ninputs.c:e->quadratic_0:w\n\n\n\n\n\noutputs.lin\n\nlin\n\n\n\n\noutputs.quad\n\nquad\n\n\n\n\nlinear_0:e->outputs.lin:w\n\n\n\n\n\nquadratic_0:e->outputs.quad:w\n\n\n\n\n\n", "text/plain": [ - "" + "" ] }, "execution_count": 26, @@ -1801,452 +1521,33 @@ "output_type": "execute_result" } ], - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "recipe.draw(depth=0)" - ] + "execution_count": 26 }, { "cell_type": "code", - "execution_count": 27, "id": "01872b7c31aaedd3", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.208054Z", - "start_time": "2026-09-08T00:49:24.049713Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.252364Z", "iopub.status.busy": "2026-09-07T17:47:04.252306Z", "iopub.status.idle": "2026-09-07T17:47:04.265118Z", "shell.execute_reply": "2026-09-07T17:47:04.264452Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.028094Z", + "start_time": "2026-09-08T02:56:46.863552Z" } }, + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "recipe.draw(depth=1)" + ], "outputs": [ { "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cluster_\n", - "\n", - "workflow\n", - "__main__.evaluate_both\n", - "\n", - "\n", - "cluster_linear_0\n", - "\n", - "linear_0\n", - "__main__.linear\n", - "\n", - "\n", - "cluster_quadratic_0\n", - "\n", - "quadratic_0\n", - "__main__.quadratic\n", - "\n", - "\n", - "\n", - "inputs.x\n", - "\n", - "x\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.inputs.x\n", - "\n", - "x\n", - "\n", - "\n", - "\n", - "\n", - "inputs.x:e->linear_0.inputs.x:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.x\n", - "\n", - "x\n", - "\n", - "\n", - "\n", - "\n", - "inputs.x:e->quadratic_0.inputs.x:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.slope\n", - "\n", - "slope\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.inputs.slope\n", - "\n", - "slope\n", - "\n", - "\n", - "\n", - "\n", - "inputs.slope:e->linear_0.inputs.slope:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.intercept\n", - "\n", - "intercept\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.inputs.intercept\n", - "\n", - "intercept\n", - "\n", - "\n", - "\n", - "\n", - "inputs.intercept:e->linear_0.inputs.intercept:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.a\n", - "\n", - "a\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.a\n", - "\n", - "a\n", - "\n", - "\n", - "\n", - "\n", - "inputs.a:e->quadratic_0.inputs.a:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.b\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.b\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "inputs.b:e->quadratic_0.inputs.b:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.c\n", - "\n", - "c\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.c\n", - "\n", - "c\n", - "\n", - "\n", - "\n", - "\n", - "inputs.c:e->quadratic_0.inputs.c:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "outputs.lin\n", - "\n", - "lin\n", - "\n", - "\n", - "\n", - "\n", - "outputs.quad\n", - "\n", - "quad\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.multiply_0\n", - "\n", - "multiply_0\n", - "__main__.multiply\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "product\n", - "\n", - "\n", - "y\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.inputs.x:e->linear_0.multiply_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.inputs.slope:e->linear_0.multiply_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.add_0\n", - "\n", - "add_0\n", - "__main__.add\n", - "\n", - "\n", - "a\n", - "\n", - "\n", - "result\n", - "\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.inputs.intercept:e->linear_0.add_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.outputs.result\n", - "\n", - "result\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.outputs.result:e->outputs.lin:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.multiply_0:e->linear_0.add_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "linear_0.add_0:e->linear_0.outputs.result:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.multiply_0\n", - "\n", - "multiply_0\n", - "__main__.multiply\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "product\n", - "\n", - "\n", - "y\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.multiply_2\n", - "\n", - "multiply_2\n", - "__main__.multiply\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "product\n", - "\n", - "\n", - "y\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.x:e->quadratic_0.multiply_2:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.multiply_1\n", - "\n", - "multiply_1\n", - "__main__.multiply\n", - "\n", - "\n", - "x\n", - "\n", - "\n", - "product\n", - "\n", - "\n", - "y\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.a:e->quadratic_0.multiply_1:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.b:e->quadratic_0.multiply_2:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.add_1\n", - "\n", - "add_1\n", - "__main__.add\n", - "\n", - "\n", - "a\n", - "\n", - "\n", - "result\n", - "\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.inputs.c:e->quadratic_0.add_1:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.outputs.result\n", - "\n", - "result\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.outputs.result:e->outputs.quad:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.multiply_0:e->quadratic_0.multiply_1:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.add_0\n", - "\n", - "add_0\n", - "__main__.add\n", - "\n", - "\n", - "a\n", - "\n", - "\n", - "result\n", - "\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.multiply_1:e->quadratic_0.add_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.multiply_2:e->quadratic_0.add_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.add_0:e->quadratic_0.add_1:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "quadratic_0.add_1:e->quadratic_0.outputs.result:w\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], + "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nworkflow\n__main__.evaluate_both\n\n\ncluster_linear_0\n\nlinear_0\n__main__.linear\n\n\ncluster_quadratic_0\n\nquadratic_0\n__main__.quadratic\n\n\n\ninputs.x\n\nx\n\n\n\n\nlinear_0.inputs.x\n\nx\n\n\n\n\ninputs.x:e->linear_0.inputs.x:w\n\n\n\n\n\nquadratic_0.inputs.x\n\nx\n\n\n\n\ninputs.x:e->quadratic_0.inputs.x:w\n\n\n\n\n\ninputs.slope\n\nslope\n\n\n\n\nlinear_0.inputs.slope\n\nslope\n\n\n\n\ninputs.slope:e->linear_0.inputs.slope:w\n\n\n\n\n\ninputs.intercept\n\nintercept\n\n\n\n\nlinear_0.inputs.intercept\n\nintercept\n\n\n\n\ninputs.intercept:e->linear_0.inputs.intercept:w\n\n\n\n\n\ninputs.a\n\na\n\n\n\n\nquadratic_0.inputs.a\n\na\n\n\n\n\ninputs.a:e->quadratic_0.inputs.a:w\n\n\n\n\n\ninputs.b\n\nb\n\n\n\n\nquadratic_0.inputs.b\n\nb\n\n\n\n\ninputs.b:e->quadratic_0.inputs.b:w\n\n\n\n\n\ninputs.c\n\nc\n\n\n\n\nquadratic_0.inputs.c\n\nc\n\n\n\n\ninputs.c:e->quadratic_0.inputs.c:w\n\n\n\n\n\noutputs.lin\n\nlin\n\n\n\n\noutputs.quad\n\nquad\n\n\n\n\nlinear_0.multiply_0\n\nmultiply_0\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nlinear_0.inputs.x:e->linear_0.multiply_0:w\n\n\n\n\n\nlinear_0.inputs.slope:e->linear_0.multiply_0:w\n\n\n\n\n\nlinear_0.add_0\n\nadd_0\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\nlinear_0.inputs.intercept:e->linear_0.add_0:w\n\n\n\n\n\nlinear_0.outputs.result\n\nresult\n\n\n\n\nlinear_0.outputs.result:e->outputs.lin:w\n\n\n\n\n\nlinear_0.multiply_0:e->linear_0.add_0:w\n\n\n\n\n\nlinear_0.add_0:e->linear_0.outputs.result:w\n\n\n\n\n\nquadratic_0.multiply_0\n\nmultiply_0\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nquadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n\n\n\n\n\nquadratic_0.inputs.x:e->quadratic_0.multiply_0:w\n\n\n\n\n\nquadratic_0.multiply_2\n\nmultiply_2\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nquadratic_0.inputs.x:e->quadratic_0.multiply_2:w\n\n\n\n\n\nquadratic_0.multiply_1\n\nmultiply_1\n__main__.multiply\n\n\nx\n\n\nproduct\n\n\ny\n\n\n\n\nquadratic_0.inputs.a:e->quadratic_0.multiply_1:w\n\n\n\n\n\nquadratic_0.inputs.b:e->quadratic_0.multiply_2:w\n\n\n\n\n\nquadratic_0.add_1\n\nadd_1\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\nquadratic_0.inputs.c:e->quadratic_0.add_1:w\n\n\n\n\n\nquadratic_0.outputs.result\n\nresult\n\n\n\n\nquadratic_0.outputs.result:e->outputs.quad:w\n\n\n\n\n\nquadratic_0.multiply_0:e->quadratic_0.multiply_1:w\n\n\n\n\n\nquadratic_0.add_0\n\nadd_0\n__main__.add\n\n\na\n\n\nresult\n\n\nb\n\n\n\n\nquadratic_0.multiply_1:e->quadratic_0.add_0:w\n\n\n\n\n\nquadratic_0.multiply_2:e->quadratic_0.add_0:w\n\n\n\n\n\nquadratic_0.add_0:e->quadratic_0.add_1:w\n\n\n\n\n\nquadratic_0.add_1:e->quadratic_0.outputs.result:w\n\n\n\n\n\n", "text/plain": [ - "" + "" ] }, "execution_count": 27, @@ -2254,10 +1555,7 @@ "output_type": "execute_result" } ], - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "recipe.draw(depth=1)" - ] + "execution_count": 27 }, { "cell_type": "markdown", @@ -2274,31 +1572,19 @@ }, { "cell_type": "code", - "execution_count": 28, "id": "55716aa9", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.232809Z", - "start_time": "2026-09-08T00:49:24.219164Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.266140Z", "iopub.status.busy": "2026-09-07T17:47:04.266056Z", "iopub.status.idle": "2026-09-07T17:47:04.269810Z", "shell.execute_reply": "2026-09-07T17:47:04.269414Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.048867Z", + "start_time": "2026-09-08T02:56:47.034149Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inputs: ['a', 'b', 'tag']\n", - "Outputs: ['result', 'tag']\n", - "Output edges: {OutputTarget(node=None, port='result'): SourceHandle(node='add_0', port='result'), OutputTarget(node=None, port='tag'): InputSource(node=None, port='tag')}\n" - ] - } - ], "source": [ "@fr.workflow\n", "def add_and_passthrough(a, b, tag):\n", @@ -2310,7 +1596,19 @@ "print(f\"Inputs: {recipe.inputs}\")\n", "print(f\"Outputs: {recipe.outputs}\")\n", "print(f\"Output edges: {recipe.output_edges}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inputs: ['a', 'b', 'tag']\n", + "Outputs: ['result', 'tag']\n", + "Output edges: {OutputTarget(node=None, port='result'): SourceHandle(node='add_0', port='result'), OutputTarget(node=None, port='tag'): InputSource(node=None, port='tag')}\n" + ] + } + ], + "execution_count": 28 }, { "cell_type": "markdown", @@ -2338,20 +1636,29 @@ }, { "cell_type": "code", - "execution_count": 29, "id": "3c9e6f41a81f5187", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.250998Z", - "start_time": "2026-09-08T00:49:24.241200Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.270542Z", "iopub.status.busy": "2026-09-07T17:47:04.270467Z", "iopub.status.idle": "2026-09-07T17:47:04.273717Z", "shell.execute_reply": "2026-09-07T17:47:04.273316Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.087537Z", + "start_time": "2026-09-08T02:56:47.060112Z" } }, + "source": [ + "@fr.workflow\n", + "def kinetic_energy(mass, velocity):\n", + " v_sq = multiply(velocity, velocity)\n", + " mv_sq = multiply(mass, v_sq)\n", + " kinetic_energy = multiply(0.5, mv_sq)\n", + " return kinetic_energy\n", + "\n", + "kinetic_energy(mass=1, velocity=2)" + ], "outputs": [ { "data": { @@ -2364,16 +1671,7 @@ "output_type": "execute_result" } ], - "source": [ - "@fr.workflow\n", - "def kinetic_energy(mass, velocity):\n", - " v_sq = multiply(velocity, velocity)\n", - " mv_sq = multiply(mass, v_sq)\n", - " kinetic_energy = multiply(0.5, mv_sq)\n", - " return kinetic_energy\n", - "\n", - "kinetic_energy(mass=1, velocity=2)" - ] + "execution_count": 29 }, { "cell_type": "markdown", @@ -2385,20 +1683,22 @@ }, { "cell_type": "code", - "execution_count": 30, "id": "964d6e961fbdc5d7", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.266388Z", - "start_time": "2026-09-08T00:49:24.259105Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.274326Z", "iopub.status.busy": "2026-09-07T17:47:04.274269Z", "iopub.status.idle": "2026-09-07T17:47:04.277949Z", "shell.execute_reply": "2026-09-07T17:47:04.277655Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.113186Z", + "start_time": "2026-09-08T02:56:47.101548Z" } }, + "source": [ + "kinetic_energy.flowrep_recipe.nodes[\"constant_0\"].model_dump(mode=\"json\")" + ], "outputs": [ { "data": { @@ -2415,9 +1715,7 @@ "output_type": "execute_result" } ], - "source": [ - "kinetic_energy.flowrep_recipe.nodes[\"constant_0\"].model_dump(mode=\"json\")" - ] + "execution_count": 30 }, { "cell_type": "markdown", @@ -2429,29 +1727,19 @@ }, { "cell_type": "code", - "execution_count": 31, "id": "f177977f81a9e15a", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.282612Z", - "start_time": "2026-09-08T00:49:24.274482Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.278675Z", "iopub.status.busy": "2026-09-07T17:47:04.278615Z", "iopub.status.idle": "2026-09-07T17:47:04.281804Z", "shell.execute_reply": "2026-09-07T17:47:04.281437Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.139166Z", + "start_time": "2026-09-08T02:56:47.123269Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Workflow python definitions accept constants only as call arguments (e.g. f(x, 5)); a bare literal assignment like 'some_var = 5' is no longer supported.\n" - ] - } - ], "source": [ "try:\n", " @fr.workflow\n", @@ -2463,7 +1751,17 @@ " return kinetic_energy\n", "except ValueError as e:\n", " print(e)" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Workflow python definitions accept constants only as call arguments (e.g. f(x, 5)); a bare literal assignment like 'some_var = 5' is no longer supported.\n" + ] + } + ], + "execution_count": 31 }, { "cell_type": "markdown", @@ -2477,32 +1775,19 @@ }, { "cell_type": "code", - "execution_count": 32, "id": "a72ef46c57a9d311", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.299233Z", - "start_time": "2026-09-08T00:49:24.290393Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.282425Z", "iopub.status.busy": "2026-09-07T17:47:04.282367Z", "iopub.status.idle": "2026-09-07T17:47:04.285455Z", "shell.execute_reply": "2026-09-07T17:47:04.285148Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.179525Z", + "start_time": "2026-09-08T02:56:47.151321Z" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "{'scaled': OutputDataPort(value=8, annotation=None)}" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "@fr.atomic\n", "class SomeDataStructure:\n", @@ -2518,7 +1803,20 @@ " return scaled\n", "\n", "fr.tools.run_recipe(stdlib_workflow.flowrep_recipe, x=2, y=3, scale=4).output_ports" - ] + ], + "outputs": [ + { + "data": { + "text/plain": [ + "{'scaled': OutputDataPort(value=8, annotation=None)}" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 32 }, { "cell_type": "markdown", @@ -2533,21 +1831,19 @@ }, { "cell_type": "code", - "execution_count": 33, "id": "e092fad543b9bfb9", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.314173Z", - "start_time": "2026-09-08T00:49:24.307787Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.286101Z", "iopub.status.busy": "2026-09-07T17:47:04.286046Z", "iopub.status.idle": "2026-09-07T17:47:04.288807Z", "shell.execute_reply": "2026-09-07T17:47:04.288428Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.197088Z", + "start_time": "2026-09-08T02:56:47.180649Z" } }, - "outputs": [], "source": [ "@fr.workflow\n", "def my_workflow_from_recipes(\n", @@ -2560,7 +1856,9 @@ " this=this, that=that, the_other=numpy_arange\n", " )\n", " return output_dict" - ] + ], + "outputs": [], + "execution_count": 33 }, { "cell_type": "markdown", @@ -2573,20 +1871,24 @@ }, { "cell_type": "code", - "execution_count": 34, "id": "7e7106b145ffbdc", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.321110Z", - "start_time": "2026-09-08T00:49:24.315052Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.289393Z", "iopub.status.busy": "2026-09-07T17:47:04.289338Z", "iopub.status.idle": "2026-09-07T17:47:04.291662Z", "shell.execute_reply": "2026-09-07T17:47:04.291380Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.228473Z", + "start_time": "2026-09-08T02:56:47.206950Z" } }, + "source": [ + "child_labels = list(my_workflow_from_recipes.flowrep_recipe.nodes.keys())\n", + "assert(child_labels == ['arange_0', 'my_dict_maker_0'])\n", + "print(child_labels)" + ], "outputs": [ { "name": "stdout", @@ -2596,11 +1898,7 @@ ] } ], - "source": [ - "child_labels = list(my_workflow_from_recipes.flowrep_recipe.nodes.keys())\n", - "assert(child_labels == ['arange_0', 'my_dict_maker_0'])\n", - "print(child_labels)" - ] + "execution_count": 34 }, { "cell_type": "markdown", @@ -2627,20 +1925,22 @@ }, { "cell_type": "code", - "execution_count": 35, "id": "6a0c65a9c8b32b63", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.337902Z", - "start_time": "2026-09-08T00:49:24.328946Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.292282Z", "iopub.status.busy": "2026-09-07T17:47:04.292225Z", "iopub.status.idle": "2026-09-07T17:47:04.294651Z", "shell.execute_reply": "2026-09-07T17:47:04.294270Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.248136Z", + "start_time": "2026-09-08T02:56:47.229825Z" } }, + "source": [ + "my_workflow_from_recipes.flowrep_recipe(start=0, stop=3, step=2)" + ], "outputs": [ { "data": { @@ -2653,9 +1953,7 @@ "output_type": "execute_result" } ], - "source": [ - "my_workflow_from_recipes.flowrep_recipe(start=0, stop=3, step=2)" - ] + "execution_count": 35 }, { "cell_type": "markdown", @@ -2712,20 +2010,22 @@ }, { "cell_type": "code", - "execution_count": 36, "id": "f91eb49fe62486aa", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.354632Z", - "start_time": "2026-09-08T00:49:24.345586Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.295260Z", "iopub.status.busy": "2026-09-07T17:47:04.295203Z", "iopub.status.idle": "2026-09-07T17:47:04.297625Z", "shell.execute_reply": "2026-09-07T17:47:04.297250Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.317296Z", + "start_time": "2026-09-08T02:56:47.267872Z" } }, + "source": [ + "fr.schemas.InputSource(port=\"inp\").node, fr.schemas.OutputTarget(port=\"out\").node" + ], "outputs": [ { "data": { @@ -2738,9 +2038,7 @@ "output_type": "execute_result" } ], - "source": [ - "fr.schemas.InputSource(port=\"inp\").node, fr.schemas.OutputTarget(port=\"out\").node" - ] + "execution_count": 36 }, { "cell_type": "markdown", @@ -2752,20 +2050,34 @@ }, { "cell_type": "code", - "execution_count": 37, "id": "7144a6706b877f78", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.370898Z", - "start_time": "2026-09-08T00:49:24.362493Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.298212Z", "iopub.status.busy": "2026-09-07T17:47:04.298158Z", "iopub.status.idle": "2026-09-07T17:47:04.302101Z", "shell.execute_reply": "2026-09-07T17:47:04.301742Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.368586Z", + "start_time": "2026-09-08T02:56:47.319179Z" } }, + "source": [ + "recipe = linear.flowrep_recipe\n", + "\n", + "print(\"Input edges (parent input → child input):\")\n", + "for target, source in recipe.input_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")\n", + "\n", + "print(\"\\nSibling edges (child → child):\")\n", + "for target, source in recipe.edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")\n", + "\n", + "print(\"\\nOutput edges (child → parent output):\")\n", + "for target, source in recipe.output_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")" + ], "outputs": [ { "name": "stdout", @@ -2784,21 +2096,7 @@ ] } ], - "source": [ - "recipe = linear.flowrep_recipe\n", - "\n", - "print(\"Input edges (parent input → child input):\")\n", - "for target, source in recipe.input_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")\n", - "\n", - "print(\"\\nSibling edges (child → child):\")\n", - "for target, source in recipe.edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")\n", - "\n", - "print(\"\\nOutput edges (child → parent output):\")\n", - "for target, source in recipe.output_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")" - ] + "execution_count": 37 }, { "cell_type": "markdown", @@ -2819,20 +2117,24 @@ }, { "cell_type": "code", - "execution_count": 38, "id": "13f799b9", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.388114Z", - "start_time": "2026-09-08T00:49:24.379120Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.302680Z", "iopub.status.busy": "2026-09-07T17:47:04.302626Z", "iopub.status.idle": "2026-09-07T17:47:04.306054Z", "shell.execute_reply": "2026-09-07T17:47:04.305708Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.419404Z", + "start_time": "2026-09-08T02:56:47.370490Z" } }, + "source": [ + "import json\n", + "\n", + "print(json.dumps(json.loads(recipe.model_dump_json()), indent=2))" + ], "outputs": [ { "name": "stdout", @@ -2915,11 +2217,7 @@ ] } ], - "source": [ - "import json\n", - "\n", - "print(json.dumps(json.loads(recipe.model_dump_json()), indent=2))" - ] + "execution_count": 38 }, { "cell_type": "markdown", @@ -2938,20 +2236,26 @@ }, { "cell_type": "code", - "execution_count": 39, "id": "e67cac8d", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.405113Z", - "start_time": "2026-09-08T00:49:24.396613Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.306626Z", "iopub.status.busy": "2026-09-07T17:47:04.306572Z", "iopub.status.idle": "2026-09-07T17:47:04.309966Z", "shell.execute_reply": "2026-09-07T17:47:04.309571Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.469424Z", + "start_time": "2026-09-08T02:56:47.421213Z" } }, + "source": [ + "# Round-trip demo\n", + "json_str = recipe.model_dump_json()\n", + "reconstructed = fr.schemas.WorkflowRecipe.model_validate_json(json_str)\n", + "assert reconstructed == recipe\n", + "print(\"Round-trip successful!\")" + ], "outputs": [ { "name": "stdout", @@ -2961,13 +2265,7 @@ ] } ], - "source": [ - "# Round-trip demo\n", - "json_str = recipe.model_dump_json()\n", - "reconstructed = fr.schemas.WorkflowRecipe.model_validate_json(json_str)\n", - "assert reconstructed == recipe\n", - "print(\"Round-trip successful!\")" - ] + "execution_count": 39 }, { "cell_type": "markdown", @@ -3002,31 +2300,19 @@ }, { "cell_type": "code", - "execution_count": 40, "id": "52f01feb2783079d", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.423440Z", - "start_time": "2026-09-08T00:49:24.413257Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.310581Z", "iopub.status.busy": "2026-09-07T17:47:04.310522Z", "iopub.status.idle": "2026-09-07T17:47:04.314901Z", "shell.execute_reply": "2026-09-07T17:47:04.314519Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.527301Z", + "start_time": "2026-09-08T02:56:47.470687Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "abs_value(-3) = 3\n", - "abs_value(5) = 5\n", - "Recipe nodes: ['if_0']\n" - ] - } - ], "source": [ "# A preview — we'll dissect each type below\n", "def less_than(a, b):\n", @@ -3051,7 +2337,19 @@ "print(f\"abs_value(-3) = {abs_value(-3)}\")\n", "print(f\"abs_value(5) = {abs_value(5)}\")\n", "print(f\"Recipe nodes: {list(abs_value.flowrep_recipe.nodes.keys())}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "abs_value(-3) = 3\n", + "abs_value(5) = 5\n", + "Recipe nodes: ['if_0']\n" + ] + } + ], + "execution_count": 40 }, { "cell_type": "markdown", @@ -3177,37 +2475,19 @@ }, { "cell_type": "code", - "execution_count": 41, "id": "bf71d040c5bb5835", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.440071Z", - "start_time": "2026-09-08T00:49:24.431292Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.315604Z", "iopub.status.busy": "2026-09-07T17:47:04.315541Z", "iopub.status.idle": "2026-09-07T17:47:04.320155Z", "shell.execute_reply": "2026-09-07T17:47:04.319736Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.545636Z", + "start_time": "2026-09-08T02:56:47.529290Z" } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "double_all([1,2,3]) = [2, 4, 6]\n", - "\n", - "ForEach-node type: for_each\n", - "Inputs: ['items']\n", - "Outputs: ['results']\n", - "Nested ports: ['item']\n", - "Zipped ports: []\n", - "Input edges: {TargetHandle(node='body', port='item'): InputSource(node=None, port='items')}\n", - "Body node: body -> WorkflowRecipe\n" - ] - } - ], + }, "source": [ "@fr.workflow\n", "def double_all(items):\n", @@ -3231,7 +2511,25 @@ "print(\n", " f\"Body node: {for_node.body_node.label} -> {type(for_node.body_node.recipe).__name__}\"\n", ")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "double_all([1,2,3]) = [2, 4, 6]\n", + "\n", + "ForEach-node type: for_each\n", + "Inputs: ['items']\n", + "Outputs: ['results']\n", + "Nested ports: ['item']\n", + "Zipped ports: []\n", + "Input edges: {TargetHandle(node='body', port='item'): InputSource(node=None, port='items')}\n", + "Body node: body -> WorkflowRecipe\n" + ] + } + ], + "execution_count": 41 }, { "cell_type": "markdown", @@ -3255,136 +2553,29 @@ }, { "cell_type": "code", - "execution_count": 42, "id": "81cce7c2fbd6a1f1", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.619656Z", - "start_time": "2026-09-08T00:49:24.447628Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.320857Z", "iopub.status.busy": "2026-09-07T17:47:04.320790Z", "iopub.status.idle": "2026-09-07T17:47:04.331825Z", "shell.execute_reply": "2026-09-07T17:47:04.331416Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.714905Z", + "start_time": "2026-09-08T02:56:47.556276Z" } }, + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "for_node.draw()" + ], "outputs": [ { "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cluster_\n", - "\n", - "for_each\n", - "\n", - "\n", - "cluster_body\n", - "\n", - "body_n\n", - "\n", - "\n", - "\n", - "inputs.items\n", - "\n", - "items\n", - "\n", - "\n", - "\n", - "\n", - "body.inputs.item\n", - "\n", - "item\n", - "nested\n", - "\n", - "\n", - "\n", - "\n", - "inputs.items:e->body.inputs.item:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "outputs.results\n", - "\n", - "results\n", - "\n", - "\n", - "\n", - "\n", - "body.mul_0\n", - "\n", - "mul_0\n", - "flowrep.std.mul\n", - "\n", - "\n", - "a\n", - "\n", - "\n", - "product\n", - "\n", - "\n", - "b\n", - "\n", - "\n", - "\n", - "\n", - "body.inputs.item:e->body.mul_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body.outputs.doubled\n", - "\n", - "doubled\n", - "\n", - "\n", - "\n", - "\n", - "body.outputs.doubled:e->outputs.results:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body.mul_0:e->body.outputs.doubled:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body.constant_0\n", - "\n", - "constant_0\n", - "2\n", - "\n", - "\n", - "constant\n", - "\n", - "\n", - "\n", - "\n", - "body.constant_0:e->body.mul_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], + "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nfor_each\n\n\ncluster_body\n\nbody_n\n\n\n\ninputs.items\n\nitems\n\n\n\n\nbody.inputs.item\n\nitem\nnested\n\n\n\n\ninputs.items:e->body.inputs.item:w\n\n\n\n\n\noutputs.results\n\nresults\n\n\n\n\nbody.mul_0\n\nmul_0\nflowrep.std.mul\n\n\na\n\n\nproduct\n\n\nb\n\n\n\n\nbody.inputs.item:e->body.mul_0:w\n\n\n\n\n\nbody.outputs.doubled\n\ndoubled\n\n\n\n\nbody.outputs.doubled:e->outputs.results:w\n\n\n\n\n\nbody.mul_0:e->body.outputs.doubled:w\n\n\n\n\n\nbody.constant_0\n\nconstant_0\n2\n\n\nconstant\n\n\n\n\nbody.constant_0:e->body.mul_0:w\n\n\n\n\n\n", "text/plain": [ - "" + "" ] }, "execution_count": 42, @@ -3392,10 +2583,7 @@ "output_type": "execute_result" } ], - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "for_node.draw()" - ] + "execution_count": 42 }, { "cell_type": "markdown", @@ -3412,33 +2600,19 @@ }, { "cell_type": "code", - "execution_count": 43, "id": "b74048bfcfa45a5c", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.695899Z", - "start_time": "2026-09-08T00:49:24.656627Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.332503Z", "iopub.status.busy": "2026-09-07T17:47:04.332439Z", "iopub.status.idle": "2026-09-07T17:47:04.335615Z", "shell.execute_reply": "2026-09-07T17:47:04.335305Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.751667Z", + "start_time": "2026-09-08T02:56:47.729939Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inputs: ['factor', 'items']\n", - "Nested ports (scattered): ['item']\n", - "Input edges:\n", - " factor → body.factor\n", - " items → body.item\n" - ] - } - ], "source": [ "@fr.workflow\n", "def scale_all(items, factor):\n", @@ -3455,7 +2629,21 @@ "print(\"Input edges:\")\n", "for target, source in for_node.input_edges.items():\n", " print(f\" {source.serialize()} → {target.serialize()}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inputs: ['factor', 'items']\n", + "Nested ports (scattered): ['item']\n", + "Input edges:\n", + " factor → body.factor\n", + " items → body.item\n" + ] + } + ], + "execution_count": 43 }, { "cell_type": "markdown", @@ -3481,30 +2669,19 @@ }, { "cell_type": "code", - "execution_count": 44, "id": "fb336f6a620be411", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.730154Z", - "start_time": "2026-09-08T00:49:24.711628Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.336356Z", "iopub.status.busy": "2026-09-07T17:47:04.336285Z", "iopub.status.idle": "2026-09-07T17:47:04.339817Z", "shell.execute_reply": "2026-09-07T17:47:04.339547Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.779180Z", + "start_time": "2026-09-08T02:56:47.763417Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Zipped ports: ['x', 'y']\n", - "Nested ports: []\n" - ] - } - ], "source": [ "@fr.workflow\n", "def add_pairwise(xs, ys):\n", @@ -3518,7 +2695,18 @@ "for_node = add_pairwise.flowrep_recipe.nodes[\"for_each_0\"]\n", "print(f\"Zipped ports: {for_node.zipped_ports}\")\n", "print(f\"Nested ports: {for_node.nested_ports}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Zipped ports: ['x', 'y']\n", + "Nested ports: []\n" + ] + } + ], + "execution_count": 44 }, { "cell_type": "markdown", @@ -3533,31 +2721,19 @@ }, { "cell_type": "code", - "execution_count": 45, "id": "bc49c138cf8fd040", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.782105Z", - "start_time": "2026-09-08T00:49:24.745839Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.340518Z", "iopub.status.busy": "2026-09-07T17:47:04.340460Z", "iopub.status.idle": "2026-09-07T17:47:04.343728Z", "shell.execute_reply": "2026-09-07T17:47:04.343342Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.813222Z", + "start_time": "2026-09-08T02:56:47.787487Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Outputs: ['results', 'sources']\n", - "Transferred (input-forwarded) outputs:\n", - " sources sourced from input 'items'\n" - ] - } - ], "source": [ "@fr.workflow\n", "def process_and_track(items):\n", @@ -3575,7 +2751,19 @@ "print(f\"Transferred (input-forwarded) outputs:\")\n", "for target, source in for_node.transferred_outputs.items():\n", " print(f\" {target.serialize()} sourced from input '{source.port}'\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outputs: ['results', 'sources']\n", + "Transferred (input-forwarded) outputs:\n", + " sources sourced from input 'items'\n" + ] + } + ], + "execution_count": 45 }, { "cell_type": "markdown", @@ -3631,38 +2819,19 @@ }, { "cell_type": "code", - "execution_count": 46, "id": "b1ae44ab3952db8b", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.818984Z", - "start_time": "2026-09-08T00:49:24.799031Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.344334Z", "iopub.status.busy": "2026-09-07T17:47:04.344278Z", "iopub.status.idle": "2026-09-07T17:47:04.347569Z", "shell.execute_reply": "2026-09-07T17:47:04.347309Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.852611Z", + "start_time": "2026-09-08T02:56:47.823242Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Type: if\n", - "Inputs: ['x']\n", - "Outputs: ['result']\n", - "Number of cases: 1\n", - "Has else: True\n", - "\n", - "Prospective output edges:\n", - " result can come from:\n", - " - body_0.result\n", - " - else_body.result\n" - ] - } - ], "source": [ "@fr.workflow\n", "def sign(x):\n", @@ -3685,7 +2854,26 @@ " print(f\" {target.serialize()} can come from:\")\n", " for s in sources:\n", " print(f\" - {s.serialize()}\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Type: if\n", + "Inputs: ['x']\n", + "Outputs: ['result']\n", + "Number of cases: 1\n", + "Has else: True\n", + "\n", + "Prospective output edges:\n", + " result can come from:\n", + " - body_0.result\n", + " - else_body.result\n" + ] + } + ], + "execution_count": 46 }, { "cell_type": "markdown", @@ -3737,20 +2925,39 @@ }, { "cell_type": "code", - "execution_count": 47, "id": "14cd83686b680b", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.869943Z", - "start_time": "2026-09-08T00:49:24.834194Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.348353Z", "iopub.status.busy": "2026-09-07T17:47:04.348298Z", "iopub.status.idle": "2026-09-07T17:47:04.351508Z", "shell.execute_reply": "2026-09-07T17:47:04.351258Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.904590Z", + "start_time": "2026-09-08T02:56:47.868243Z" } }, + "source": [ + "# Aside example\n", + "import traceback\n", + "\n", + "# Parses acceptably\n", + "@fr.workflow\n", + "def conditional_availability(x):\n", + " if is_positive(x):\n", + " result = fr.std.identity(x)\n", + " downstream = fr.std.identity(result)\n", + " return downstream\n", + "\n", + "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].outputs)\n", + "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].prospective_output_edges)\n", + "\n", + "try:\n", + " conditional_availability(-1)\n", + "except Exception:\n", + " traceback.print_exc()" + ], "outputs": [ { "name": "stdout", @@ -3765,36 +2972,16 @@ "output_type": "stream", "text": [ "Traceback (most recent call last):\n", - " File \"/tmp/ipykernel_51635/3523357707.py\", line 16, in \n", + " File \"/var/folders/nn/6kd6nhmj2rx7610kd9r_8h0m0000gn/T/ipykernel_19769/3523357707.py\", line 16, in \n", " conditional_availability(-1)\n", - " ~~~~~~~~~~~~~~~~~~~~~~~~^^^^\n", - " File \"/tmp/ipykernel_51635/3523357707.py\", line 9, in conditional_availability\n", + " File \"/var/folders/nn/6kd6nhmj2rx7610kd9r_8h0m0000gn/T/ipykernel_19769/3523357707.py\", line 9, in conditional_availability\n", " downstream = fr.std.identity(result)\n", " ^^^^^^\n", "UnboundLocalError: cannot access local variable 'result' where it is not associated with a value\n" ] } ], - "source": [ - "# Aside example\n", - "import traceback\n", - "\n", - "# Parses acceptably\n", - "@fr.workflow\n", - "def conditional_availability(x):\n", - " if is_positive(x):\n", - " result = fr.std.identity(x)\n", - " downstream = fr.std.identity(result)\n", - " return downstream\n", - "\n", - "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].outputs)\n", - "print(conditional_availability.flowrep_recipe.nodes[\"if_0\"].prospective_output_edges)\n", - "\n", - "try:\n", - " conditional_availability(-1)\n", - "except Exception:\n", - " traceback.print_exc()" - ] + "execution_count": 47 }, { "cell_type": "markdown", @@ -3821,21 +3008,19 @@ }, { "cell_type": "code", - "execution_count": 48, "id": "f170e946bc3d3e53", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.904725Z", - "start_time": "2026-09-08T00:49:24.885947Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.352215Z", "iopub.status.busy": "2026-09-07T17:47:04.352159Z", "iopub.status.idle": "2026-09-07T17:47:04.354682Z", "shell.execute_reply": "2026-09-07T17:47:04.354396Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.954633Z", + "start_time": "2026-09-08T02:56:47.933573Z" } }, - "outputs": [], "source": [ "@fr.atomic\n", "def is_below_threshold(value, threshold):\n", @@ -3847,7 +3032,9 @@ "def increment(x):\n", " result = x + 1\n", " return result" - ] + ], + "outputs": [], + "execution_count": 48 }, { "cell_type": "markdown", @@ -3877,36 +3064,20 @@ }, { "cell_type": "code", - "execution_count": 49, "id": "460686cf", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.937635Z", - "start_time": "2026-09-08T00:49:24.917364Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.355360Z", "iopub.status.busy": "2026-09-07T17:47:04.355293Z", "iopub.status.idle": "2026-09-07T17:47:04.358457Z", "shell.execute_reply": "2026-09-07T17:47:04.358187Z" }, - "lines_to_next_cell": 2 - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "count_up(0, 5) = 5\n", - "\n", - "Type: while\n", - "Inputs: ['start', 'threshold']\n", - "Outputs: ['start']\n", - "Condition: condition\n", - "Body: body\n" - ] + "lines_to_next_cell": 2, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:47.989228Z", + "start_time": "2026-09-08T02:56:47.969466Z" } - ], + }, "source": [ "@fr.workflow\n", "def count_up(start, threshold):\n", @@ -3925,25 +3096,49 @@ "print(f\"Body: {while_node.case.body.label}\")\n", "#\n", "# The while-node also exposes inferred iteration edges:" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "count_up(0, 5) = 5\n", + "\n", + "Type: while\n", + "Inputs: ['start', 'threshold']\n", + "Outputs: ['start']\n", + "Condition: condition\n", + "Body: body\n" + ] + } + ], + "execution_count": 49 }, { "cell_type": "code", - "execution_count": 50, "id": "515eae1c", - "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:24.972230Z", - "start_time": "2026-09-08T00:49:24.951542Z" - }, + "metadata": { "execution": { "iopub.execute_input": "2026-09-07T17:47:04.359285Z", "iopub.status.busy": "2026-09-07T17:47:04.359222Z", "iopub.status.idle": "2026-09-07T17:47:04.361620Z", "shell.execute_reply": "2026-09-07T17:47:04.361287Z" }, - "lines_to_next_cell": 2 + "lines_to_next_cell": 2, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.039889Z", + "start_time": "2026-09-08T02:56:47.989915Z" + } }, + "source": [ + "print(\"Body → body (next iteration) edges:\")\n", + "for target, source in while_node.body_body_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")\n", + "\n", + "print(\"\\nBody → condition (next check) edges:\")\n", + "for target, source in while_node.body_condition_edges.items():\n", + " print(f\" {source.serialize()} → {target.serialize()}\")" + ], "outputs": [ { "name": "stdout", @@ -3957,15 +3152,7 @@ ] } ], - "source": [ - "print(\"Body → body (next iteration) edges:\")\n", - "for target, source in while_node.body_body_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")\n", - "\n", - "print(\"\\nBody → condition (next check) edges:\")\n", - "for target, source in while_node.body_condition_edges.items():\n", - " print(f\" {source.serialize()} → {target.serialize()}\")" - ] + "execution_count": 50 }, { "cell_type": "markdown", @@ -4015,37 +3202,19 @@ }, { "cell_type": "code", - "execution_count": 51, "id": "4cbe44877e1af10e", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.097055Z", - "start_time": "2026-09-08T00:49:24.986713Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.362292Z", "iopub.status.busy": "2026-09-07T17:47:04.362231Z", "iopub.status.idle": "2026-09-07T17:47:04.366828Z", "shell.execute_reply": "2026-09-07T17:47:04.366397Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.060974Z", + "start_time": "2026-09-08T02:56:48.046657Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "careful_divide(10, 2) = 5.0\n", - "careful_divide(10, 0) = 0\n", - "\n", - "Type: try\n", - "Inputs: ['a', 'b']\n", - "Outputs: ['result']\n", - "Try body: try_body\n", - "Exception cases: 1\n", - " [0] catches ['builtins.ZeroDivisionError'] → body 'except_body_0'\n" - ] - } - ], "source": [ "@fr.atomic\n", "def zero():\n", @@ -4074,7 +3243,25 @@ "for i, case in enumerate(try_node.exception_cases):\n", " exc_names = [e.fully_qualified_name for e in case.exceptions]\n", " print(f\" [{i}] catches {exc_names} → body '{case.body.label}'\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "careful_divide(10, 2) = 5.0\n", + "careful_divide(10, 0) = 0\n", + "\n", + "Type: try\n", + "Inputs: ['a', 'b']\n", + "Outputs: ['result']\n", + "Try body: try_body\n", + "Exception cases: 1\n", + " [0] catches ['builtins.ZeroDivisionError'] → body 'except_body_0'\n" + ] + } + ], + "execution_count": 51 }, { "cell_type": "markdown", @@ -4108,20 +3295,23 @@ }, { "cell_type": "code", - "execution_count": 52, "id": "d76836d6", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.170604Z", - "start_time": "2026-09-08T00:49:25.116830Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.367488Z", "iopub.status.busy": "2026-09-07T17:47:04.367424Z", "iopub.status.idle": "2026-09-07T17:47:04.369603Z", "shell.execute_reply": "2026-09-07T17:47:04.369266Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.086857Z", + "start_time": "2026-09-08T02:56:48.062227Z" } }, + "source": [ + "intermediate = count_up.flowrep_recipe.nodes[\"while_0\"].case.body\n", + "print(f\"The intermediate node {intermediate.label!r} is of type {str(intermediate.recipe.type)!r}\")" + ], "outputs": [ { "name": "stdout", @@ -4131,10 +3321,7 @@ ] } ], - "source": [ - "intermediate = count_up.flowrep_recipe.nodes[\"while_0\"].case.body\n", - "print(f\"The intermediate node {intermediate.label!r} is of type {str(intermediate.recipe.type)!r}\")" - ] + "execution_count": 52 }, { "cell_type": "markdown", @@ -4146,20 +3333,23 @@ }, { "cell_type": "code", - "execution_count": 53, "id": "880a7caa20cb48ec", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.214732Z", - "start_time": "2026-09-08T00:49:25.172797Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.370226Z", "iopub.status.busy": "2026-09-07T17:47:04.370162Z", "iopub.status.idle": "2026-09-07T17:47:04.372832Z", "shell.execute_reply": "2026-09-07T17:47:04.372456Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.104203Z", + "start_time": "2026-09-08T02:56:48.088119Z" } }, + "source": [ + "child_label, child_recipe = next(iter(intermediate.recipe.nodes.items()))\n", + "print(f\"This workflow has a single-node body: {child_label!r} of type {str(child_recipe.type)!r}\")" + ], "outputs": [ { "name": "stdout", @@ -4169,10 +3359,7 @@ ] } ], - "source": [ - "child_label, child_recipe = next(iter(intermediate.recipe.nodes.items()))\n", - "print(f\"This workflow has a single-node body: {child_label!r} of type {str(child_recipe.type)!r}\")" - ] + "execution_count": 53 }, { "cell_type": "markdown", @@ -4204,68 +3391,58 @@ }, { "cell_type": "code", - "execution_count": 54, "id": "257fa2cc1c78265d", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.253059Z", - "start_time": "2026-09-08T00:49:25.216523Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.373477Z", "iopub.status.busy": "2026-09-07T17:47:04.373411Z", "iopub.status.idle": "2026-09-07T17:47:04.377943Z", "shell.execute_reply": "2026-09-07T17:47:04.377537Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.128010Z", + "start_time": "2026-09-08T02:56:48.113327Z" } }, + "source": [ + "# This will fail because we're in __main__:\n", + "try:\n", + "\n", + " @fr.atomic(forbid_main=True)\n", + " def my_func(x):\n", + " y = x\n", + " return y\n", + "\n", + "except Exception:\n", + " traceback.print_exc()" + ], "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Traceback (most recent call last):\n", - " File \"/tmp/ipykernel_51635/3041652459.py\", line 4, in \n", + " File \"/var/folders/nn/6kd6nhmj2rx7610kd9r_8h0m0000gn/T/ipykernel_19769/3041652459.py\", line 4, in \n", " @fr.atomic(forbid_main=True)\n", - " ~~~~~~~~~^^^^^^^^^^^^^^^^^^\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", " File \"/Users/liamhuber/dev/pyiron/flowrep/src/flowrep/parsers/parser_helpers.py\", line 64, in deferred\n", " return wrap(f, labels)\n", + " ^^^^^^^^^^^^^^^\n", " File \"/Users/liamhuber/dev/pyiron/flowrep/src/flowrep/parsers/atomic_parser.py\", line 71, in wrap\n", " f.flowrep_recipe = parse_atomic(f, *labels, **kwargs)\n", - " ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", " File \"/Users/liamhuber/dev/pyiron/flowrep/src/flowrep/parsers/atomic_parser.py\", line 147, in parse_atomic\n", " function_info = versions.VersionInfo.of(\n", - " func,\n", - " ...<3 lines>...\n", - " require_version=require_version,\n", - " )\n", - " File \"/Users/liamhuber/dev/pyiron/flowrep/.venv/lib/python3.14/site-packages/pyiron_snippets/versions.py\", line 125, in of\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/liamhuber/dev/pyiron/pyiron_snippets/pyiron_snippets/versions.py\", line 125, in of\n", " info.validate_constraints(\n", - " ~~~~~~~~~~~~~~~~~~~~~~~~~^\n", - " forbid_main=forbid_main,\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^\n", - " ...<3 lines>...\n", - " strict=strict,\n", - " ^^^^^^^^^^^^^^\n", - " )\n", - " ^\n", - " File \"/Users/liamhuber/dev/pyiron/flowrep/.venv/lib/python3.14/site-packages/pyiron_snippets/versions.py\", line 143, in validate_constraints\n", + " File \"/Users/liamhuber/dev/pyiron/pyiron_snippets/pyiron_snippets/versions.py\", line 143, in validate_constraints\n", " raise ValueError(f\"Found forbidden module '__main__' in module for {self}\")\n", "ValueError: Found forbidden module '__main__' in module for VersionInfo(module='__main__', qualname='my_func', version=None)\n" ] } ], - "source": [ - "# This will fail because we're in __main__:\n", - "try:\n", - "\n", - " @fr.atomic(forbid_main=True)\n", - " def my_func(x):\n", - " y = x\n", - " return y\n", - "\n", - "except Exception:\n", - " traceback.print_exc()" - ] + "execution_count": 54 }, { "cell_type": "markdown", @@ -4302,20 +3479,27 @@ }, { "cell_type": "code", - "execution_count": 55, "id": "3efa3180", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.290347Z", - "start_time": "2026-09-08T00:49:25.254929Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.378601Z", "iopub.status.busy": "2026-09-07T17:47:04.378534Z", "iopub.status.idle": "2026-09-07T17:47:04.382029Z", "shell.execute_reply": "2026-09-07T17:47:04.381642Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.156724Z", + "start_time": "2026-09-08T02:56:48.136770Z" } }, + "source": [ + "# Hypothetical example for a package with non-standard versioning:\n", + "@fr.atomic(version_scraping={\"__main__\": lambda _: \"1.2.3\"})\n", + "def my_func(x):\n", + " return x\n", + "\n", + "print(my_func.flowrep_recipe.reference.info.version)" + ], "outputs": [ { "name": "stdout", @@ -4325,14 +3509,7 @@ ] } ], - "source": [ - "# Hypothetical example for a package with non-standard versioning:\n", - "@fr.atomic(version_scraping={\"__main__\": lambda _: \"1.2.3\"})\n", - "def my_func(x):\n", - " return x\n", - "\n", - "print(my_func.flowrep_recipe.reference.info.version)" - ] + "execution_count": 55 }, { "cell_type": "markdown", @@ -4349,29 +3526,19 @@ }, { "cell_type": "code", - "execution_count": 56, "id": "369c2717ee847047", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.308539Z", - "start_time": "2026-09-08T00:49:25.292292Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.382667Z", "iopub.status.busy": "2026-09-07T17:47:04.382604Z", "iopub.status.idle": "2026-09-07T17:47:04.385926Z", "shell.execute_reply": "2026-09-07T17:47:04.385533Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.178329Z", + "start_time": "2026-09-08T02:56:48.164977Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This recipe already has an underlying Python reference: info=VersionInfo(module='__main__', qualname='my_recipe', version=None) inputs_with_defaults=['y'] restricted_input_kinds={}\n" - ] - } - ], "source": [ "@fr.workflow\n", "def my_recipe(x: int, y: int = 4):\n", @@ -4382,7 +3549,17 @@ " fr.tools.flowrep2python(my_recipe.flowrep_recipe)\n", "except ValueError as e:\n", " print(e)" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This recipe already has an underlying Python reference: info=VersionInfo(module='__main__', qualname='my_recipe', version=None) inputs_with_defaults=['y'] restricted_input_kinds={}\n" + ] + } + ], + "execution_count": 56 }, { "cell_type": "markdown", @@ -4401,25 +3578,25 @@ }, { "cell_type": "code", - "execution_count": 57, "id": "e0ac6cf3086d983a", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.324399Z", - "start_time": "2026-09-08T00:49:25.309756Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.386567Z", "iopub.status.busy": "2026-09-07T17:47:04.386503Z", "iopub.status.idle": "2026-09-07T17:47:04.388630Z", "shell.execute_reply": "2026-09-07T17:47:04.388319Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.200708Z", + "start_time": "2026-09-08T02:56:48.179077Z" } }, - "outputs": [], "source": [ "unreferenced_recipe = my_recipe.flowrep_recipe.model_copy(update={\"reference\": None})\n", "rendered = fr.tools.flowrep2python(unreferenced_recipe)" - ] + ], + "outputs": [], + "execution_count": 57 }, { "cell_type": "markdown", @@ -4436,20 +3613,22 @@ }, { "cell_type": "code", - "execution_count": 58, "id": "f906e1e3d41e5b3b", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.364514Z", - "start_time": "2026-09-08T00:49:25.325354Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.389287Z", "iopub.status.busy": "2026-09-07T17:47:04.389226Z", "iopub.status.idle": "2026-09-07T17:47:04.391804Z", "shell.execute_reply": "2026-09-07T17:47:04.391427Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.245471Z", + "start_time": "2026-09-08T02:56:48.202572Z" } }, + "source": [ + "print(rendered.source)" + ], "outputs": [ { "name": "stdout", @@ -4470,9 +3649,7 @@ ] } ], - "source": [ - "print(rendered.source)" - ] + "execution_count": 58 }, { "cell_type": "markdown", @@ -4489,20 +3666,23 @@ }, { "cell_type": "code", - "execution_count": 59, "id": "612788a582d4680", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.401131Z", - "start_time": "2026-09-08T00:49:25.366200Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.392448Z", "iopub.status.busy": "2026-09-07T17:47:04.392382Z", "iopub.status.idle": "2026-09-07T17:47:04.395946Z", "shell.execute_reply": "2026-09-07T17:47:04.395514Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.260864Z", + "start_time": "2026-09-08T02:56:48.247360Z" } }, + "source": [ + "rebuilt = rendered.build()\n", + "rebuilt(1, 2)" + ], "outputs": [ { "data": { @@ -4515,10 +3695,7 @@ "output_type": "execute_result" } ], - "source": [ - "rebuilt = rendered.build()\n", - "rebuilt(1, 2)" - ] + "execution_count": 59 }, { "cell_type": "markdown", @@ -4537,20 +3714,34 @@ }, { "cell_type": "code", - "execution_count": 60, "id": "54362e50de84e98d", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.422673Z", - "start_time": "2026-09-08T00:49:25.402434Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.396659Z", "iopub.status.busy": "2026-09-07T17:47:04.396599Z", "iopub.status.idle": "2026-09-07T17:47:04.399906Z", "shell.execute_reply": "2026-09-07T17:47:04.399545Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.274964Z", + "start_time": "2026-09-08T02:56:48.261820Z" } }, + "source": [ + "import inspect\n", + "\n", + "sig = inspect.Signature([\n", + " inspect.Parameter(\"x\", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),\n", + " inspect.Parameter(\"y\", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=4, annotation=int),\n", + "])\n", + "\n", + "rendered_with_annotations = fr.tools.flowrep2python(\n", + " unreferenced_recipe,\n", + " function_name=\"my_function_with_annotations\",\n", + " signature=sig,\n", + ")\n", + "print(rendered_with_annotations.source)" + ], "outputs": [ { "name": "stdout", @@ -4571,21 +3762,7 @@ ] } ], - "source": [ - "import inspect\n", - "\n", - "sig = inspect.Signature([\n", - " inspect.Parameter(\"x\", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),\n", - " inspect.Parameter(\"y\", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=4, annotation=int),\n", - "])\n", - "\n", - "rendered_with_annotations = fr.tools.flowrep2python(\n", - " unreferenced_recipe,\n", - " function_name=\"my_function_with_annotations\",\n", - " signature=sig,\n", - ")\n", - "print(rendered_with_annotations.source)" - ] + "execution_count": 60 }, { "cell_type": "markdown", @@ -4599,45 +3776,24 @@ "source": [ "### 6.3 Complex signatures\n", "\n", - "Where the annotations and defaults have reprs that play nicely with `ast.literal_eval`, these signature items inline nicely into the source code. Where they are more complex, we use placeholders" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "281ef568be306e93", - "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.454244Z", - "start_time": "2026-09-08T00:49:25.434370Z" - }, - "execution": { - "iopub.execute_input": "2026-09-07T17:47:04.400566Z", - "iopub.status.busy": "2026-09-07T17:47:04.400504Z", - "iopub.status.idle": "2026-09-07T17:47:04.403904Z", - "shell.execute_reply": "2026-09-07T17:47:04.403529Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "from __future__ import annotations\n", - "\n", - "import flowrep\n", - "import flowrep.std\n", - "import typing\n", - "\n", - "@flowrep.workflow(\"total\")\n", - "def wont_dump(x: _ann_x, y: _ann_y = _default_y):\n", - " add = flowrep.std.add(x, y)\n", - " return add\n", - "\n", - "\n" - ] + "Where the annotations and defaults have reprs that play nicely with `ast.literal_eval`, these signature items inline nicely into the source code. Where they are more complex, we use placeholders" + ] + }, + { + "cell_type": "code", + "id": "281ef568be306e93", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T17:47:04.400566Z", + "iopub.status.busy": "2026-09-07T17:47:04.400504Z", + "iopub.status.idle": "2026-09-07T17:47:04.403904Z", + "shell.execute_reply": "2026-09-07T17:47:04.403529Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.288321Z", + "start_time": "2026-09-08T02:56:48.275973Z" } - ], + }, "source": [ "class MyCustomThing(int): ...\n", "\n", @@ -4661,7 +3817,28 @@ " signature=complex_sig,\n", ")\n", "print(complex_rendered.source)" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "from __future__ import annotations\n", + "\n", + "import flowrep\n", + "import flowrep.std\n", + "import typing\n", + "\n", + "@flowrep.workflow(\"total\")\n", + "def wont_dump(x: _ann_x, y: _ann_y = _default_y):\n", + " add = flowrep.std.add(x, y)\n", + " return add\n", + "\n", + "\n" + ] + } + ], + "execution_count": 61 }, { "cell_type": "markdown", @@ -4680,20 +3857,22 @@ }, { "cell_type": "code", - "execution_count": 62, "id": "730469f5b44b6dd8", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.497258Z", - "start_time": "2026-09-08T00:49:25.466554Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.404540Z", "iopub.status.busy": "2026-09-07T17:47:04.404479Z", "iopub.status.idle": "2026-09-07T17:47:04.408069Z", "shell.execute_reply": "2026-09-07T17:47:04.407717Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.302072Z", + "start_time": "2026-09-08T02:56:48.289287Z" } }, + "source": [ + "complex_rendered.namespace" + ], "outputs": [ { "data": { @@ -4708,9 +3887,7 @@ "output_type": "execute_result" } ], - "source": [ - "complex_rendered.namespace" - ] + "execution_count": 62 }, { "cell_type": "markdown", @@ -4727,20 +3904,23 @@ }, { "cell_type": "code", - "execution_count": 63, "id": "578b4f2ccc1ee02e", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.546887Z", - "start_time": "2026-09-08T00:49:25.511639Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.408650Z", "iopub.status.busy": "2026-09-07T17:47:04.408594Z", "iopub.status.idle": "2026-09-07T17:47:04.411697Z", "shell.execute_reply": "2026-09-07T17:47:04.411468Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.316176Z", + "start_time": "2026-09-08T02:56:48.303046Z" } }, + "source": [ + "rebuilt_complex = complex_rendered.build()\n", + "rebuilt(MyCustomThing(1), MyCustomThing(2))" + ], "outputs": [ { "data": { @@ -4753,10 +3933,7 @@ "output_type": "execute_result" } ], - "source": [ - "rebuilt_complex = complex_rendered.build()\n", - "rebuilt(MyCustomThing(1), MyCustomThing(2))" - ] + "execution_count": 63 }, { "cell_type": "markdown", @@ -4803,37 +3980,19 @@ }, { "cell_type": "code", - "execution_count": 64, "id": "e05fec796a1c063b", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.598236Z", - "start_time": "2026-09-08T00:49:25.549243Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.412493Z", "iopub.status.busy": "2026-09-07T17:47:04.412436Z", "iopub.status.idle": "2026-09-07T17:47:04.415846Z", "shell.execute_reply": "2026-09-07T17:47:04.415488Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.339636Z", + "start_time": "2026-09-08T02:56:48.326541Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "PWD version: 0.1.5\n", - "Nodes: 6\n", - " id=0, type=input, name=x\n", - " id=1, type=input, name=slope\n", - " id=2, type=input, name=intercept\n", - " id=3, type=output, name=result\n", - " id=4, type=function, value=__main__.multiply\n", - " id=5, type=function, value=__main__.add\n", - "Edges: 5\n" - ] - } - ], "source": [ "try:\n", "\n", @@ -4855,7 +4014,25 @@ "except ImportError:\n", " print(\"python-workflow-definition is not installed — skipping this section.\")\n", " print(\"Install with: pip install python-workflow-definition\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PWD version: 0.0.1\n", + "Nodes: 6\n", + " id=0, type=input, name=x\n", + " id=1, type=input, name=slope\n", + " id=2, type=input, name=intercept\n", + " id=3, type=output, name=result\n", + " id=4, type=function, value=__main__.multiply\n", + " id=5, type=function, value=__main__.add\n", + "Edges: 5\n" + ] + } + ], + "execution_count": 64 }, { "cell_type": "markdown", @@ -4867,32 +4044,19 @@ }, { "cell_type": "code", - "execution_count": 65, "id": "5ccd8c05", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.618360Z", - "start_time": "2026-09-08T00:49:25.600192Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.416472Z", "iopub.status.busy": "2026-09-07T17:47:04.416412Z", "iopub.status.idle": "2026-09-07T17:47:04.419865Z", "shell.execute_reply": "2026-09-07T17:47:04.419595Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.361353Z", + "start_time": "2026-09-08T02:56:48.347951Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Round-tripped inputs: ['x', 'slope', 'intercept']\n", - "Round-tripped outputs: ['result']\n", - "Round-tripped nodes: ['multiply_0', 'add_0']\n", - "Defaults: {'x': 0.0, 'slope': 1.0, 'intercept': 0.0}\n" - ] - } - ], "source": [ "try:\n", " # Round-trip: flowrep → PWD → flowrep\n", @@ -4909,7 +4073,20 @@ " print(f\"Defaults: {defaults}\")\n", "except ImportError:\n", " print(\"python-workflow-definition is not installed — skipping.\")" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Round-tripped inputs: ['x', 'slope', 'intercept']\n", + "Round-tripped outputs: ['result']\n", + "Round-tripped nodes: ['multiply_0', 'add_0']\n", + "Defaults: {'x': 0.0, 'slope': 1.0, 'intercept': 0.0}\n" + ] + } + ], + "execution_count": 65 }, { "cell_type": "markdown", @@ -4962,21 +4139,19 @@ }, { "cell_type": "code", - "execution_count": 66, "id": "2370bd6b", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.647477Z", - "start_time": "2026-09-08T00:49:25.631485Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.420550Z", "iopub.status.busy": "2026-09-07T17:47:04.420492Z", "iopub.status.idle": "2026-09-07T17:47:04.424988Z", "shell.execute_reply": "2026-09-07T17:47:04.424633Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.387066Z", + "start_time": "2026-09-08T02:56:48.370843Z" } }, - "outputs": [], "source": [ "@fr.atomic\n", "def rescale(value: int | float, factor: float = 1.0) -> float:\n", @@ -5003,45 +4178,49 @@ " to_scale = int_list(n)\n", " scaled = rescale_all(to_scale, scale)\n", " return scaled" - ] + ], + "outputs": [], + "execution_count": 66 }, { "cell_type": "code", - "execution_count": 67, "id": "833e6b0ceb72e917", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.673022Z", - "start_time": "2026-09-08T00:49:25.658428Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.425593Z", "iopub.status.busy": "2026-09-07T17:47:04.425533Z", "iopub.status.idle": "2026-09-07T17:47:04.427716Z", "shell.execute_reply": "2026-09-07T17:47:04.427333Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.409606Z", + "start_time": "2026-09-08T02:56:48.388018Z" } }, - "outputs": [], "source": [ "node_data = fr.tools.recipe2data(scaled_range.flowrep_recipe)" - ] + ], + "outputs": [], + "execution_count": 67 }, { "cell_type": "code", - "execution_count": 68, "id": "d8537a11cde237e4", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.709555Z", - "start_time": "2026-09-08T00:49:25.673728Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.428325Z", "iopub.status.busy": "2026-09-07T17:47:04.428268Z", "iopub.status.idle": "2026-09-07T17:47:04.430665Z", "shell.execute_reply": "2026-09-07T17:47:04.430298Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.442235Z", + "start_time": "2026-09-08T02:56:48.410141Z" } }, + "source": [ + "type(node_data.recipe)" + ], "outputs": [ { "data": { @@ -5054,9 +4233,7 @@ "output_type": "execute_result" } ], - "source": [ - "type(node_data.recipe)" - ] + "execution_count": 68 }, { "cell_type": "markdown", @@ -5076,20 +4253,30 @@ }, { "cell_type": "code", - "execution_count": 69, "id": "f044f5954e57507c", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.744949Z", - "start_time": "2026-09-08T00:49:25.711398Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.431367Z", "iopub.status.busy": "2026-09-07T17:47:04.431307Z", "iopub.status.idle": "2026-09-07T17:47:04.434935Z", "shell.execute_reply": "2026-09-07T17:47:04.434659Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.455085Z", + "start_time": "2026-09-08T02:56:48.443947Z" } }, + "source": [ + "def recursively_look_at_data_nodes(node: fr.schemas.NodeData, tabs=0):\n", + " for recipe_child_label in node.recipe.nodes:\n", + " live_child = node.nodes[recipe_child_label]\n", + " assert(isinstance(live_child, fr.schemas.NodeData))\n", + " print(f\"{tabs*'\\t'}{recipe_child_label}\")\n", + " if hasattr(live_child.recipe, \"nodes\"):\n", + " recursively_look_at_data_nodes(live_child, tabs+1)\n", + "\n", + "recursively_look_at_data_nodes(node_data)" + ], "outputs": [ { "name": "stdout", @@ -5101,17 +4288,7 @@ ] } ], - "source": [ - "def recursively_look_at_data_nodes(node: fr.schemas.NodeData, tabs=0):\n", - " for recipe_child_label in node.recipe.nodes:\n", - " live_child = node.nodes[recipe_child_label]\n", - " assert(isinstance(live_child, fr.schemas.NodeData))\n", - " print(f\"{tabs*'\\t'}{recipe_child_label}\")\n", - " if hasattr(live_child.recipe, \"nodes\"):\n", - " recursively_look_at_data_nodes(live_child, tabs+1)\n", - "\n", - "recursively_look_at_data_nodes(node_data)" - ] + "execution_count": 69 }, { "cell_type": "markdown", @@ -5137,20 +4314,22 @@ }, { "cell_type": "code", - "execution_count": 70, "id": "aec301f79141f523", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.765309Z", - "start_time": "2026-09-08T00:49:25.746891Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.435651Z", "iopub.status.busy": "2026-09-07T17:47:04.435594Z", "iopub.status.idle": "2026-09-07T17:47:04.438897Z", "shell.execute_reply": "2026-09-07T17:47:04.438623Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.477951Z", + "start_time": "2026-09-08T02:56:48.463469Z" } }, + "source": [ + "node_data.input_ports[\"n\"].value" + ], "outputs": [ { "data": { @@ -5163,9 +4342,7 @@ "output_type": "execute_result" } ], - "source": [ - "node_data.input_ports[\"n\"].value" - ] + "execution_count": 70 }, { "cell_type": "markdown", @@ -5177,20 +4354,22 @@ }, { "cell_type": "code", - "execution_count": 71, "id": "c9e8560bc82a6038", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.807722Z", - "start_time": "2026-09-08T00:49:25.777628Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.439550Z", "iopub.status.busy": "2026-09-07T17:47:04.439493Z", "iopub.status.idle": "2026-09-07T17:47:04.441381Z", "shell.execute_reply": "2026-09-07T17:47:04.441135Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.502495Z", + "start_time": "2026-09-08T02:56:48.478805Z" } }, + "source": [ + "node_data.input_ports[\"scale\"].default" + ], "outputs": [ { "data": { @@ -5203,9 +4382,7 @@ "output_type": "execute_result" } ], - "source": [ - "node_data.input_ports[\"scale\"].default" - ] + "execution_count": 71 }, { "cell_type": "markdown", @@ -5222,20 +4399,25 @@ }, { "cell_type": "code", - "execution_count": 72, "id": "6d43e80546840ed0", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.838530Z", - "start_time": "2026-09-08T00:49:25.823047Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.442098Z", "iopub.status.busy": "2026-09-07T17:47:04.442038Z", "iopub.status.idle": "2026-09-07T17:47:04.444716Z", "shell.execute_reply": "2026-09-07T17:47:04.444356Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.547062Z", + "start_time": "2026-09-08T02:56:48.514254Z" } }, + "source": [ + "(\n", + " node_data.input_ports[\"scale\"].annotation,\n", + " node_data.output_ports[\"scaled\"].annotation\n", + ")" + ], "outputs": [ { "data": { @@ -5248,12 +4430,7 @@ "output_type": "execute_result" } ], - "source": [ - "(\n", - " node_data.input_ports[\"scale\"].annotation,\n", - " node_data.output_ports[\"scaled\"].annotation\n", - ")" - ] + "execution_count": 72 }, { "cell_type": "markdown", @@ -5270,20 +4447,28 @@ }, { "cell_type": "code", - "execution_count": 73, "id": "c0c1dc9c6ca8e156", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.867642Z", - "start_time": "2026-09-08T00:49:25.849593Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.445338Z", "iopub.status.busy": "2026-09-07T17:47:04.445278Z", "iopub.status.idle": "2026-09-07T17:47:04.449045Z", "shell.execute_reply": "2026-09-07T17:47:04.448695Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.565098Z", + "start_time": "2026-09-08T02:56:48.548238Z" } }, + "source": [ + "(\n", + " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].value,\n", + " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].default,\n", + " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].annotation,\n", + " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].value,\n", + " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].annotation,\n", + ")" + ], "outputs": [ { "data": { @@ -5296,15 +4481,7 @@ "output_type": "execute_result" } ], - "source": [ - "(\n", - " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].value,\n", - " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].default,\n", - " node_data.nodes[\"int_list_0\"].input_ports[\"n\"].annotation,\n", - " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].value,\n", - " node_data.nodes[\"int_list_0\"].output_ports[\"output_0\"].annotation,\n", - ")" - ] + "execution_count": 73 }, { "cell_type": "markdown", @@ -5324,20 +4501,23 @@ }, { "cell_type": "code", - "execution_count": 74, "id": "29bb5cf64f1b2ea2", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.898443Z", - "start_time": "2026-09-08T00:49:25.879124Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.449665Z", "iopub.status.busy": "2026-09-07T17:47:04.449605Z", "iopub.status.idle": "2026-09-07T17:47:04.453045Z", "shell.execute_reply": "2026-09-07T17:47:04.452678Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.609987Z", + "start_time": "2026-09-08T02:56:48.574756Z" } }, + "source": [ + "for target, source in node_data.input_edges.items():\n", + " print(f\"{source.serialize()} → {target.serialize()}\")" + ], "outputs": [ { "name": "stdout", @@ -5348,27 +4528,27 @@ ] } ], - "source": [ - "for target, source in node_data.input_edges.items():\n", - " print(f\"{source.serialize()} → {target.serialize()}\")" - ] + "execution_count": 74 }, { "cell_type": "code", - "execution_count": 75, "id": "1468f7bd033bb028", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.926976Z", - "start_time": "2026-09-08T00:49:25.910261Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.453670Z", "iopub.status.busy": "2026-09-07T17:47:04.453610Z", "iopub.status.idle": "2026-09-07T17:47:04.456965Z", "shell.execute_reply": "2026-09-07T17:47:04.456630Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.628393Z", + "start_time": "2026-09-08T02:56:48.611092Z" } }, + "source": [ + "for target, source in node_data.edges.items():\n", + " print(f\"{source.serialize()} → {target.serialize()}\")" + ], "outputs": [ { "name": "stdout", @@ -5378,27 +4558,27 @@ ] } ], - "source": [ - "for target, source in node_data.edges.items():\n", - " print(f\"{source.serialize()} → {target.serialize()}\")" - ] + "execution_count": 75 }, { "cell_type": "code", - "execution_count": 76, "id": "df3500015b3a2f5a", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.946146Z", - "start_time": "2026-09-08T00:49:25.927642Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.457621Z", "iopub.status.busy": "2026-09-07T17:47:04.457556Z", "iopub.status.idle": "2026-09-07T17:47:04.461086Z", "shell.execute_reply": "2026-09-07T17:47:04.460725Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.653508Z", + "start_time": "2026-09-08T02:56:48.629413Z" } }, + "source": [ + "for target, source in node_data.output_edges.items():\n", + " print(f\"{source.serialize()} → {target.serialize()}\")" + ], "outputs": [ { "name": "stdout", @@ -5408,10 +4588,7 @@ ] } ], - "source": [ - "for target, source in node_data.output_edges.items():\n", - " print(f\"{source.serialize()} → {target.serialize()}\")" - ] + "execution_count": 76 }, { "cell_type": "markdown", @@ -5435,20 +4612,30 @@ }, { "cell_type": "code", - "execution_count": 77, "id": "8003b380315257cd", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:25.977517Z", - "start_time": "2026-09-08T00:49:25.961257Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.461719Z", "iopub.status.busy": "2026-09-07T17:47:04.461657Z", "iopub.status.idle": "2026-09-07T17:47:04.464870Z", "shell.execute_reply": "2026-09-07T17:47:04.464553Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.670914Z", + "start_time": "2026-09-08T02:56:48.654673Z" } }, + "source": [ + "for_data = node_data.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", + "\n", + "(\n", + " for_data.recipe.type,\n", + " for_data.nodes,\n", + " for_data.input_edges,\n", + " for_data.edges,\n", + " for_data.output_edges,\n", + ")" + ], "outputs": [ { "data": { @@ -5461,17 +4648,7 @@ "output_type": "execute_result" } ], - "source": [ - "for_data = node_data.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", - "\n", - "(\n", - " for_data.recipe.type,\n", - " for_data.nodes,\n", - " for_data.input_edges,\n", - " for_data.edges,\n", - " for_data.output_edges,\n", - ")" - ] + "execution_count": 77 }, { "cell_type": "markdown", @@ -5492,21 +4669,19 @@ }, { "cell_type": "code", - "execution_count": 78, "id": "cc1e5c958dd87720", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.005125Z", - "start_time": "2026-09-08T00:49:25.991627Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.465499Z", "iopub.status.busy": "2026-09-07T17:47:04.465437Z", "iopub.status.idle": "2026-09-07T17:47:04.469033Z", "shell.execute_reply": "2026-09-07T17:47:04.468753Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.707646Z", + "start_time": "2026-09-08T02:56:48.679595Z" } }, - "outputs": [], "source": [ "from __future__ import annotations\n", "\n", @@ -5518,7 +4693,9 @@ "@fr.atomic\n", "def get_blue(colors: SeabornColors) -> str:\n", " return colors.blue" - ] + ], + "outputs": [], + "execution_count": 78 }, { "cell_type": "markdown", @@ -5535,20 +4712,25 @@ }, { "cell_type": "code", - "execution_count": 79, "id": "5dd2fc6e39e728b4", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.044462Z", - "start_time": "2026-09-08T00:49:26.016285Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.469790Z", "iopub.status.busy": "2026-09-07T17:47:04.469729Z", "iopub.status.idle": "2026-09-07T17:47:04.473038Z", "shell.execute_reply": "2026-09-07T17:47:04.472631Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.756875Z", + "start_time": "2026-09-08T02:56:48.723533Z" } }, + "source": [ + "try:\n", + " fr.tools.recipe2data(get_blue.flowrep_recipe)\n", + "except NameError as e:\n", + " print(e)" + ], "outputs": [ { "name": "stdout", @@ -5558,12 +4740,7 @@ ] } ], - "source": [ - "try:\n", - " fr.tools.recipe2data(get_blue.flowrep_recipe)\n", - "except NameError as e:\n", - " print(e)" - ] + "execution_count": 79 }, { "cell_type": "markdown", @@ -5580,20 +4757,26 @@ }, { "cell_type": "code", - "execution_count": 80, "id": "36045479099ade66", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.087170Z", - "start_time": "2026-09-08T00:49:26.046374Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.473699Z", "iopub.status.busy": "2026-09-07T17:47:04.473637Z", "iopub.status.idle": "2026-09-07T17:47:04.476602Z", "shell.execute_reply": "2026-09-07T17:47:04.476290Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.777172Z", + "start_time": "2026-09-08T02:56:48.758564Z" } }, + "source": [ + "from pyiron_snippets.colors import SeabornColors\n", + "\n", + "data = fr.tools.recipe2data(get_blue.flowrep_recipe)\n", + "\n", + "data.input_ports[\"colors\"].annotation" + ], "outputs": [ { "data": { @@ -5606,13 +4789,7 @@ "output_type": "execute_result" } ], - "source": [ - "from pyiron_snippets.colors import SeabornColors\n", - "\n", - "data = fr.tools.recipe2data(get_blue.flowrep_recipe)\n", - "\n", - "data.input_ports[\"colors\"].annotation" - ] + "execution_count": 80 }, { "cell_type": "markdown", @@ -5635,24 +4812,24 @@ }, { "cell_type": "code", - "execution_count": 81, "id": "8fc856912a06a735", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.109193Z", - "start_time": "2026-09-08T00:49:26.090219Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.477268Z", "iopub.status.busy": "2026-09-07T17:47:04.477205Z", "iopub.status.idle": "2026-09-07T17:47:04.481014Z", "shell.execute_reply": "2026-09-07T17:47:04.480670Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.788660Z", + "start_time": "2026-09-08T02:56:48.778098Z" } }, - "outputs": [], "source": [ "ran_node = fr.tools.run_recipe(scaled_range.flowrep_recipe, n=3)" - ] + ], + "outputs": [], + "execution_count": 81 }, { "cell_type": "markdown", @@ -5670,20 +4847,22 @@ }, { "cell_type": "code", - "execution_count": 82, "id": "47a7f46ae249d2f6", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.126195Z", - "start_time": "2026-09-08T00:49:26.110203Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.481675Z", "iopub.status.busy": "2026-09-07T17:47:04.481610Z", "iopub.status.idle": "2026-09-07T17:47:04.485011Z", "shell.execute_reply": "2026-09-07T17:47:04.484630Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.802907Z", + "start_time": "2026-09-08T02:56:48.789507Z" } }, + "source": [ + "ran_node.output_ports[\"scaled\"].value" + ], "outputs": [ { "data": { @@ -5696,9 +4875,7 @@ "output_type": "execute_result" } ], - "source": [ - "ran_node.output_ports[\"scaled\"].value" - ] + "execution_count": 82 }, { "cell_type": "markdown", @@ -5715,32 +4892,19 @@ }, { "cell_type": "code", - "execution_count": 83, "id": "72982d6646db5e03", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.142502Z", - "start_time": "2026-09-08T00:49:26.127102Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.485676Z", "iopub.status.busy": "2026-09-07T17:47:04.485614Z", "iopub.status.idle": "2026-09-07T17:47:04.488771Z", "shell.execute_reply": "2026-09-07T17:47:04.488491Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:48.827109Z", + "start_time": "2026-09-08T02:56:48.803859Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All recorded nodes: ['scatter_items', 'body_0', 'body_1', 'body_2', 'aggregate_results']\n", - "For-loop child body_0 transformed 0*1.0 → 0.0\n", - "For-loop child body_1 transformed 1*1.0 → 1.0\n", - "For-loop child body_2 transformed 2*1.0 → 2.0\n" - ] - } - ], "source": [ "ran_for = ran_node.nodes[\"rescale_all_0\"].nodes[\"for_each_0\"]\n", "\n", @@ -5754,7 +4918,20 @@ " f\"{body_node.input_ports['item'].value}*{body_node.input_ports['factor'].value} \"\n", " f\"→ {body_node.output_ports['scaled'].value}\"\n", " )" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All recorded nodes: ['scatter_items', 'body_0', 'body_1', 'body_2', 'aggregate_results']\n", + "For-loop child body_0 transformed 0*1.0 → 0.0\n", + "For-loop child body_1 transformed 1*1.0 → 1.0\n", + "For-loop child body_2 transformed 2*1.0 → 2.0\n" + ] + } + ], + "execution_count": 83 }, { "cell_type": "markdown", @@ -5772,219 +4949,29 @@ }, { "cell_type": "code", - "execution_count": 84, "id": "838c4719f577a6a3", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.305789Z", - "start_time": "2026-09-08T00:49:26.143499Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.489459Z", "iopub.status.busy": "2026-09-07T17:47:04.489394Z", "iopub.status.idle": "2026-09-07T17:47:04.501821Z", "shell.execute_reply": "2026-09-07T17:47:04.501446Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:49.004155Z", + "start_time": "2026-09-08T02:56:48.828232Z" } }, + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "ran_for.draw()" + ], "outputs": [ { "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cluster_\n", - "\n", - "for_each\n", - "\n", - "\n", - "\n", - "inputs.factor\n", - "\n", - "factor\n", - "\n", - "\n", - "\n", - "\n", - "body_0\n", - "\n", - "body_0\n", - "\n", - "\n", - "item\n", - "\n", - "\n", - "scaled\n", - "\n", - "\n", - "factor\n", - "\n", - "\n", - "\n", - "\n", - "inputs.factor:e->body_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body_1\n", - "\n", - "body_1\n", - "\n", - "\n", - "item\n", - "\n", - "\n", - "scaled\n", - "\n", - "\n", - "factor\n", - "\n", - "\n", - "\n", - "\n", - "inputs.factor:e->body_1:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body_2\n", - "\n", - "body_2\n", - "\n", - "\n", - "item\n", - "\n", - "\n", - "scaled\n", - "\n", - "\n", - "factor\n", - "\n", - "\n", - "\n", - "\n", - "inputs.factor:e->body_2:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "inputs.items\n", - "\n", - "items\n", - "\n", - "\n", - "\n", - "\n", - "scatter_items\n", - "\n", - "scatter_items\n", - "…orm1toN.iterable_to_outputs\n", - "\n", - "\n", - "items\n", - "\n", - "\n", - "output_0\n", - "\n", - "\n", - "output_1\n", - "\n", - "\n", - "output_2\n", - "\n", - "\n", - "\n", - "\n", - "inputs.items:e->scatter_items:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "outputs.results\n", - "\n", - "results\n", - "\n", - "\n", - "\n", - "\n", - "scatter_items:e->body_0:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "scatter_items:e->body_1:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "scatter_items:e->body_2:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "aggregate_results\n", - "\n", - "aggregate_results\n", - "…ransformNto1.inputs_to_list\n", - "\n", - "\n", - "item_0\n", - "\n", - "\n", - "output_0\n", - "\n", - "\n", - "item_1\n", - "\n", - "\n", - "item_2\n", - "\n", - "\n", - "\n", - "\n", - "body_0:e->aggregate_results:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body_1:e->aggregate_results:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "body_2:e->aggregate_results:w\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "aggregate_results:e->outputs.results:w\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], + "image/svg+xml": "\n\n\n\n\n\n\n\ncluster_\n\nfor_each\n\n\n\ninputs.factor\n\nfactor\n\n\n\n\nbody_0\n\nbody_0\n\n\nitem\n\n\nscaled\n\n\nfactor\n\n\n\n\ninputs.factor:e->body_0:w\n\n\n\n\n\nbody_1\n\nbody_1\n\n\nitem\n\n\nscaled\n\n\nfactor\n\n\n\n\ninputs.factor:e->body_1:w\n\n\n\n\n\nbody_2\n\nbody_2\n\n\nitem\n\n\nscaled\n\n\nfactor\n\n\n\n\ninputs.factor:e->body_2:w\n\n\n\n\n\ninputs.items\n\nitems\n\n\n\n\nscatter_items\n\nscatter_items\n…orm1toN.iterable_to_outputs\n\n\nitems\n\n\noutput_0\n\n\noutput_1\n\n\noutput_2\n\n\n\n\ninputs.items:e->scatter_items:w\n\n\n\n\n\noutputs.results\n\nresults\n\n\n\n\nscatter_items:e->body_0:w\n\n\n\n\n\nscatter_items:e->body_1:w\n\n\n\n\n\nscatter_items:e->body_2:w\n\n\n\n\n\naggregate_results\n\naggregate_results\n…ransformNto1.inputs_to_list\n\n\nitem_0\n\n\noutput_0\n\n\nitem_1\n\n\nitem_2\n\n\n\n\nbody_0:e->aggregate_results:w\n\n\n\n\n\nbody_1:e->aggregate_results:w\n\n\n\n\n\nbody_2:e->aggregate_results:w\n\n\n\n\n\naggregate_results:e->outputs.results:w\n\n\n\n\n\n", "text/plain": [ - "" + "" ] }, "execution_count": 84, @@ -5992,10 +4979,7 @@ "output_type": "execute_result" } ], - "source": [ - "# NBVAL_IGNORE_OUTPUT\n", - "ran_for.draw()" - ] + "execution_count": 84 }, { "cell_type": "markdown", @@ -6017,32 +5001,19 @@ }, { "cell_type": "code", - "execution_count": 85, "id": "1264d43293ffbd43", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.327352Z", - "start_time": "2026-09-08T00:49:26.310319Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.502531Z", "iopub.status.busy": "2026-09-07T17:47:04.502463Z", "iopub.status.idle": "2026-09-07T17:47:04.507520Z", "shell.execute_reply": "2026-09-07T17:47:04.507162Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:49.040012Z", + "start_time": "2026-09-08T02:56:49.020473Z" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "{'scaled': OutputDataPort(value=[0.0, 1.0, 2.0, 3.0, 4.0], annotation=list[float])}" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "@fr.atomic\n", "def rescale(value: int | float, factor: float = 1.0) -> float:\n", @@ -6072,7 +5043,20 @@ "\n", "executed = fr.tools.run_recipe(scaled_range.flowrep_recipe, n=5)\n", "executed.output_ports" - ] + ], + "outputs": [ + { + "data": { + "text/plain": [ + "{'scaled': OutputDataPort(value=[0.0, 1.0, 2.0, 3.0, 4.0], annotation=list[float])}" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 85 }, { "cell_type": "markdown", @@ -6089,26 +5073,26 @@ }, { "cell_type": "code", - "execution_count": 86, "id": "8d092cdcf9c0e517", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.677341Z", - "start_time": "2026-09-08T00:49:26.340152Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.508185Z", "iopub.status.busy": "2026-09-07T17:47:04.508118Z", "iopub.status.idle": "2026-09-07T17:47:04.916081Z", "shell.execute_reply": "2026-09-07T17:47:04.915679Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:49.767165Z", + "start_time": "2026-09-08T02:56:49.051200Z" } }, - "outputs": [], "source": [ "import bagofholding as boh\n", "\n", "boh.H5Bag.save(executed, \"executed.h5\", overwrite_existing=True)" - ] + ], + "outputs": [], + "execution_count": 86 }, { "cell_type": "markdown", @@ -6130,20 +5114,23 @@ }, { "cell_type": "code", - "execution_count": 87, "id": "caeb3c190f954f71", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:26.713353Z", - "start_time": "2026-09-08T00:49:26.683278Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.917952Z", "iopub.status.busy": "2026-09-07T17:47:04.917844Z", "iopub.status.idle": "2026-09-07T17:47:04.935777Z", "shell.execute_reply": "2026-09-07T17:47:04.935439Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:49.858755Z", + "start_time": "2026-09-08T02:56:49.811706Z" } }, + "source": [ + "browser = fr.tools.LexicalBagBrowser(\"executed.h5\")\n", + "browser.list_paths()" + ], "outputs": [ { "data": { @@ -6223,10 +5210,7 @@ "output_type": "execute_result" } ], - "source": [ - "browser = fr.tools.LexicalBagBrowser(\"executed.h5\")\n", - "browser.list_paths()" - ] + "execution_count": 87 }, { "cell_type": "markdown", @@ -6243,20 +5227,23 @@ }, { "cell_type": "code", - "execution_count": 88, "id": "412ddc4424bcefd3", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:27.019082Z", - "start_time": "2026-09-08T00:49:26.725883Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:04.936667Z", "iopub.status.busy": "2026-09-07T17:47:04.936594Z", "iopub.status.idle": "2026-09-07T17:47:05.282848Z", "shell.execute_reply": "2026-09-07T17:47:05.282467Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:50.408676Z", + "start_time": "2026-09-08T02:56:49.873645Z" } }, + "source": [ + "reloaded_node = browser.load(\"rescale_all_0\")\n", + "isinstance(reloaded_node, fr.schemas.DagData), reloaded_node.nodes.keys()" + ], "outputs": [ { "data": { @@ -6269,10 +5256,7 @@ "output_type": "execute_result" } ], - "source": [ - "reloaded_node = browser.load(\"rescale_all_0\")\n", - "isinstance(reloaded_node, fr.schemas.DagData), reloaded_node.nodes.keys()" - ] + "execution_count": 88 }, { "cell_type": "markdown", @@ -6289,20 +5273,23 @@ }, { "cell_type": "code", - "execution_count": 89, "id": "d948155042459cb", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:27.038801Z", - "start_time": "2026-09-08T00:49:27.023414Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:05.283568Z", "iopub.status.busy": "2026-09-07T17:47:05.283501Z", "iopub.status.idle": "2026-09-07T17:47:05.290753Z", "shell.execute_reply": "2026-09-07T17:47:05.290317Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:50.461153Z", + "start_time": "2026-09-08T02:56:50.426393Z" } }, + "source": [ + "reloaded_port = browser.load(\"rescale_all_0.for_each_0.body_1.inputs.item\")\n", + "isinstance(reloaded_port, fr.schemas.InputDataPort), reloaded_port.value" + ], "outputs": [ { "data": { @@ -6315,10 +5302,7 @@ "output_type": "execute_result" } ], - "source": [ - "reloaded_port = browser.load(\"rescale_all_0.for_each_0.body_1.inputs.item\")\n", - "isinstance(reloaded_port, fr.schemas.InputDataPort), reloaded_port.value" - ] + "execution_count": 89 }, { "cell_type": "markdown", @@ -6336,20 +5320,25 @@ }, { "cell_type": "code", - "execution_count": 90, "id": "e0e550efad910586", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:27.081959Z", - "start_time": "2026-09-08T00:49:27.049913Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:05.291422Z", "iopub.status.busy": "2026-09-07T17:47:05.291360Z", "iopub.status.idle": "2026-09-07T17:47:05.301903Z", "shell.execute_reply": "2026-09-07T17:47:05.301571Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:50.522014Z", + "start_time": "2026-09-08T02:56:50.480017Z" } }, + "source": [ + "try:\n", + " browser.load(\"rescale_all_0.inputs\")\n", + "except ValueError as e:\n", + " print(e)" + ], "outputs": [ { "name": "stdout", @@ -6359,12 +5348,7 @@ ] } ], - "source": [ - "try:\n", - " browser.load(\"rescale_all_0.inputs\")\n", - "except ValueError as e:\n", - " print(e)" - ] + "execution_count": 90 }, { "cell_type": "markdown", @@ -6381,41 +5365,41 @@ }, { "cell_type": "code", - "execution_count": 91, "id": "1b10ad697f945ac5", "metadata": { - "ExecuteTime": { - "end_time": "2026-09-08T00:49:27.134241Z", - "start_time": "2026-09-08T00:49:27.083861Z" - }, "execution": { "iopub.execute_input": "2026-09-07T17:47:05.302578Z", "iopub.status.busy": "2026-09-07T17:47:05.302514Z", "iopub.status.idle": "2026-09-07T17:47:05.311803Z", "shell.execute_reply": "2026-09-07T17:47:05.311417Z" + }, + "ExecuteTime": { + "end_time": "2026-09-08T02:56:50.574681Z", + "start_time": "2026-09-08T02:56:50.523048Z" } }, + "source": [ + "widget = browser.browse()\n", + "widget" + ], "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ed85d138b9aa4e2f9a830cbb066f494a", - "version_major": 2, - "version_minor": 0 - }, "text/plain": [ "LexicalBagTree(multiple_selection=False, nodes=(Node(close_icon_style='danger', name='Workflow', nodes=(Node(c…" - ] + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "801497e75f31405da4897ef20ab6257b" + } }, "execution_count": 91, "metadata": {}, "output_type": "execute_result" } ], - "source": [ - "widget = browser.browse()\n", - "widget" - ] + "execution_count": 91 }, { "cell_type": "markdown",