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.
- tg_model/__init__.py +46 -0
- tg_model/analysis/__init__.py +38 -0
- tg_model/analysis/_coherence.py +91 -0
- tg_model/analysis/compare_variants.py +197 -0
- tg_model/analysis/impact.py +101 -0
- tg_model/analysis/sweep.py +199 -0
- tg_model/execution/__init__.py +122 -0
- tg_model/execution/behavior.py +826 -0
- tg_model/execution/configured_model.py +754 -0
- tg_model/execution/connection_bindings.py +87 -0
- tg_model/execution/dependency_graph.py +217 -0
- tg_model/execution/evaluator.py +419 -0
- tg_model/execution/external_ops.py +153 -0
- tg_model/execution/graph_compiler.py +1110 -0
- tg_model/execution/instances.py +274 -0
- tg_model/execution/requirements.py +104 -0
- tg_model/execution/rollups.py +60 -0
- tg_model/execution/run_context.py +304 -0
- tg_model/execution/solve_groups.py +104 -0
- tg_model/execution/validation.py +211 -0
- tg_model/execution/value_slots.py +63 -0
- tg_model/export/__init__.py +7 -0
- tg_model/integrations/__init__.py +36 -0
- tg_model/integrations/external_compute.py +122 -0
- tg_model/model/__init__.py +39 -0
- tg_model/model/compile_types.py +896 -0
- tg_model/model/declarations/__init__.py +8 -0
- tg_model/model/declarations/behavior.py +37 -0
- tg_model/model/declarations/values.py +53 -0
- tg_model/model/definition_context.py +1742 -0
- tg_model/model/elements.py +159 -0
- tg_model/model/expr.py +75 -0
- tg_model/model/identity.py +63 -0
- tg_model/model/refs.py +319 -0
- thundergraph_model-1.0.0.dist-info/METADATA +82 -0
- thundergraph_model-1.0.0.dist-info/RECORD +38 -0
- thundergraph_model-1.0.0.dist-info/WHEEL +4 -0
- 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
|