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
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"""Compile a DependencyGraph from a ConfiguredModel's authored semantics.
|
|
2
|
+
|
|
3
|
+
Walks the configured topology, inspects authored expressions and constraints,
|
|
4
|
+
and builds the bipartite dependency graph automatically.
|
|
5
|
+
|
|
6
|
+
Edges are kept so compute nodes depend on **value** nodes (with ``slot_id``);
|
|
7
|
+
``Evaluator._check_dependencies_ready`` only inspects those value dependencies.
|
|
8
|
+
The only public entry point is ``compile_graph()``. All other functions
|
|
9
|
+
are internal compilation helpers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from unitflow import Quantity
|
|
18
|
+
|
|
19
|
+
from tg_model.execution.configured_model import ConfiguredModel
|
|
20
|
+
from tg_model.execution.connection_bindings import AllocationBinding
|
|
21
|
+
from tg_model.execution.dependency_graph import DependencyGraph, DependencyNode, NodeKind
|
|
22
|
+
from tg_model.execution.external_ops import (
|
|
23
|
+
ExternalOpsError,
|
|
24
|
+
materialize_external_result,
|
|
25
|
+
)
|
|
26
|
+
from tg_model.execution.external_ops import (
|
|
27
|
+
resolve_attribute_ref_to_slot as _resolve_attr_ref_core,
|
|
28
|
+
)
|
|
29
|
+
from tg_model.execution.instances import ElementInstance, PartInstance, RequirementPackageInstance
|
|
30
|
+
from tg_model.execution.value_slots import ValueSlot
|
|
31
|
+
from tg_model.integrations.external_compute import (
|
|
32
|
+
ExternalComputeBinding,
|
|
33
|
+
ExternalComputeResult,
|
|
34
|
+
assert_sync_external,
|
|
35
|
+
)
|
|
36
|
+
from tg_model.model.refs import AttributeRef
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _alloc_target_as_part(target: ElementInstance, *, where: str) -> PartInstance:
|
|
40
|
+
"""Narrow allocation targets to :class:`PartInstance` with a single graph-level check."""
|
|
41
|
+
if not isinstance(target, PartInstance):
|
|
42
|
+
raise GraphCompilationError(f"{where}: allocation target must be PartInstance, got {type(target).__name__}")
|
|
43
|
+
return target
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _first_value_slot_under_requirement_package(
|
|
47
|
+
pkg: RequirementPackageInstance,
|
|
48
|
+
) -> ValueSlot | None:
|
|
49
|
+
"""First package :class:`ValueSlot` in stable name order (including nested packages)."""
|
|
50
|
+
for key in sorted(pkg._members.keys()):
|
|
51
|
+
m = pkg._members[key]
|
|
52
|
+
if isinstance(m, ValueSlot):
|
|
53
|
+
return m
|
|
54
|
+
if isinstance(m, RequirementPackageInstance):
|
|
55
|
+
inner = _first_value_slot_under_requirement_package(m)
|
|
56
|
+
if inner is not None:
|
|
57
|
+
return inner
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GraphCompilationError(Exception):
|
|
62
|
+
"""Raised when graph compilation cannot resolve symbols, slots, or bindings."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compile_graph(model: ConfiguredModel) -> tuple[DependencyGraph, dict[str, Callable]]:
|
|
66
|
+
"""Compile dependency graph and per-node compute handlers from a configured model.
|
|
67
|
+
|
|
68
|
+
This is the **only** supported public entry point for graph compilation.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
model : ConfiguredModel
|
|
73
|
+
Frozen topology from :func:`~tg_model.execution.configured_model.instantiate`.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
graph : DependencyGraph
|
|
78
|
+
Bipartite value/compute graph in topological-evaluable form.
|
|
79
|
+
handlers : dict[str, Callable]
|
|
80
|
+
Sync callables keyed by compute ``node_id`` (expressions, roll-ups, externals, constraints).
|
|
81
|
+
|
|
82
|
+
Raises
|
|
83
|
+
------
|
|
84
|
+
GraphCompilationError
|
|
85
|
+
On unresolvable references, binding errors, or other compile failures.
|
|
86
|
+
|
|
87
|
+
Notes
|
|
88
|
+
-----
|
|
89
|
+
Walks value slots, requirement acceptance, constraints, solve groups, and external nodes.
|
|
90
|
+
Async externals are still scheduled from sync :meth:`~tg_model.execution.evaluator.Evaluator.evaluate_async`.
|
|
91
|
+
|
|
92
|
+
Successful results are **cached** on ``model._compiled_graph`` so repeated calls and
|
|
93
|
+
:meth:`~tg_model.execution.configured_model.ConfiguredModel.evaluate` reuse the same
|
|
94
|
+
``(graph, handlers)`` tuple without recompilation.
|
|
95
|
+
"""
|
|
96
|
+
cached = getattr(model, "_compiled_graph", None)
|
|
97
|
+
if cached is not None:
|
|
98
|
+
return cached
|
|
99
|
+
|
|
100
|
+
graph = DependencyGraph()
|
|
101
|
+
handlers: dict[str, Callable] = {}
|
|
102
|
+
|
|
103
|
+
_compile_part(model.root, graph, handlers, model)
|
|
104
|
+
_compile_requirement_packages_from_parts(model.root, graph, handlers, model)
|
|
105
|
+
_compile_constraints_for_part(model.root, graph, handlers, model)
|
|
106
|
+
_compile_requirement_derived_slots(model, graph, handlers)
|
|
107
|
+
_compile_requirement_acceptance(model, graph, handlers)
|
|
108
|
+
_compile_solve_groups_for_part(model.root, graph, handlers, model)
|
|
109
|
+
|
|
110
|
+
model._compiled_graph = (graph, handlers)
|
|
111
|
+
return graph, handlers
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _compile_part(
|
|
115
|
+
part: PartInstance,
|
|
116
|
+
graph: DependencyGraph,
|
|
117
|
+
handlers: dict[str, Callable],
|
|
118
|
+
model: ConfiguredModel,
|
|
119
|
+
) -> None:
|
|
120
|
+
for slot in part.value_slots:
|
|
121
|
+
_compile_slot(slot, part, graph, handlers, model)
|
|
122
|
+
_compile_external_for_part(part, graph, handlers, model)
|
|
123
|
+
for child in part.children:
|
|
124
|
+
_compile_part(child, graph, handlers, model)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _compile_slot(
|
|
128
|
+
slot: ValueSlot,
|
|
129
|
+
owner: PartInstance,
|
|
130
|
+
graph: DependencyGraph,
|
|
131
|
+
handlers: dict[str, Callable],
|
|
132
|
+
model: ConfiguredModel,
|
|
133
|
+
) -> None:
|
|
134
|
+
slot_node_id = f"val:{slot.path_string}"
|
|
135
|
+
|
|
136
|
+
if slot.is_parameter:
|
|
137
|
+
graph.add_node(
|
|
138
|
+
DependencyNode(
|
|
139
|
+
slot_node_id,
|
|
140
|
+
NodeKind.INPUT_PARAMETER,
|
|
141
|
+
slot_id=slot.stable_id,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
expr = slot.metadata.get("_expr")
|
|
147
|
+
|
|
148
|
+
if expr is None:
|
|
149
|
+
graph.add_node(
|
|
150
|
+
DependencyNode(
|
|
151
|
+
slot_node_id,
|
|
152
|
+
NodeKind.ATTRIBUTE_VALUE,
|
|
153
|
+
slot_id=slot.stable_id,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
from tg_model.model.declarations.values import RollupDecl
|
|
159
|
+
|
|
160
|
+
if isinstance(expr, RollupDecl):
|
|
161
|
+
_compile_rollup(slot, slot_node_id, expr, owner, graph, handlers)
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
graph.add_node(
|
|
165
|
+
DependencyNode(
|
|
166
|
+
slot_node_id,
|
|
167
|
+
NodeKind.ATTRIBUTE_VALUE,
|
|
168
|
+
slot_id=slot.stable_id,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
expr_node_id = f"expr:{slot.path_string}"
|
|
173
|
+
graph.add_node(
|
|
174
|
+
DependencyNode(
|
|
175
|
+
expr_node_id,
|
|
176
|
+
NodeKind.LOCAL_EXPRESSION,
|
|
177
|
+
slot_id=slot.stable_id,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
graph.add_edge(expr_node_id, slot_node_id)
|
|
181
|
+
|
|
182
|
+
if isinstance(expr, AttributeRef):
|
|
183
|
+
dep_slot = _resolve_attribute_ref_to_slot(expr, owner, model)
|
|
184
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
185
|
+
graph.add_edge(dep_node_id, expr_node_id)
|
|
186
|
+
|
|
187
|
+
def make_ref_passthrough_handler(dnid: str) -> Callable[..., Any]:
|
|
188
|
+
def handler(dep_values: dict[str, Any]) -> Any:
|
|
189
|
+
return dep_values[dnid]
|
|
190
|
+
|
|
191
|
+
return handler
|
|
192
|
+
|
|
193
|
+
handlers[expr_node_id] = make_ref_passthrough_handler(dep_node_id)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if hasattr(expr, "free_symbols") and expr.free_symbols:
|
|
197
|
+
for sym in expr.free_symbols:
|
|
198
|
+
dep_slot = _resolve_symbol_to_slot(sym, owner, model)
|
|
199
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
200
|
+
graph.add_edge(dep_node_id, expr_node_id)
|
|
201
|
+
|
|
202
|
+
def make_expr_handler(expression: Any, owner_part: PartInstance, cm: ConfiguredModel) -> Callable:
|
|
203
|
+
def handler(dep_values: dict[str, Any]) -> Any:
|
|
204
|
+
context = {}
|
|
205
|
+
for sym in expression.free_symbols:
|
|
206
|
+
dep_slot = _resolve_symbol_to_slot(sym, owner_part, cm)
|
|
207
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
208
|
+
if dep_node_id in dep_values:
|
|
209
|
+
context[sym] = dep_values[dep_node_id]
|
|
210
|
+
return expression.evaluate(context)
|
|
211
|
+
|
|
212
|
+
return handler
|
|
213
|
+
|
|
214
|
+
handlers[expr_node_id] = make_expr_handler(expr, owner, model)
|
|
215
|
+
elif hasattr(expr, "evaluate"):
|
|
216
|
+
handlers[expr_node_id] = lambda dep_values, e=expr: e.evaluate({})
|
|
217
|
+
elif callable(expr):
|
|
218
|
+
handlers[expr_node_id] = lambda dep_values, fn=expr: fn(dep_values)
|
|
219
|
+
else:
|
|
220
|
+
handlers[expr_node_id] = lambda dep_values, val=expr: val
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _resolve_attribute_ref_to_slot(
|
|
224
|
+
ref: Any,
|
|
225
|
+
owner: PartInstance,
|
|
226
|
+
model: ConfiguredModel,
|
|
227
|
+
) -> ValueSlot:
|
|
228
|
+
try:
|
|
229
|
+
return _resolve_attr_ref_core(ref, owner, model)
|
|
230
|
+
except ExternalOpsError as e:
|
|
231
|
+
raise GraphCompilationError(str(e)) from e
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _compile_external_for_part(
|
|
235
|
+
part: PartInstance,
|
|
236
|
+
graph: DependencyGraph,
|
|
237
|
+
handlers: dict[str, Callable],
|
|
238
|
+
model: ConfiguredModel,
|
|
239
|
+
) -> None:
|
|
240
|
+
groups: dict[int, list[ValueSlot]] = {}
|
|
241
|
+
for slot in part.value_slots:
|
|
242
|
+
cb = slot.metadata.get("_computed_by")
|
|
243
|
+
if cb is None:
|
|
244
|
+
continue
|
|
245
|
+
if not isinstance(cb, ExternalComputeBinding):
|
|
246
|
+
raise GraphCompilationError(
|
|
247
|
+
f"computed_by must be an ExternalComputeBinding at '{slot.path_string}', got {type(cb).__name__}"
|
|
248
|
+
)
|
|
249
|
+
if slot.metadata.get("_expr") is not None:
|
|
250
|
+
raise GraphCompilationError(f"Attribute '{slot.path_string}' cannot combine expr= with computed_by=")
|
|
251
|
+
groups.setdefault(id(cb), []).append(slot)
|
|
252
|
+
|
|
253
|
+
for slots in groups.values():
|
|
254
|
+
_build_external_compute_node(slots, part, graph, handlers, model)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _build_external_compute_node(
|
|
258
|
+
slots: list[ValueSlot],
|
|
259
|
+
owner: PartInstance,
|
|
260
|
+
graph: DependencyGraph,
|
|
261
|
+
handlers: dict[str, Callable],
|
|
262
|
+
model: ConfiguredModel,
|
|
263
|
+
) -> None:
|
|
264
|
+
binding: ExternalComputeBinding = slots[0].metadata["_computed_by"]
|
|
265
|
+
for s in slots[1:]:
|
|
266
|
+
if s.metadata.get("_computed_by") is not binding:
|
|
267
|
+
raise GraphCompilationError("External binding identity mismatch within compute group")
|
|
268
|
+
|
|
269
|
+
routes = binding.output_routes
|
|
270
|
+
slot_ids = {s.stable_id for s in slots}
|
|
271
|
+
|
|
272
|
+
if routes is None:
|
|
273
|
+
if len(slots) != 1:
|
|
274
|
+
raise GraphCompilationError(
|
|
275
|
+
"Single-slot ExternalComputeBinding (output_routes is None) requires exactly "
|
|
276
|
+
"one attribute with that binding on this part"
|
|
277
|
+
)
|
|
278
|
+
output_slot_ids = [slots[0].stable_id]
|
|
279
|
+
else:
|
|
280
|
+
resolved_by_key: dict[str, ValueSlot] = {}
|
|
281
|
+
for key, ref in routes.items():
|
|
282
|
+
resolved_by_key[key] = _resolve_attribute_ref_to_slot(ref, owner, model)
|
|
283
|
+
route_ids = {vs.stable_id for vs in resolved_by_key.values()}
|
|
284
|
+
if slot_ids != route_ids:
|
|
285
|
+
raise GraphCompilationError(
|
|
286
|
+
f"external output_routes target slots {route_ids!r} must match exactly "
|
|
287
|
+
f"attributes carrying the same computed_by ({slot_ids!r}) "
|
|
288
|
+
f"(part '{owner.path_string}')"
|
|
289
|
+
)
|
|
290
|
+
output_slot_ids = [resolved_by_key[k].stable_id for k in sorted(routes.keys())]
|
|
291
|
+
|
|
292
|
+
node_id = f"ext:{id(binding)}:{owner.path_string}"
|
|
293
|
+
if node_id in graph.nodes:
|
|
294
|
+
raise GraphCompilationError(f"Duplicate external compute node '{node_id}'")
|
|
295
|
+
|
|
296
|
+
input_name_to_dep: dict[str, str] = {}
|
|
297
|
+
for _iname, ref in binding.inputs.items():
|
|
298
|
+
in_slot = _resolve_attribute_ref_to_slot(ref, owner, model)
|
|
299
|
+
dep_node = f"val:{in_slot.path_string}"
|
|
300
|
+
input_name_to_dep[_iname] = dep_node
|
|
301
|
+
|
|
302
|
+
# Live binding reference: treat as frozen after compile_graph(); mutating it afterward is UB.
|
|
303
|
+
graph.add_node(
|
|
304
|
+
DependencyNode(
|
|
305
|
+
node_id,
|
|
306
|
+
NodeKind.EXTERNAL_COMPUTATION,
|
|
307
|
+
metadata={
|
|
308
|
+
"output_slot_ids": tuple(output_slot_ids),
|
|
309
|
+
"binding_id": id(binding),
|
|
310
|
+
"binding": binding,
|
|
311
|
+
"owner_path": tuple(owner.instance_path),
|
|
312
|
+
"input_name_to_dep": dict(input_name_to_dep),
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
for _dep in input_name_to_dep.values():
|
|
318
|
+
graph.add_edge(_dep, node_id)
|
|
319
|
+
|
|
320
|
+
for s in slots:
|
|
321
|
+
graph.add_edge(node_id, f"val:{s.path_string}")
|
|
322
|
+
|
|
323
|
+
handlers[node_id] = _make_external_handler(
|
|
324
|
+
binding=binding,
|
|
325
|
+
owner=owner,
|
|
326
|
+
model=model,
|
|
327
|
+
input_name_to_dep=input_name_to_dep,
|
|
328
|
+
slots=slots,
|
|
329
|
+
node_id=node_id,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _make_external_handler(
|
|
334
|
+
*,
|
|
335
|
+
binding: ExternalComputeBinding,
|
|
336
|
+
owner: PartInstance,
|
|
337
|
+
model: ConfiguredModel,
|
|
338
|
+
input_name_to_dep: dict[str, str],
|
|
339
|
+
slots: list[ValueSlot],
|
|
340
|
+
node_id: str,
|
|
341
|
+
) -> Callable[..., None]:
|
|
342
|
+
from tg_model.execution.evaluator import RunResult
|
|
343
|
+
from tg_model.execution.run_context import RunContext
|
|
344
|
+
|
|
345
|
+
def handler(dep_values: dict[str, Any], ctx: RunContext, run_result: RunResult) -> None:
|
|
346
|
+
try:
|
|
347
|
+
assert_sync_external(binding.external, context=node_id)
|
|
348
|
+
except TypeError as e:
|
|
349
|
+
msg = str(e)
|
|
350
|
+
for s in slots:
|
|
351
|
+
ctx.get_or_create_record(s.stable_id).block(msg)
|
|
352
|
+
run_result.failures.append(msg)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
inputs_dict: dict[str, Quantity] = {}
|
|
356
|
+
try:
|
|
357
|
+
for name, dep_node_id in input_name_to_dep.items():
|
|
358
|
+
if dep_node_id not in dep_values:
|
|
359
|
+
raise KeyError(f"missing dependency {dep_node_id}")
|
|
360
|
+
inputs_dict[name] = dep_values[dep_node_id]
|
|
361
|
+
compute_fn = getattr(binding.external, "compute", None)
|
|
362
|
+
if compute_fn is None:
|
|
363
|
+
raise TypeError("external object has no compute()")
|
|
364
|
+
res = compute_fn(inputs_dict)
|
|
365
|
+
if not isinstance(res, ExternalComputeResult):
|
|
366
|
+
raise TypeError(f"External compute must return ExternalComputeResult, got {type(res).__name__}")
|
|
367
|
+
materialize_external_result(binding, res, owner, model, ctx, slots)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
msg = str(e)
|
|
370
|
+
for s in slots:
|
|
371
|
+
ctx.get_or_create_record(s.stable_id).fail(msg)
|
|
372
|
+
run_result.failures.append(f"External compute '{node_id}' failed: {msg}")
|
|
373
|
+
|
|
374
|
+
return handler
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _compile_rollup(
|
|
378
|
+
slot: ValueSlot,
|
|
379
|
+
slot_node_id: str,
|
|
380
|
+
expr: Any,
|
|
381
|
+
owner: PartInstance,
|
|
382
|
+
graph: DependencyGraph,
|
|
383
|
+
handlers: dict[str, Callable],
|
|
384
|
+
) -> None:
|
|
385
|
+
graph.add_node(
|
|
386
|
+
DependencyNode(
|
|
387
|
+
slot_node_id,
|
|
388
|
+
NodeKind.ATTRIBUTE_VALUE,
|
|
389
|
+
slot_id=slot.stable_id,
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
expr_node_id = f"rollup:{slot.path_string}"
|
|
394
|
+
graph.add_node(
|
|
395
|
+
DependencyNode(
|
|
396
|
+
expr_node_id,
|
|
397
|
+
NodeKind.ROLLUP_COMPUTATION,
|
|
398
|
+
slot_id=slot.stable_id,
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
graph.add_edge(expr_node_id, slot_node_id)
|
|
402
|
+
|
|
403
|
+
child_slots: list[str] = []
|
|
404
|
+
for child in owner.children:
|
|
405
|
+
try:
|
|
406
|
+
target_slot = expr.value_func(child)
|
|
407
|
+
if isinstance(target_slot, ValueSlot):
|
|
408
|
+
dep_node_id = f"val:{target_slot.path_string}"
|
|
409
|
+
graph.add_edge(dep_node_id, expr_node_id)
|
|
410
|
+
child_slots.append(dep_node_id)
|
|
411
|
+
except AttributeError:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
from tg_model.execution.rollups import build_rollup_handler
|
|
415
|
+
|
|
416
|
+
handlers[expr_node_id] = build_rollup_handler(expr.kind, expr.value_func, child_slots)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _resolve_symbol_for_requirement_acceptance(
|
|
420
|
+
sym: Any,
|
|
421
|
+
allocate_target: PartInstance,
|
|
422
|
+
requirement_definition_type: type,
|
|
423
|
+
_model: ConfiguredModel,
|
|
424
|
+
alloc: AllocationBinding,
|
|
425
|
+
) -> ValueSlot:
|
|
426
|
+
"""Map a unitflow symbol to a :class:`ValueSlot` for requirement acceptance (Phase 7).
|
|
427
|
+
|
|
428
|
+
If :class:`~tg_model.execution.connection_bindings.AllocationBinding` carries
|
|
429
|
+
``input_bindings`` (from ``allocate(..., inputs=…)``), symbols owned by the configured root
|
|
430
|
+
whose path matches ``<requirement instance path tail> + (input_name,)`` resolve to the bound
|
|
431
|
+
part slot.
|
|
432
|
+
|
|
433
|
+
Otherwise: symbols declared on the **same type as the allocate() call site** use paths from
|
|
434
|
+
that type's root (e.g. ``('motor', 'shaft_power')``); we strip the allocate target's path
|
|
435
|
+
under the configured root and resolve the remainder from ``allocate_target``.
|
|
436
|
+
|
|
437
|
+
Symbols declared on the **allocate target's part type** (e.g. attributes in ``Motor.define``)
|
|
438
|
+
use paths relative to that part (e.g. ``('shaft_power',)``) and resolve directly from
|
|
439
|
+
``allocate_target``.
|
|
440
|
+
|
|
441
|
+
Requirement-local :meth:`~tg_model.model.definition_context.ModelDefinitionContext.requirement_attribute`
|
|
442
|
+
values are registered as :class:`~tg_model.execution.value_slots.ValueSlot` objects on the
|
|
443
|
+
configured root and resolved via ``path_registry`` when not covered by ``inputs=`` bindings.
|
|
444
|
+
"""
|
|
445
|
+
from tg_model.model.refs import _symbol_id_to_path
|
|
446
|
+
|
|
447
|
+
result = _symbol_id_to_path.get(id(sym))
|
|
448
|
+
if result is not None:
|
|
449
|
+
sym_owner, tg_path = result
|
|
450
|
+
req_inst = alloc.requirement
|
|
451
|
+
if sym_owner is requirement_definition_type and isinstance(req_inst, ElementInstance):
|
|
452
|
+
rtail = tuple(req_inst.instance_path[1:])
|
|
453
|
+
if len(tg_path) == len(rtail) + 1 and tg_path[: len(rtail)] == rtail:
|
|
454
|
+
iname = tg_path[-1]
|
|
455
|
+
if alloc.input_bindings:
|
|
456
|
+
bound = alloc.input_bindings.get(iname)
|
|
457
|
+
if bound is not None:
|
|
458
|
+
return bound
|
|
459
|
+
full_key = ".".join((req_inst.instance_path[0], *tg_path))
|
|
460
|
+
hit = _model.path_registry.get(full_key)
|
|
461
|
+
if isinstance(hit, ValueSlot):
|
|
462
|
+
return hit
|
|
463
|
+
raise GraphCompilationError(
|
|
464
|
+
f"Requirement acceptance symbol {iname!r} at path {tg_path!r} has no "
|
|
465
|
+
f"matching entry in allocate(..., inputs=...) or requirement_attribute slot"
|
|
466
|
+
)
|
|
467
|
+
current: Any = allocate_target
|
|
468
|
+
if sym_owner is allocate_target.definition_type:
|
|
469
|
+
rel = tg_path
|
|
470
|
+
elif sym_owner is requirement_definition_type:
|
|
471
|
+
anchor = tuple(allocate_target.instance_path[1:])
|
|
472
|
+
if len(tg_path) < len(anchor) or tg_path[: len(anchor)] != anchor:
|
|
473
|
+
raise GraphCompilationError(
|
|
474
|
+
f"Symbol '{getattr(sym, 'name', '?')}' path {tg_path!r} is not under "
|
|
475
|
+
f"allocate target path {anchor!r} from configured root"
|
|
476
|
+
)
|
|
477
|
+
rel = tg_path[len(anchor) :]
|
|
478
|
+
else:
|
|
479
|
+
raise GraphCompilationError(
|
|
480
|
+
f"Symbol '{getattr(sym, 'name', '?')}' is owned by {sym_owner.__name__}; "
|
|
481
|
+
f"requirement acceptance allows symbols from {requirement_definition_type.__name__} "
|
|
482
|
+
f"or from allocate target type {allocate_target.definition_type.__name__} only."
|
|
483
|
+
)
|
|
484
|
+
try:
|
|
485
|
+
for segment in rel:
|
|
486
|
+
current = getattr(current, segment)
|
|
487
|
+
if isinstance(current, ValueSlot):
|
|
488
|
+
return current
|
|
489
|
+
except AttributeError:
|
|
490
|
+
pass
|
|
491
|
+
raise GraphCompilationError(
|
|
492
|
+
f"Symbol '{getattr(sym, 'name', '?')}' path {tg_path!r} "
|
|
493
|
+
f"could not be resolved under '{allocate_target.path_string}' for requirement acceptance"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
raise GraphCompilationError(
|
|
497
|
+
f"Symbol '{getattr(sym, 'name', '?')}' is not a canonical AttributeRef-derived symbol. "
|
|
498
|
+
f"All expression symbols must originate from model.attribute() or model.parameter() refs."
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _resolve_symbol_to_slot(
|
|
503
|
+
sym: Any,
|
|
504
|
+
owner: PartInstance,
|
|
505
|
+
model: ConfiguredModel,
|
|
506
|
+
) -> ValueSlot:
|
|
507
|
+
"""Resolve a unitflow Symbol to its corresponding ValueSlot.
|
|
508
|
+
|
|
509
|
+
Uses the canonical symbol-id registry from AttributeRef.sym.
|
|
510
|
+
When the symbol belongs to a different type than ``owner`` (e.g. a root
|
|
511
|
+
system parameter referenced via :func:`~tg_model.model.definition_context.parameter_ref`),
|
|
512
|
+
resolution falls back to ``model.root``.
|
|
513
|
+
|
|
514
|
+
Fails loudly if the symbol cannot be resolved — silent misbinding
|
|
515
|
+
is not acceptable in a safety-critical context.
|
|
516
|
+
"""
|
|
517
|
+
from tg_model.model.refs import _symbol_id_to_path
|
|
518
|
+
|
|
519
|
+
result = _symbol_id_to_path.get(id(sym))
|
|
520
|
+
if result is not None:
|
|
521
|
+
_owner_type, tg_path = result
|
|
522
|
+
current: Any = owner
|
|
523
|
+
try:
|
|
524
|
+
for segment in tg_path:
|
|
525
|
+
current = getattr(current, segment)
|
|
526
|
+
if isinstance(current, ValueSlot):
|
|
527
|
+
return current
|
|
528
|
+
except AttributeError:
|
|
529
|
+
pass
|
|
530
|
+
if _owner_type is not owner.definition_type and _owner_type is model.root.definition_type:
|
|
531
|
+
current = model.root
|
|
532
|
+
try:
|
|
533
|
+
for segment in tg_path:
|
|
534
|
+
current = getattr(current, segment)
|
|
535
|
+
if isinstance(current, ValueSlot):
|
|
536
|
+
return current
|
|
537
|
+
except AttributeError:
|
|
538
|
+
pass
|
|
539
|
+
raise GraphCompilationError(
|
|
540
|
+
f"Symbol '{getattr(sym, 'name', '?')}' has registered path {tg_path} "
|
|
541
|
+
f"but could not be resolved under '{owner.path_string}'"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
raise GraphCompilationError(
|
|
545
|
+
f"Symbol '{getattr(sym, 'name', '?')}' is not a canonical AttributeRef-derived symbol. "
|
|
546
|
+
f"All expression symbols must originate from model.attribute() or model.parameter() refs."
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _resolve_sym_for_requirement_expr(
|
|
551
|
+
sym: Any,
|
|
552
|
+
allocate_target: PartInstance,
|
|
553
|
+
requirement_definition_type: type,
|
|
554
|
+
model: ConfiguredModel,
|
|
555
|
+
alloc: AllocationBinding,
|
|
556
|
+
) -> ValueSlot:
|
|
557
|
+
"""Resolve a symbol for :meth:`requirement_attribute` expressions (inputs, derived, root, part)."""
|
|
558
|
+
from tg_model.model.refs import _symbol_id_to_path
|
|
559
|
+
|
|
560
|
+
info = _symbol_id_to_path.get(id(sym))
|
|
561
|
+
if info is None:
|
|
562
|
+
raise GraphCompilationError(
|
|
563
|
+
f"Symbol '{getattr(sym, 'name', '?')}' is not a canonical AttributeRef-derived symbol."
|
|
564
|
+
)
|
|
565
|
+
sym_owner, tg_path = info
|
|
566
|
+
req_inst = alloc.requirement
|
|
567
|
+
if isinstance(req_inst, ElementInstance) and sym_owner is requirement_definition_type:
|
|
568
|
+
rtail = tuple(req_inst.instance_path[1:])
|
|
569
|
+
if len(tg_path) == len(rtail) + 1 and tg_path[: len(rtail)] == rtail:
|
|
570
|
+
return _resolve_symbol_for_requirement_acceptance(
|
|
571
|
+
sym,
|
|
572
|
+
allocate_target,
|
|
573
|
+
requirement_definition_type,
|
|
574
|
+
model,
|
|
575
|
+
alloc,
|
|
576
|
+
)
|
|
577
|
+
if sym_owner is model.root.definition_type:
|
|
578
|
+
return _resolve_symbol_to_slot(sym, model.root, model)
|
|
579
|
+
if sym_owner is allocate_target.definition_type:
|
|
580
|
+
return _resolve_symbol_to_slot(sym, allocate_target, model)
|
|
581
|
+
raise GraphCompilationError(
|
|
582
|
+
f"Symbol '{getattr(sym, 'name', '?')}' cannot be resolved for requirement_attribute "
|
|
583
|
+
f"(owner {sym_owner.__name__} is not root, allocate target, or requirement namespace)."
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _compile_requirement_attribute_slot(
|
|
588
|
+
slot: ValueSlot,
|
|
589
|
+
alloc: AllocationBinding,
|
|
590
|
+
graph: DependencyGraph,
|
|
591
|
+
handlers: dict[str, Callable],
|
|
592
|
+
model: ConfiguredModel,
|
|
593
|
+
) -> None:
|
|
594
|
+
"""Compile one derived requirement slot (same graph pattern as :func:`_compile_slot`)."""
|
|
595
|
+
target = alloc.target
|
|
596
|
+
if not isinstance(target, PartInstance):
|
|
597
|
+
raise GraphCompilationError(
|
|
598
|
+
f"requirement_attribute needs allocate target PartInstance; got {type(target).__name__}"
|
|
599
|
+
)
|
|
600
|
+
req = alloc.requirement
|
|
601
|
+
if not isinstance(req, ElementInstance) or req.kind != "requirement":
|
|
602
|
+
raise GraphCompilationError("requirement_attribute allocation must reference a requirement instance")
|
|
603
|
+
req_owner_type = req.definition_type
|
|
604
|
+
|
|
605
|
+
expr = slot.metadata.get("_expr")
|
|
606
|
+
if expr is None:
|
|
607
|
+
graph.add_node(
|
|
608
|
+
DependencyNode(
|
|
609
|
+
f"val:{slot.path_string}",
|
|
610
|
+
NodeKind.ATTRIBUTE_VALUE,
|
|
611
|
+
slot_id=slot.stable_id,
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
slot_node_id = f"val:{slot.path_string}"
|
|
617
|
+
graph.add_node(
|
|
618
|
+
DependencyNode(
|
|
619
|
+
slot_node_id,
|
|
620
|
+
NodeKind.ATTRIBUTE_VALUE,
|
|
621
|
+
slot_id=slot.stable_id,
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
expr_node_id = f"expr:{slot.path_string}"
|
|
625
|
+
graph.add_node(
|
|
626
|
+
DependencyNode(
|
|
627
|
+
expr_node_id,
|
|
628
|
+
NodeKind.LOCAL_EXPRESSION,
|
|
629
|
+
slot_id=slot.stable_id,
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
graph.add_edge(expr_node_id, slot_node_id)
|
|
633
|
+
|
|
634
|
+
if hasattr(expr, "free_symbols") and expr.free_symbols:
|
|
635
|
+
for sym in expr.free_symbols:
|
|
636
|
+
dep_slot = _resolve_sym_for_requirement_expr(sym, target, req_owner_type, model, alloc)
|
|
637
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
638
|
+
graph.add_edge(dep_node_id, expr_node_id)
|
|
639
|
+
|
|
640
|
+
def make_handler(expression: Any, a: AllocationBinding, cm: ConfiguredModel) -> Callable:
|
|
641
|
+
def handler(dep_values: dict[str, Any]) -> Any:
|
|
642
|
+
context: dict[Any, Any] = {}
|
|
643
|
+
tgt = _alloc_target_as_part(a.target, where="requirement_attribute expression")
|
|
644
|
+
for sym in expression.free_symbols:
|
|
645
|
+
dep_slot = _resolve_sym_for_requirement_expr(
|
|
646
|
+
sym,
|
|
647
|
+
tgt,
|
|
648
|
+
a.requirement.definition_type,
|
|
649
|
+
cm,
|
|
650
|
+
a,
|
|
651
|
+
)
|
|
652
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
653
|
+
if dep_node_id in dep_values:
|
|
654
|
+
context[sym] = dep_values[dep_node_id]
|
|
655
|
+
return expression.evaluate(context)
|
|
656
|
+
|
|
657
|
+
return handler
|
|
658
|
+
|
|
659
|
+
handlers[expr_node_id] = make_handler(expr, alloc, model)
|
|
660
|
+
elif hasattr(expr, "evaluate"):
|
|
661
|
+
handlers[expr_node_id] = lambda dep_values, e=expr: e.evaluate({})
|
|
662
|
+
elif callable(expr):
|
|
663
|
+
handlers[expr_node_id] = lambda dep_values, fn=expr: fn(dep_values)
|
|
664
|
+
else:
|
|
665
|
+
handlers[expr_node_id] = lambda dep_values, val=expr: val
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _compile_requirement_derived_slots(
|
|
669
|
+
model: ConfiguredModel,
|
|
670
|
+
graph: DependencyGraph,
|
|
671
|
+
handlers: dict[str, Callable],
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Compile :meth:`requirement_attribute` value slots (before requirement acceptance checks)."""
|
|
674
|
+
from collections import defaultdict
|
|
675
|
+
|
|
676
|
+
if not model.requirement_value_slots:
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
by_req: dict[str, list[AllocationBinding]] = defaultdict(list)
|
|
680
|
+
for a in model.allocations:
|
|
681
|
+
r = a.requirement
|
|
682
|
+
if isinstance(r, ElementInstance) and r.kind == "requirement":
|
|
683
|
+
by_req[r.path_string].append(a)
|
|
684
|
+
|
|
685
|
+
grouped: dict[str, list[ValueSlot]] = defaultdict(list)
|
|
686
|
+
for slot in model.requirement_value_slots:
|
|
687
|
+
parent = ".".join(slot.instance_path[:-1])
|
|
688
|
+
grouped[parent].append(slot)
|
|
689
|
+
|
|
690
|
+
for req_path, slots in grouped.items():
|
|
691
|
+
allocs = by_req.get(req_path)
|
|
692
|
+
if not allocs:
|
|
693
|
+
raise GraphCompilationError(
|
|
694
|
+
f"requirement_attribute slot(s) under {req_path!r} require a matching allocate(...) edge"
|
|
695
|
+
)
|
|
696
|
+
if len(allocs) > 1:
|
|
697
|
+
raise GraphCompilationError(
|
|
698
|
+
f"requirement {req_path!r} has requirement_attribute-derived slots but multiple "
|
|
699
|
+
f"allocate(...) edges; use a single allocation for derived requirement attributes."
|
|
700
|
+
)
|
|
701
|
+
alloc = allocs[0]
|
|
702
|
+
req = alloc.requirement
|
|
703
|
+
if not isinstance(req, ElementInstance):
|
|
704
|
+
raise GraphCompilationError("requirement allocation invalid for requirement_attribute compile")
|
|
705
|
+
order = list(req.metadata.get("_requirement_attribute_names") or [])
|
|
706
|
+
rank = {n: i for i, n in enumerate(order)}
|
|
707
|
+
slots_sorted = sorted(slots, key=lambda s: rank.get(s.instance_path[-1], 10_000))
|
|
708
|
+
for slot in slots_sorted:
|
|
709
|
+
_compile_requirement_attribute_slot(slot, alloc, graph, handlers, model)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _compile_requirement_packages_from_parts(
|
|
713
|
+
part: PartInstance,
|
|
714
|
+
graph: DependencyGraph,
|
|
715
|
+
handlers: dict[str, Callable],
|
|
716
|
+
model: ConfiguredModel,
|
|
717
|
+
) -> None:
|
|
718
|
+
"""Compile value slots and constraints declared on composable requirement packages."""
|
|
719
|
+
compiled = part.definition_type.compile()
|
|
720
|
+
tr = compiled.get("_type_registry", {})
|
|
721
|
+
for name, node in compiled.get("nodes", {}).items():
|
|
722
|
+
if node.get("kind") != "requirement_block" or tr.get(name) is None:
|
|
723
|
+
continue
|
|
724
|
+
sub = getattr(part, name, None)
|
|
725
|
+
if isinstance(sub, RequirementPackageInstance):
|
|
726
|
+
_compile_requirement_package_tree(sub, graph, handlers, model)
|
|
727
|
+
for child in part.children:
|
|
728
|
+
_compile_requirement_packages_from_parts(child, graph, handlers, model)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _compile_requirement_package_tree(
|
|
732
|
+
pkg: RequirementPackageInstance,
|
|
733
|
+
graph: DependencyGraph,
|
|
734
|
+
handlers: dict[str, Callable],
|
|
735
|
+
model: ConfiguredModel,
|
|
736
|
+
) -> None:
|
|
737
|
+
from tg_model.model.declarations.values import RollupDecl
|
|
738
|
+
|
|
739
|
+
compiled = pkg.package_type.compile()
|
|
740
|
+
# Declaration order matches Python 3.7+ dict insertion order (same as compile_type recording).
|
|
741
|
+
for name, node in compiled["nodes"].items():
|
|
742
|
+
kind = node["kind"]
|
|
743
|
+
if kind in ("parameter", "attribute"):
|
|
744
|
+
slot = getattr(pkg, name)
|
|
745
|
+
if not isinstance(slot, ValueSlot):
|
|
746
|
+
raise GraphCompilationError(
|
|
747
|
+
f"Expected ValueSlot for {pkg.path_string}.{name}, got {type(slot).__name__}"
|
|
748
|
+
)
|
|
749
|
+
if slot.metadata.get("_computed_by") is not None:
|
|
750
|
+
raise GraphCompilationError(
|
|
751
|
+
f"computed_by= is not supported on requirement package slot '{slot.path_string}'"
|
|
752
|
+
)
|
|
753
|
+
expr_m = slot.metadata.get("_expr")
|
|
754
|
+
if isinstance(expr_m, RollupDecl):
|
|
755
|
+
raise GraphCompilationError(
|
|
756
|
+
f"RollupDecl is not supported on requirement package slot '{slot.path_string}'"
|
|
757
|
+
)
|
|
758
|
+
_compile_slot(slot, model.root, graph, handlers, model)
|
|
759
|
+
elif kind == "constraint":
|
|
760
|
+
_compile_requirement_package_constraint(pkg, name, node, graph, handlers, model)
|
|
761
|
+
elif kind == "requirement_block":
|
|
762
|
+
inner = getattr(pkg, name, None)
|
|
763
|
+
if isinstance(inner, RequirementPackageInstance):
|
|
764
|
+
_compile_requirement_package_tree(inner, graph, handlers, model)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _compile_requirement_package_constraint(
|
|
768
|
+
pkg: RequirementPackageInstance,
|
|
769
|
+
name: str,
|
|
770
|
+
node: dict[str, Any],
|
|
771
|
+
graph: DependencyGraph,
|
|
772
|
+
handlers: dict[str, Callable],
|
|
773
|
+
model: ConfiguredModel,
|
|
774
|
+
) -> None:
|
|
775
|
+
constraint_node_id = f"check:{pkg.path_string}.{name}"
|
|
776
|
+
graph.add_node(
|
|
777
|
+
DependencyNode(
|
|
778
|
+
constraint_node_id,
|
|
779
|
+
NodeKind.CONSTRAINT_CHECK,
|
|
780
|
+
metadata={"name": f"{pkg.path_string}.{name}"},
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
expr = node.get("metadata", {}).get("_expr")
|
|
784
|
+
if expr is None:
|
|
785
|
+
raise GraphCompilationError(
|
|
786
|
+
f"Requirement package constraint '{pkg.path_string}.{name}' has no expr "
|
|
787
|
+
f"(expected compile-time validation to reject this)."
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
def make_pkg_constraint_handler(
|
|
791
|
+
constraint_expr: Any,
|
|
792
|
+
cm: ConfiguredModel,
|
|
793
|
+
) -> Callable[..., bool]:
|
|
794
|
+
def handler(dep_values: dict[str, Any]) -> bool:
|
|
795
|
+
context: dict[Any, Any] = {}
|
|
796
|
+
for sym in constraint_expr.free_symbols:
|
|
797
|
+
dep_slot = _resolve_symbol_to_slot(sym, cm.root, cm)
|
|
798
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
799
|
+
if dep_node_id in dep_values:
|
|
800
|
+
context[sym] = dep_values[dep_node_id]
|
|
801
|
+
return constraint_expr.evaluate(context)
|
|
802
|
+
|
|
803
|
+
return handler
|
|
804
|
+
|
|
805
|
+
if hasattr(expr, "free_symbols") and expr.free_symbols:
|
|
806
|
+
for sym in expr.free_symbols:
|
|
807
|
+
dep_slot = _resolve_symbol_to_slot(sym, model.root, model)
|
|
808
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
809
|
+
graph.add_edge(dep_node_id, constraint_node_id)
|
|
810
|
+
handlers[constraint_node_id] = make_pkg_constraint_handler(expr, model)
|
|
811
|
+
elif hasattr(expr, "evaluate"):
|
|
812
|
+
handlers[constraint_node_id] = lambda dep_values, e=expr: bool(e.evaluate({}))
|
|
813
|
+
elif callable(expr):
|
|
814
|
+
handlers[constraint_node_id] = lambda dep_values, fn=expr: bool(fn(dep_values))
|
|
815
|
+
else:
|
|
816
|
+
handlers[constraint_node_id] = lambda dep_values, val=expr: bool(val)
|
|
817
|
+
|
|
818
|
+
# Constant / symbol-free constraints need at least one incoming edge so validation does not
|
|
819
|
+
# treat the check node as orphaned, and so evaluation runs after package inputs exist.
|
|
820
|
+
if not graph.dependencies_of(constraint_node_id):
|
|
821
|
+
anchor = _first_value_slot_under_requirement_package(pkg)
|
|
822
|
+
if anchor is not None:
|
|
823
|
+
graph.add_edge(f"val:{anchor.path_string}", constraint_node_id)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _compile_constraints_for_part(
|
|
827
|
+
part: PartInstance,
|
|
828
|
+
graph: DependencyGraph,
|
|
829
|
+
handlers: dict[str, Callable],
|
|
830
|
+
model: ConfiguredModel,
|
|
831
|
+
) -> None:
|
|
832
|
+
compiled = part.definition_type.compile()
|
|
833
|
+
|
|
834
|
+
for name, node in compiled.get("nodes", {}).items():
|
|
835
|
+
if node["kind"] != "constraint":
|
|
836
|
+
continue
|
|
837
|
+
|
|
838
|
+
constraint_node_id = f"check:{part.path_string}.{name}"
|
|
839
|
+
graph.add_node(
|
|
840
|
+
DependencyNode(
|
|
841
|
+
constraint_node_id,
|
|
842
|
+
NodeKind.CONSTRAINT_CHECK,
|
|
843
|
+
metadata={"name": f"{part.path_string}.{name}"},
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
expr = node["metadata"].get("_expr")
|
|
848
|
+
if expr is not None and hasattr(expr, "free_symbols"):
|
|
849
|
+
for sym in expr.free_symbols:
|
|
850
|
+
dep_slot = _resolve_symbol_to_slot(sym, part, model)
|
|
851
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
852
|
+
graph.add_edge(dep_node_id, constraint_node_id)
|
|
853
|
+
|
|
854
|
+
def make_constraint_handler(
|
|
855
|
+
constraint_expr: Any,
|
|
856
|
+
owner_part: PartInstance,
|
|
857
|
+
cm: ConfiguredModel,
|
|
858
|
+
) -> Callable[..., bool]:
|
|
859
|
+
def handler(dep_values: dict[str, Any]) -> bool:
|
|
860
|
+
context = {}
|
|
861
|
+
for sym in constraint_expr.free_symbols:
|
|
862
|
+
dep_slot = _resolve_symbol_to_slot(sym, owner_part, cm)
|
|
863
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
864
|
+
if dep_node_id in dep_values:
|
|
865
|
+
context[sym] = dep_values[dep_node_id]
|
|
866
|
+
return constraint_expr.evaluate(context)
|
|
867
|
+
|
|
868
|
+
return handler
|
|
869
|
+
|
|
870
|
+
handlers[constraint_node_id] = make_constraint_handler(expr, part, model)
|
|
871
|
+
|
|
872
|
+
for child in part.children:
|
|
873
|
+
_compile_constraints_for_part(child, graph, handlers, model)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _compile_requirement_acceptance(
|
|
877
|
+
model: ConfiguredModel,
|
|
878
|
+
graph: DependencyGraph,
|
|
879
|
+
handlers: dict[str, Callable],
|
|
880
|
+
) -> None:
|
|
881
|
+
"""Add constraint-check nodes for requirements with ``_accept_expr`` per allocation (Phase 7).
|
|
882
|
+
|
|
883
|
+
Uses :func:`_resolve_symbol_for_requirement_acceptance` (not :func:`_resolve_symbol_to_slot`):
|
|
884
|
+
symbols may be authored on the requirement owner type (system path prefix stripped to the
|
|
885
|
+
allocate target) or on the allocate target's part type. Same ``Evaluator`` / graph kinds as
|
|
886
|
+
part constraints; resolution rules differ.
|
|
887
|
+
"""
|
|
888
|
+
for alloc in model.allocations:
|
|
889
|
+
req = alloc.requirement
|
|
890
|
+
if not isinstance(req, ElementInstance) or req.kind != "requirement":
|
|
891
|
+
continue
|
|
892
|
+
expr = req.metadata.get("_accept_expr")
|
|
893
|
+
if expr is None:
|
|
894
|
+
continue
|
|
895
|
+
target = alloc.target
|
|
896
|
+
if not isinstance(target, PartInstance):
|
|
897
|
+
raise GraphCompilationError(
|
|
898
|
+
f"Requirement acceptance for '{req.path_string}' needs allocate target to be a "
|
|
899
|
+
f"PartInstance; got {type(target).__name__} at '{target.path_string}'"
|
|
900
|
+
)
|
|
901
|
+
req_def_type = req.definition_type
|
|
902
|
+
node_id = f"reqcheck:{req.path_string}@{alloc.stable_id}"
|
|
903
|
+
graph.add_node(
|
|
904
|
+
DependencyNode(
|
|
905
|
+
node_id,
|
|
906
|
+
NodeKind.CONSTRAINT_CHECK,
|
|
907
|
+
metadata={
|
|
908
|
+
"name": node_id,
|
|
909
|
+
"requirement_path": req.path_string,
|
|
910
|
+
"allocation_target_path": target.path_string,
|
|
911
|
+
"check_kind": "requirement_acceptance",
|
|
912
|
+
},
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
if not hasattr(expr, "free_symbols"):
|
|
916
|
+
raise GraphCompilationError(
|
|
917
|
+
f"Requirement '{req.path_string}' acceptance expr has no free_symbols; "
|
|
918
|
+
"use the same expression types as model.constraint(expr=...)."
|
|
919
|
+
)
|
|
920
|
+
for sym in expr.free_symbols:
|
|
921
|
+
dep_slot = _resolve_symbol_for_requirement_acceptance(
|
|
922
|
+
sym,
|
|
923
|
+
target,
|
|
924
|
+
req_def_type,
|
|
925
|
+
model,
|
|
926
|
+
alloc,
|
|
927
|
+
)
|
|
928
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
929
|
+
graph.add_edge(dep_node_id, node_id)
|
|
930
|
+
if not expr.free_symbols:
|
|
931
|
+
anchor = _first_value_graph_node_id_under_part(target)
|
|
932
|
+
if anchor is None:
|
|
933
|
+
raise GraphCompilationError(
|
|
934
|
+
f"Requirement '{req.path_string}' has a constant acceptance expr but allocate "
|
|
935
|
+
f"target '{target.path_string}' has no value slots to order evaluation after."
|
|
936
|
+
)
|
|
937
|
+
graph.add_edge(anchor, node_id)
|
|
938
|
+
|
|
939
|
+
def make_req_accept_handler(
|
|
940
|
+
constraint_expr: Any,
|
|
941
|
+
owner_part: PartInstance,
|
|
942
|
+
cm: ConfiguredModel,
|
|
943
|
+
req_owner_type: type,
|
|
944
|
+
binding: AllocationBinding,
|
|
945
|
+
) -> Callable[..., bool]:
|
|
946
|
+
def handler(dep_values: dict[str, Any]) -> bool:
|
|
947
|
+
context: dict[Any, Any] = {}
|
|
948
|
+
for sym in constraint_expr.free_symbols:
|
|
949
|
+
dep_slot = _resolve_symbol_for_requirement_acceptance(
|
|
950
|
+
sym,
|
|
951
|
+
owner_part,
|
|
952
|
+
req_owner_type,
|
|
953
|
+
cm,
|
|
954
|
+
binding,
|
|
955
|
+
)
|
|
956
|
+
dep_node_id = f"val:{dep_slot.path_string}"
|
|
957
|
+
if dep_node_id in dep_values:
|
|
958
|
+
context[sym] = dep_values[dep_node_id]
|
|
959
|
+
return constraint_expr.evaluate(context)
|
|
960
|
+
|
|
961
|
+
return handler
|
|
962
|
+
|
|
963
|
+
handlers[node_id] = make_req_accept_handler(expr, target, model, req_def_type, alloc)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _first_value_graph_node_id_under_part(part: PartInstance) -> str | None:
|
|
967
|
+
"""First ``val:...`` node id in a deterministic DFS under ``part`` (for ordering-only edges)."""
|
|
968
|
+
for slot in part.value_slots:
|
|
969
|
+
return f"val:{slot.path_string}"
|
|
970
|
+
for child in part.children:
|
|
971
|
+
found = _first_value_graph_node_id_under_part(child)
|
|
972
|
+
if found is not None:
|
|
973
|
+
return found
|
|
974
|
+
return None
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _resolve_path_to_slot(
|
|
978
|
+
path: list[str],
|
|
979
|
+
part: PartInstance,
|
|
980
|
+
group_name: str,
|
|
981
|
+
) -> ValueSlot:
|
|
982
|
+
"""Resolve a declaration path to its corresponding ValueSlot under a part."""
|
|
983
|
+
current: Any = part
|
|
984
|
+
try:
|
|
985
|
+
for seg in path:
|
|
986
|
+
current = getattr(current, seg)
|
|
987
|
+
if isinstance(current, ValueSlot):
|
|
988
|
+
return current
|
|
989
|
+
except AttributeError:
|
|
990
|
+
pass
|
|
991
|
+
raise GraphCompilationError(
|
|
992
|
+
f"Solve group '{group_name}': path {list(path)} could not be resolved to a ValueSlot under '{part.path_string}'"
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _compile_solve_groups_for_part(
|
|
997
|
+
part: PartInstance,
|
|
998
|
+
graph: DependencyGraph,
|
|
999
|
+
handlers: dict[str, Callable],
|
|
1000
|
+
model: ConfiguredModel,
|
|
1001
|
+
) -> None:
|
|
1002
|
+
compiled = part.definition_type.compile()
|
|
1003
|
+
|
|
1004
|
+
for name, node in compiled.get("nodes", {}).items():
|
|
1005
|
+
if node["kind"] != "solve_group":
|
|
1006
|
+
continue
|
|
1007
|
+
|
|
1008
|
+
sg_node_id = f"solve:{part.path_string}.{name}"
|
|
1009
|
+
meta = node.get("metadata", {})
|
|
1010
|
+
|
|
1011
|
+
unknown_paths = meta.get("_unknowns", [])
|
|
1012
|
+
given_paths = meta.get("_givens", [])
|
|
1013
|
+
equations = meta.get("_equations", [])
|
|
1014
|
+
|
|
1015
|
+
unknown_slot_ids: set[str] = set()
|
|
1016
|
+
unknown_slot_by_path: dict[tuple[str, ...], str] = {}
|
|
1017
|
+
for upath in unknown_paths:
|
|
1018
|
+
slot = _resolve_path_to_slot(upath, part, name)
|
|
1019
|
+
if slot.stable_id in unknown_slot_ids:
|
|
1020
|
+
raise GraphCompilationError(f"Solve group '{name}': duplicate unknown '{'.'.join(upath)}'")
|
|
1021
|
+
unknown_slot_ids.add(slot.stable_id)
|
|
1022
|
+
unknown_slot_by_path[tuple(upath)] = slot.stable_id
|
|
1023
|
+
|
|
1024
|
+
given_slot_ids: set[str] = set()
|
|
1025
|
+
for gpath in given_paths:
|
|
1026
|
+
slot = _resolve_path_to_slot(gpath, part, name)
|
|
1027
|
+
if slot.stable_id in given_slot_ids:
|
|
1028
|
+
raise GraphCompilationError(f"Solve group '{name}': duplicate given '{'.'.join(gpath)}'")
|
|
1029
|
+
if slot.stable_id in unknown_slot_ids:
|
|
1030
|
+
raise GraphCompilationError(
|
|
1031
|
+
f"Solve group '{name}': '{'.'.join(gpath)}' declared as both unknown and given"
|
|
1032
|
+
)
|
|
1033
|
+
given_slot_ids.add(slot.stable_id)
|
|
1034
|
+
|
|
1035
|
+
target_slots = {sid: sid for sid in unknown_slot_ids}
|
|
1036
|
+
graph.add_node(
|
|
1037
|
+
DependencyNode(
|
|
1038
|
+
sg_node_id,
|
|
1039
|
+
NodeKind.SOLVE_GROUP,
|
|
1040
|
+
metadata={"name": name, "target_slots": target_slots},
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
unknown_syms: list[Any] = []
|
|
1045
|
+
given_syms: list[Any] = []
|
|
1046
|
+
given_to_node_id: dict[Any, str] = {}
|
|
1047
|
+
found_unknown_ids: set[str] = set()
|
|
1048
|
+
found_given_ids: set[str] = set()
|
|
1049
|
+
|
|
1050
|
+
for eq in equations:
|
|
1051
|
+
if not hasattr(eq, "free_symbols"):
|
|
1052
|
+
continue
|
|
1053
|
+
for sym in eq.free_symbols:
|
|
1054
|
+
if any(s is sym for s in unknown_syms) or any(s is sym for s in given_syms):
|
|
1055
|
+
continue
|
|
1056
|
+
slot = _resolve_symbol_to_slot(sym, part, model)
|
|
1057
|
+
dep_node_id = f"val:{slot.path_string}"
|
|
1058
|
+
|
|
1059
|
+
if slot.stable_id in unknown_slot_ids:
|
|
1060
|
+
unknown_syms.append(sym)
|
|
1061
|
+
found_unknown_ids.add(slot.stable_id)
|
|
1062
|
+
graph.add_edge(sg_node_id, dep_node_id)
|
|
1063
|
+
elif slot.stable_id in given_slot_ids:
|
|
1064
|
+
given_syms.append(sym)
|
|
1065
|
+
found_given_ids.add(slot.stable_id)
|
|
1066
|
+
given_to_node_id[sym] = dep_node_id
|
|
1067
|
+
graph.add_edge(dep_node_id, sg_node_id)
|
|
1068
|
+
else:
|
|
1069
|
+
raise GraphCompilationError(
|
|
1070
|
+
f"Solve group '{name}': symbol '{getattr(sym, 'name', '?')}' "
|
|
1071
|
+
f"resolves to slot '{slot.path_string}' which is not declared "
|
|
1072
|
+
f"as unknown or given."
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
missing_unknowns = unknown_slot_ids - found_unknown_ids
|
|
1076
|
+
if missing_unknowns:
|
|
1077
|
+
raise GraphCompilationError(
|
|
1078
|
+
f"Solve group '{name}': declared unknowns not found in any equation. "
|
|
1079
|
+
f"Missing slot IDs: {missing_unknowns}"
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
missing_givens = given_slot_ids - found_given_ids
|
|
1083
|
+
if missing_givens:
|
|
1084
|
+
raise GraphCompilationError(
|
|
1085
|
+
f"Solve group '{name}': declared givens not found in any equation. Missing slot IDs: {missing_givens}"
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
sym_to_slot_id: dict[int, str] = {}
|
|
1089
|
+
for sym in unknown_syms:
|
|
1090
|
+
from tg_model.model.refs import _symbol_id_to_path
|
|
1091
|
+
|
|
1092
|
+
result = _symbol_id_to_path.get(id(sym))
|
|
1093
|
+
if result is not None:
|
|
1094
|
+
_, sym_path = result
|
|
1095
|
+
slot_id = unknown_slot_by_path.get(tuple(sym_path))
|
|
1096
|
+
if slot_id is not None:
|
|
1097
|
+
sym_to_slot_id[id(sym)] = slot_id
|
|
1098
|
+
|
|
1099
|
+
from tg_model.execution.solve_groups import build_solve_group_handler
|
|
1100
|
+
|
|
1101
|
+
handlers[sg_node_id] = build_solve_group_handler(
|
|
1102
|
+
equations,
|
|
1103
|
+
unknown_syms,
|
|
1104
|
+
given_syms,
|
|
1105
|
+
given_to_node_id,
|
|
1106
|
+
sym_to_slot_id,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
for child in part.children:
|
|
1110
|
+
_compile_solve_groups_for_part(child, graph, handlers, model)
|