thundergraph-model 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. tg_model/__init__.py +46 -0
  2. tg_model/analysis/__init__.py +38 -0
  3. tg_model/analysis/_coherence.py +91 -0
  4. tg_model/analysis/compare_variants.py +197 -0
  5. tg_model/analysis/impact.py +101 -0
  6. tg_model/analysis/sweep.py +199 -0
  7. tg_model/execution/__init__.py +122 -0
  8. tg_model/execution/behavior.py +826 -0
  9. tg_model/execution/configured_model.py +754 -0
  10. tg_model/execution/connection_bindings.py +87 -0
  11. tg_model/execution/dependency_graph.py +217 -0
  12. tg_model/execution/evaluator.py +419 -0
  13. tg_model/execution/external_ops.py +153 -0
  14. tg_model/execution/graph_compiler.py +1110 -0
  15. tg_model/execution/instances.py +274 -0
  16. tg_model/execution/requirements.py +104 -0
  17. tg_model/execution/rollups.py +60 -0
  18. tg_model/execution/run_context.py +304 -0
  19. tg_model/execution/solve_groups.py +104 -0
  20. tg_model/execution/validation.py +211 -0
  21. tg_model/execution/value_slots.py +63 -0
  22. tg_model/export/__init__.py +7 -0
  23. tg_model/integrations/__init__.py +36 -0
  24. tg_model/integrations/external_compute.py +122 -0
  25. tg_model/model/__init__.py +39 -0
  26. tg_model/model/compile_types.py +896 -0
  27. tg_model/model/declarations/__init__.py +8 -0
  28. tg_model/model/declarations/behavior.py +37 -0
  29. tg_model/model/declarations/values.py +53 -0
  30. tg_model/model/definition_context.py +1742 -0
  31. tg_model/model/elements.py +159 -0
  32. tg_model/model/expr.py +75 -0
  33. tg_model/model/identity.py +63 -0
  34. tg_model/model/refs.py +319 -0
  35. thundergraph_model-1.0.0.dist-info/METADATA +82 -0
  36. thundergraph_model-1.0.0.dist-info/RECORD +38 -0
  37. thundergraph_model-1.0.0.dist-info/WHEEL +4 -0
  38. thundergraph_model-1.0.0.dist-info/licenses/LICENSE +201 -0
tg_model/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """ThunderGraph Model — executable systems modeling in Python.
2
+
3
+ This package re-exports the **primary authoring surface** (elements, definition
4
+ context, refs, and roll-up helpers). For configure-time and run-time APIs
5
+ (:class:`~tg_model.execution.configured_model.ConfiguredModel`, graph compile,
6
+ evaluation), import :mod:`tg_model.execution` explicitly.
7
+
8
+ Notes
9
+ -----
10
+ Types are **compiled** once per class (cached). :func:`~tg_model.execution.configured_model.instantiate`
11
+ builds a frozen topology; :class:`~tg_model.execution.run_context.RunContext` holds
12
+ per-run values. See the user documentation plan in ``docs/user_docs/``.
13
+ """
14
+
15
+ from tg_model.model.declarations.values import rollup
16
+ from tg_model.model.definition_context import (
17
+ ModelDefinitionContext,
18
+ ModelDefinitionError,
19
+ parameter_ref,
20
+ requirement_ref,
21
+ )
22
+ from tg_model.model.elements import Element, Part, Requirement, System
23
+ from tg_model.model.refs import (
24
+ AttributeRef,
25
+ PartRef,
26
+ PortRef,
27
+ Ref,
28
+ RequirementRef,
29
+ )
30
+
31
+ __all__ = [
32
+ "AttributeRef",
33
+ "Element",
34
+ "ModelDefinitionContext",
35
+ "ModelDefinitionError",
36
+ "Part",
37
+ "PartRef",
38
+ "PortRef",
39
+ "Ref",
40
+ "Requirement",
41
+ "RequirementRef",
42
+ "System",
43
+ "parameter_ref",
44
+ "requirement_ref",
45
+ "rollup",
46
+ ]
@@ -0,0 +1,38 @@
1
+ """Multi-run orchestration: sweeps, variant comparison, value-graph propagation (Phase 5).
2
+
3
+ ``impact`` / :func:`dependency_impact` report **dependency reachability on the value graph**
4
+ only — not program-wide “engineering impact” (see :mod:`tg_model.analysis.impact`).
5
+ """
6
+
7
+ from tg_model.analysis.compare_variants import (
8
+ CapturedSlotOutput,
9
+ CompareVariantsValidationError,
10
+ VariantComparisonRow,
11
+ VariantScenario,
12
+ compare_variants,
13
+ compare_variants_async,
14
+ )
15
+ from tg_model.analysis.impact import (
16
+ ImpactReport,
17
+ dependency_impact,
18
+ value_graph_propagation,
19
+ )
20
+ from tg_model.analysis.sweep import SweepRecord, sweep, sweep_async
21
+
22
+ impact = dependency_impact
23
+
24
+ __all__ = [
25
+ "CapturedSlotOutput",
26
+ "CompareVariantsValidationError",
27
+ "ImpactReport",
28
+ "SweepRecord",
29
+ "VariantComparisonRow",
30
+ "VariantScenario",
31
+ "compare_variants",
32
+ "compare_variants_async",
33
+ "dependency_impact",
34
+ "impact",
35
+ "sweep",
36
+ "sweep_async",
37
+ "value_graph_propagation",
38
+ ]
@@ -0,0 +1,91 @@
1
+ """Shared checks: configured model, compiled graph, and ValueSlot handles must align."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from tg_model.execution.configured_model import ConfiguredModel
9
+ from tg_model.execution.dependency_graph import DependencyGraph
10
+ from tg_model.execution.value_slots import ValueSlot
11
+
12
+
13
+ def assert_slots_align_with_graph(
14
+ configured_model: ConfiguredModel,
15
+ graph: DependencyGraph,
16
+ slots: Iterable[ValueSlot],
17
+ *,
18
+ context: str,
19
+ ) -> None:
20
+ """Verify slots belong to ``configured_model`` and appear as ``val:<path>`` nodes in ``graph``.
21
+
22
+ Parameters
23
+ ----------
24
+ configured_model : ConfiguredModel
25
+ Topology built from the same root as ``graph``.
26
+ graph : DependencyGraph
27
+ Output of :func:`~tg_model.execution.graph_compiler.compile_graph` for that model.
28
+ slots : iterable of ValueSlot
29
+ Handles to verify (parameters, prune targets, …).
30
+ context : str
31
+ Label for error messages (e.g. ``\"sweep\"``).
32
+
33
+ Raises
34
+ ------
35
+ ValueError
36
+ On registry mismatch, path mismatch, or missing graph node.
37
+ """
38
+ for s in slots:
39
+ if s.stable_id not in configured_model.id_registry:
40
+ raise ValueError(
41
+ f"{context}: slot {s.path_string!r} (stable_id={s.stable_id!r}) "
42
+ f"is not registered on the given configured_model."
43
+ )
44
+ reg = configured_model.id_registry[s.stable_id]
45
+ if not isinstance(reg, ValueSlot):
46
+ raise ValueError(f"{context}: id_registry[{s.stable_id!r}] is not a ValueSlot (got {type(reg).__name__}).")
47
+ if reg.path_string != s.path_string:
48
+ raise ValueError(
49
+ f"{context}: slot path mismatch for stable_id {s.stable_id!r}: "
50
+ f"handle has {s.path_string!r}, model has {reg.path_string!r}."
51
+ )
52
+ vid = f"val:{s.path_string}"
53
+ if vid not in graph.nodes:
54
+ raise ValueError(
55
+ f"{context}: no graph node {vid!r} for slot {s.path_string!r}. "
56
+ "Pass the DependencyGraph from compile_graph(same configured_model)."
57
+ )
58
+
59
+
60
+ def plan_sweep_axes(
61
+ parameter_values: Mapping[ValueSlot, Sequence[Any]],
62
+ ) -> tuple[list[ValueSlot], list[Sequence[Any]]]:
63
+ """Sort sweep axes by ``stable_id`` and validate each axis is non-empty.
64
+
65
+ Parameters
66
+ ----------
67
+ parameter_values
68
+ Map each parameter slot to its value sequence.
69
+
70
+ Returns
71
+ -------
72
+ slots_sorted : list[ValueSlot]
73
+ Keys sorted by ``stable_id``.
74
+ combo_lists : list[Sequence[Any]]
75
+ Parallel list of value sequences (same order as ``slots_sorted``).
76
+
77
+ Raises
78
+ ------
79
+ ValueError
80
+ If any slot maps to an empty sequence.
81
+ """
82
+ if not parameter_values:
83
+ return [], []
84
+ slots_sorted = sorted(parameter_values.keys(), key=lambda s: s.stable_id)
85
+ combo_lists: list[Sequence[Any]] = []
86
+ for slot in slots_sorted:
87
+ seq = parameter_values[slot]
88
+ if not seq:
89
+ raise ValueError(f"Empty value sequence for slot '{slot.path_string}'")
90
+ combo_lists.append(seq)
91
+ return slots_sorted, combo_lists
@@ -0,0 +1,197 @@
1
+ """Cross-variant evaluation: same workflow, isolated runs, aligned outputs (Phase 5).
2
+
3
+ Each scenario compiles its own graph from its :class:`ConfiguredModel` — structure
4
+ may differ per variant. There is no shared :class:`tg_model.execution.run_context.RunContext`.
5
+
6
+ By default, :func:`validate_graph` runs (with ``configured_model``) before each
7
+ evaluation so ill-posed variants fail with :class:`CompareVariantsValidationError`
8
+ instead of obscure runtime errors.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Mapping, Sequence
14
+ from dataclasses import dataclass
15
+ from typing import Any, TypeAlias
16
+
17
+ from tg_model.execution.configured_model import ConfiguredModel
18
+ from tg_model.execution.dependency_graph import DependencyGraph
19
+ from tg_model.execution.evaluator import Evaluator, RunResult
20
+ from tg_model.execution.graph_compiler import compile_graph
21
+ from tg_model.execution.run_context import RunContext
22
+ from tg_model.execution.validation import validate_graph
23
+ from tg_model.execution.value_slots import ValueSlot
24
+
25
+ VariantScenario: TypeAlias = tuple[str, ConfiguredModel, Mapping[str, Any]] # noqa: UP040
26
+
27
+
28
+ class CompareVariantsValidationError(Exception):
29
+ """Raised when :func:`~tg_model.execution.validation.validate_graph` fails for one variant."""
30
+
31
+ def __init__(self, label: str, failures: list[str]) -> None:
32
+ """Attach ``label`` and ``failures`` to the exception for programmatic handling."""
33
+ self.label = label
34
+ self.failures = list(failures)
35
+ detail = "; ".join(failures)
36
+ super().__init__(f"validate_graph failed for variant {label!r}: {detail}")
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class CapturedSlotOutput:
41
+ """Resolved output for one ``output_paths`` entry in :class:`VariantComparisonRow`."""
42
+
43
+ value: Any | None
44
+ present_in_run_outputs: bool
45
+ """True iff the slot's ``stable_id`` was present in ``RunResult.outputs``."""
46
+
47
+ @property
48
+ def realized(self) -> bool:
49
+ return self.present_in_run_outputs
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class VariantComparisonRow:
54
+ """One scenario row from :func:`compare_variants`."""
55
+
56
+ label: str
57
+ outputs: dict[str, CapturedSlotOutput]
58
+ result: RunResult
59
+
60
+
61
+ def _assert_same_root_if_requested(
62
+ scenarios: Sequence[VariantScenario],
63
+ *,
64
+ require_same_root_definition_type: bool,
65
+ ) -> None:
66
+ if not require_same_root_definition_type or len(scenarios) <= 1:
67
+ return
68
+ t0 = scenarios[0][1].root.definition_type
69
+ for label, cm, _ in scenarios[1:]:
70
+ if cm.root.definition_type is not t0:
71
+ raise ValueError(
72
+ f"compare_variants: scenario {label!r} root type "
73
+ f"{cm.root.definition_type!r} differs from the first scenario ({t0!r}). "
74
+ "Set require_same_root_definition_type=False to compare structurally "
75
+ "different roots, or align your configured models."
76
+ )
77
+
78
+
79
+ def _compile_and_maybe_validate(
80
+ label: str,
81
+ cm: ConfiguredModel,
82
+ *,
83
+ validate_before_run: bool,
84
+ ) -> tuple[DependencyGraph, dict[str, Any]]:
85
+ graph, handlers = compile_graph(cm)
86
+ if validate_before_run:
87
+ v = validate_graph(graph, configured_model=cm)
88
+ if not v.passed:
89
+ raise CompareVariantsValidationError(label, [f.message for f in v.failures])
90
+ return graph, handlers
91
+
92
+
93
+ def _collect_outputs(cm: ConfiguredModel, result: RunResult, paths: Sequence[str]) -> dict[str, CapturedSlotOutput]:
94
+ out: dict[str, CapturedSlotOutput] = {}
95
+ for path in paths:
96
+ handle = cm.handle(path)
97
+ if not isinstance(handle, ValueSlot):
98
+ raise TypeError(f"output_paths entry {path!r} must resolve to a ValueSlot, got {type(handle).__name__}")
99
+ sid = handle.stable_id
100
+ present = sid in result.outputs
101
+ out[path] = CapturedSlotOutput(
102
+ value=result.outputs[sid] if present else None,
103
+ present_in_run_outputs=present,
104
+ )
105
+ return out
106
+
107
+
108
+ def compare_variants(
109
+ *,
110
+ scenarios: Sequence[VariantScenario],
111
+ output_paths: Sequence[str],
112
+ validate_before_run: bool = True,
113
+ require_same_root_definition_type: bool = False,
114
+ ) -> list[VariantComparisonRow]:
115
+ """Evaluate each ``(label, configured_model, inputs)`` with a fresh graph and context.
116
+
117
+ Parameters
118
+ ----------
119
+ scenarios
120
+ Sequence of ``(label, configured_model, inputs)`` tuples.
121
+ output_paths
122
+ Dotted paths that must resolve to :class:`~tg_model.execution.value_slots.ValueSlot`.
123
+ validate_before_run : bool, default True
124
+ Run :func:`~tg_model.execution.validation.validate_graph` before each evaluation.
125
+ require_same_root_definition_type : bool, default False
126
+ When True, all roots must share the same Python type.
127
+
128
+ Returns
129
+ -------
130
+ list of VariantComparisonRow
131
+ One row per scenario in input order.
132
+
133
+ Raises
134
+ ------
135
+ CompareVariantsValidationError
136
+ When validation fails for a labeled scenario.
137
+ ValueError
138
+ When ``require_same_root_definition_type`` is violated.
139
+ TypeError
140
+ When an ``output_paths`` entry does not resolve to a ``ValueSlot``.
141
+
142
+ Notes
143
+ -----
144
+ ``inputs`` maps ``ValueSlot.stable_id`` strings to values, same as
145
+ :meth:`~tg_model.execution.evaluator.Evaluator.evaluate`. ``CapturedSlotOutput``
146
+ distinguishes absent outputs from ``None`` values via ``present_in_run_outputs``.
147
+ """
148
+ _assert_same_root_if_requested(scenarios, require_same_root_definition_type=require_same_root_definition_type)
149
+
150
+ rows: list[VariantComparisonRow] = []
151
+ for label, cm, inputs in scenarios:
152
+ graph, handlers = _compile_and_maybe_validate(
153
+ label,
154
+ cm,
155
+ validate_before_run=validate_before_run,
156
+ )
157
+ evaluator = Evaluator(graph, compute_handlers=handlers)
158
+ ctx = RunContext()
159
+ result = evaluator.evaluate(ctx, inputs=dict(inputs))
160
+ outputs = _collect_outputs(cm, result, output_paths)
161
+ rows.append(VariantComparisonRow(label=label, outputs=outputs, result=result))
162
+ return rows
163
+
164
+
165
+ async def compare_variants_async(
166
+ *,
167
+ scenarios: Sequence[VariantScenario],
168
+ output_paths: Sequence[str],
169
+ validate_before_run: bool = True,
170
+ require_same_root_definition_type: bool = False,
171
+ ) -> list[VariantComparisonRow]:
172
+ """Async variant of :func:`compare_variants` (uses ``evaluate_async`` per scenario).
173
+
174
+ Raises
175
+ ------
176
+ CompareVariantsValidationError, ValueError, TypeError
177
+ Same families as :func:`compare_variants`.
178
+ """
179
+ _assert_same_root_if_requested(scenarios, require_same_root_definition_type=require_same_root_definition_type)
180
+
181
+ rows: list[VariantComparisonRow] = []
182
+ for label, cm, inputs in scenarios:
183
+ graph, handlers = _compile_and_maybe_validate(
184
+ label,
185
+ cm,
186
+ validate_before_run=validate_before_run,
187
+ )
188
+ evaluator = Evaluator(graph, compute_handlers=handlers)
189
+ ctx = RunContext()
190
+ result = await evaluator.evaluate_async(
191
+ ctx,
192
+ configured_model=cm,
193
+ inputs=dict(inputs),
194
+ )
195
+ outputs = _collect_outputs(cm, result, output_paths)
196
+ rows.append(VariantComparisonRow(label=label, outputs=outputs, result=result))
197
+ return rows
@@ -0,0 +1,101 @@
1
+ """Value-graph propagation (Phase 5): upstream / downstream **value slots** only.
2
+
3
+ This is **dependency reachability** on one compiled :class:`~tg_model.execution.dependency_graph.DependencyGraph`.
4
+ It does **not** aggregate requirements, hazards, behavior, interfaces, or other program semantics —
5
+ do not treat it as a full “engineering impact” or FMEA surface without layering your own model.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Sequence
11
+ from dataclasses import dataclass
12
+
13
+ from tg_model.execution.dependency_graph import DependencyGraph
14
+ from tg_model.execution.value_slots import ValueSlot
15
+
16
+
17
+ def _value_node_id(slot: ValueSlot) -> str:
18
+ return f"val:{slot.path_string}"
19
+
20
+
21
+ def _slot_ids_for_nodes(graph: DependencyGraph, node_ids: set[str]) -> frozenset[str]:
22
+ ids: set[str] = set()
23
+ for nid in node_ids:
24
+ sid = graph.get_node(nid).slot_id
25
+ if sid is not None:
26
+ ids.add(sid)
27
+ return frozenset(ids)
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ImpactReport:
32
+ """Value-graph reachability summary from a set of changed slots."""
33
+
34
+ changed_paths: tuple[str, ...]
35
+ upstream_slot_ids: frozenset[str]
36
+ downstream_slot_ids: frozenset[str]
37
+
38
+
39
+ def dependency_impact(
40
+ graph: DependencyGraph,
41
+ changed: Sequence[ValueSlot],
42
+ *,
43
+ upstream: bool = True,
44
+ downstream: bool = True,
45
+ ) -> ImpactReport:
46
+ """Return other value slots reachable from ``changed`` on the value graph.
47
+
48
+ Parameters
49
+ ----------
50
+ graph : DependencyGraph
51
+ Compiled graph for the configuration under study.
52
+ changed : sequence of ValueSlot
53
+ Slots whose perturbation you want to analyze.
54
+ upstream, downstream : bool, default True
55
+ Include reachability in each direction.
56
+
57
+ Returns
58
+ -------
59
+ ImpactReport
60
+ Excludes the changed slots' own ``stable_id`` values from the sets.
61
+
62
+ Raises
63
+ ------
64
+ ValueError
65
+ If a changed slot does not map to a ``val:<path>`` node in ``graph``.
66
+
67
+ Notes
68
+ -----
69
+ This is **dependency reachability only**, not full engineering impact (see module docstring).
70
+ """
71
+ if not changed:
72
+ return ImpactReport((), frozenset(), frozenset())
73
+
74
+ seeds = [_value_node_id(s) for s in changed]
75
+ for vid in seeds:
76
+ if vid not in graph.nodes:
77
+ raise ValueError(
78
+ f"changed slot maps to unknown graph node {vid!r} "
79
+ f"(is this graph compiled for the same configured model?)"
80
+ )
81
+
82
+ seed_ids = {s.stable_id for s in changed}
83
+
84
+ up_ids: frozenset[str] = frozenset()
85
+ down_ids: frozenset[str] = frozenset()
86
+
87
+ if upstream:
88
+ up_ids = frozenset(
89
+ sid for sid in _slot_ids_for_nodes(graph, graph.dependency_closure(seeds)) if sid not in seed_ids
90
+ )
91
+ if downstream:
92
+ down_ids = frozenset(
93
+ sid for sid in _slot_ids_for_nodes(graph, graph.dependent_closure(seeds)) if sid not in seed_ids
94
+ )
95
+
96
+ paths = tuple(s.path_string for s in changed)
97
+ return ImpactReport(paths, up_ids, down_ids)
98
+
99
+
100
+ # Explicit alias: prefer this name when “impact” would oversell scope.
101
+ value_graph_propagation = dependency_impact
@@ -0,0 +1,199 @@
1
+ """Parameter sweeps over a fixed dependency graph (Phase 5).
2
+
3
+ Reuses :class:`tg_model.execution.evaluator.Evaluator` for each sample; every
4
+ sample gets a fresh :class:`tg_model.execution.run_context.RunContext`.
5
+
6
+ **Coherence:** Pass ``configured_model`` whenever you have it; the library then
7
+ verifies sweep :class:`ValueSlot` handles match ``compile_graph(configured_model)``.
8
+
9
+ **Throughput:** Samples run sequentially. This is not a parallel study runner.
10
+
11
+ **Pruning:** ``prune_to_slots`` evaluates an *upstream-closed* subgraph only.
12
+ Constraint (and other) nodes outside that closure are *not* executed —
13
+ ``RunResult.constraint_results`` may be empty. Do not treat a pruned sweep as a
14
+ compliance run unless you know what you excluded.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import itertools
20
+ from collections.abc import Callable, Mapping, Sequence
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ from tg_model.analysis._coherence import assert_slots_align_with_graph, plan_sweep_axes
25
+ from tg_model.execution.configured_model import ConfiguredModel
26
+ from tg_model.execution.dependency_graph import DependencyGraph
27
+ from tg_model.execution.evaluator import Evaluator, RunResult
28
+ from tg_model.execution.run_context import RunContext
29
+ from tg_model.execution.value_slots import ValueSlot
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class SweepRecord:
34
+ """One row from :func:`sweep` / :func:`sweep_async`."""
35
+
36
+ index: int
37
+ inputs: dict[str, Any]
38
+ result: RunResult
39
+
40
+
41
+ def _value_node_id(slot: ValueSlot) -> str:
42
+ return f"val:{slot.path_string}"
43
+
44
+
45
+ def _prepare_pruned(
46
+ graph: DependencyGraph,
47
+ handlers: dict[str, Any],
48
+ prune_to_slots: Sequence[ValueSlot] | None,
49
+ ) -> tuple[DependencyGraph, dict[str, Any]]:
50
+ if not prune_to_slots:
51
+ return graph, handlers
52
+ seeds = [_value_node_id(s) for s in prune_to_slots]
53
+ for vid in seeds:
54
+ if vid not in graph.nodes:
55
+ raise ValueError(
56
+ f"Prune target {vid!r} is not a graph node (expected val:<instance.path> for a compiled value slot)."
57
+ )
58
+ needed = graph.dependency_closure(seeds)
59
+ sub = graph.induced_subgraph(needed)
60
+ sub_handlers = {k: handlers[k] for k in sub.nodes if k in handlers}
61
+ return sub, sub_handlers
62
+
63
+
64
+ def _maybe_assert_coherence(
65
+ configured_model: ConfiguredModel | None,
66
+ graph: DependencyGraph,
67
+ parameter_slots: Sequence[ValueSlot],
68
+ prune_slots: Sequence[ValueSlot] | None,
69
+ ) -> None:
70
+ if configured_model is None:
71
+ return
72
+ to_check = list(parameter_slots)
73
+ if prune_slots:
74
+ to_check.extend(prune_slots)
75
+ assert_slots_align_with_graph(
76
+ configured_model,
77
+ graph,
78
+ to_check,
79
+ context="sweep",
80
+ )
81
+
82
+
83
+ def sweep(
84
+ *,
85
+ graph: DependencyGraph,
86
+ handlers: dict[str, Any],
87
+ parameter_values: Mapping[ValueSlot, Sequence[Any]],
88
+ configured_model: ConfiguredModel | None = None,
89
+ prune_to_slots: Sequence[ValueSlot] | None = None,
90
+ collect: bool = True,
91
+ sink: Callable[[SweepRecord], None] | None = None,
92
+ ) -> list[SweepRecord]:
93
+ """Cartesian product over ``parameter_values``; one synchronous evaluation per tuple.
94
+
95
+ Parameters
96
+ ----------
97
+ graph, handlers
98
+ From :func:`~tg_model.execution.graph_compiler.compile_graph`.
99
+ parameter_values
100
+ Maps each parameter :class:`~tg_model.execution.value_slots.ValueSlot` to a sequence
101
+ of values (axes). Dimension order is sorted by ``stable_id``.
102
+ configured_model : ConfiguredModel, optional
103
+ When passed, asserts sweep slots match the graph (coherence check).
104
+ prune_to_slots : sequence of ValueSlot, optional
105
+ Restricts to upstream closure of these slots (see module warnings).
106
+ collect : bool, default True
107
+ When False, return an empty list and stream via ``sink``.
108
+ sink : callable, optional
109
+ Receives each :class:`SweepRecord` when provided.
110
+
111
+ Returns
112
+ -------
113
+ list of SweepRecord
114
+ All samples when ``collect`` is True.
115
+
116
+ Raises
117
+ ------
118
+ ValueError
119
+ If ``collect=False`` without ``sink``, or prune targets are not graph nodes.
120
+ """
121
+ if not collect and sink is None:
122
+ raise ValueError("sweep(..., collect=False) requires a sink callable")
123
+
124
+ slots_sorted, combo_lists = plan_sweep_axes(parameter_values)
125
+ _maybe_assert_coherence(configured_model, graph, slots_sorted, prune_to_slots)
126
+
127
+ eval_graph, eval_handlers = _prepare_pruned(graph, handlers, prune_to_slots)
128
+ evaluator = Evaluator(eval_graph, compute_handlers=eval_handlers)
129
+
130
+ records: list[SweepRecord] = []
131
+ for idx, combo in enumerate(itertools.product(*combo_lists)):
132
+ inputs: dict[str, Any] = {}
133
+ for slot, val in zip(slots_sorted, combo, strict=True):
134
+ inputs[slot.stable_id] = val
135
+ ctx = RunContext()
136
+ result = evaluator.evaluate(ctx, inputs=inputs)
137
+ rec = SweepRecord(index=idx, inputs=dict(inputs), result=result)
138
+ if sink is not None:
139
+ sink(rec)
140
+ if collect:
141
+ records.append(rec)
142
+ return records
143
+
144
+
145
+ async def sweep_async(
146
+ *,
147
+ configured_model: ConfiguredModel,
148
+ graph: DependencyGraph,
149
+ handlers: dict[str, Any],
150
+ parameter_values: Mapping[ValueSlot, Sequence[Any]],
151
+ prune_to_slots: Sequence[ValueSlot] | None = None,
152
+ collect: bool = True,
153
+ sink: Callable[[SweepRecord], None] | None = None,
154
+ ) -> list[SweepRecord]:
155
+ """Like :func:`sweep` but awaits :meth:`~tg_model.execution.evaluator.Evaluator.evaluate_async`.
156
+
157
+ Parameters
158
+ ----------
159
+ configured_model : ConfiguredModel
160
+ Required for async externals and always used for coherence checks.
161
+ graph, handlers, parameter_values, prune_to_slots, collect, sink
162
+ Same semantics as :func:`sweep`.
163
+
164
+ Returns
165
+ -------
166
+ list of SweepRecord
167
+ Same as :func:`sweep`.
168
+
169
+ Raises
170
+ ------
171
+ ValueError
172
+ Same as :func:`sweep`.
173
+ """
174
+ if not collect and sink is None:
175
+ raise ValueError("sweep_async(..., collect=False) requires a sink callable")
176
+
177
+ slots_sorted, combo_lists = plan_sweep_axes(parameter_values)
178
+ _maybe_assert_coherence(configured_model, graph, slots_sorted, prune_to_slots)
179
+
180
+ eval_graph, eval_handlers = _prepare_pruned(graph, handlers, prune_to_slots)
181
+ evaluator = Evaluator(eval_graph, compute_handlers=eval_handlers)
182
+
183
+ records: list[SweepRecord] = []
184
+ for idx, combo in enumerate(itertools.product(*combo_lists)):
185
+ inputs: dict[str, Any] = {}
186
+ for slot, val in zip(slots_sorted, combo, strict=True):
187
+ inputs[slot.stable_id] = val
188
+ ctx = RunContext()
189
+ result = await evaluator.evaluate_async(
190
+ ctx,
191
+ configured_model=configured_model,
192
+ inputs=inputs,
193
+ )
194
+ rec = SweepRecord(index=idx, inputs=dict(inputs), result=result)
195
+ if sink is not None:
196
+ sink(rec)
197
+ if collect:
198
+ records.append(rec)
199
+ return records