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,754 @@
|
|
|
1
|
+
"""Frozen configured topology: :class:`ConfiguredModel` and :func:`instantiate`.
|
|
2
|
+
|
|
3
|
+
A configured model holds the root :class:`~tg_model.execution.instances.PartInstance`,
|
|
4
|
+
registries of handles, structural connections, allocations, and references.
|
|
5
|
+
|
|
6
|
+
Per-run **values** (slot state for one evaluation) live in
|
|
7
|
+
:class:`~tg_model.execution.run_context.RunContext`, not on the model. The model may cache a
|
|
8
|
+
compiled dependency graph and handlers (see :meth:`ConfiguredModel.evaluate` and
|
|
9
|
+
:func:`~tg_model.execution.graph_compiler.compile_graph`) for reuse; that cache is **not**
|
|
10
|
+
per-run scenario data.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from tg_model.execution.evaluator import RunResult
|
|
19
|
+
from tg_model.execution.run_context import RunContext
|
|
20
|
+
|
|
21
|
+
from tg_model.execution.connection_bindings import (
|
|
22
|
+
AllocationBinding,
|
|
23
|
+
ConnectionBinding,
|
|
24
|
+
ReferenceBinding,
|
|
25
|
+
)
|
|
26
|
+
from tg_model.execution.instances import (
|
|
27
|
+
ElementInstance,
|
|
28
|
+
PartInstance,
|
|
29
|
+
PortInstance,
|
|
30
|
+
RequirementPackageInstance,
|
|
31
|
+
)
|
|
32
|
+
from tg_model.execution.value_slots import ValueSlot
|
|
33
|
+
from tg_model.model.compile_types import _requirement_block_compiled_artifact
|
|
34
|
+
from tg_model.model.identity import derive_declaration_id
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConfiguredModel:
|
|
38
|
+
"""Immutable configured topology for one root type instance.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
Evaluations use fresh :class:`~tg_model.execution.run_context.RunContext` objects per
|
|
43
|
+
:meth:`evaluate` call; the part tree and registries do not change. A successful compile may
|
|
44
|
+
be cached on this instance (``_compiled_graph``) for reuse.
|
|
45
|
+
|
|
46
|
+
**Thread safety:** Do not call :meth:`evaluate` or :func:`~tg_model.execution.graph_compiler.compile_graph`
|
|
47
|
+
concurrently on the **same** instance from multiple threads; the cache is not locked.
|
|
48
|
+
|
|
49
|
+
**Copy / pickle:** Caching a compiled graph on the instance means copying or unpickling a
|
|
50
|
+
configured model without clearing ``_compiled_graph`` is **unsupported**; treat cached graphs as
|
|
51
|
+
invalid across process or deep-copy boundaries unless you add an explicit clear or rebuild.
|
|
52
|
+
|
|
53
|
+
Attribute access delegates to the root part for ergonomics.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
root: PartInstance,
|
|
59
|
+
*,
|
|
60
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
61
|
+
id_registry: dict[str, ElementInstance | ValueSlot],
|
|
62
|
+
connections: list[ConnectionBinding],
|
|
63
|
+
allocations: list[AllocationBinding],
|
|
64
|
+
references: list[ReferenceBinding],
|
|
65
|
+
requirement_value_slots: list[ValueSlot] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Assemble a frozen topology (call via :func:`instantiate`, not directly).
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
root : PartInstance
|
|
72
|
+
Configured root part instance.
|
|
73
|
+
path_registry : dict
|
|
74
|
+
Maps dotted path strings to instances or slots.
|
|
75
|
+
id_registry : dict
|
|
76
|
+
Maps ``stable_id`` strings to instances or slots.
|
|
77
|
+
connections : list[ConnectionBinding]
|
|
78
|
+
Structural port connections.
|
|
79
|
+
allocations : list[AllocationBinding]
|
|
80
|
+
Resolved requirement allocations.
|
|
81
|
+
references : list[ReferenceBinding]
|
|
82
|
+
Resolved citation reference edges.
|
|
83
|
+
requirement_value_slots : list[ValueSlot], optional
|
|
84
|
+
Derived :class:`~tg_model.execution.value_slots.ValueSlot` nodes for
|
|
85
|
+
:meth:`tg_model.model.definition_context.ModelDefinitionContext.requirement_attribute`
|
|
86
|
+
declarations (registered in ``path_registry`` / ``id_registry``).
|
|
87
|
+
"""
|
|
88
|
+
self.root = root
|
|
89
|
+
self.path_registry = path_registry
|
|
90
|
+
self.id_registry = id_registry
|
|
91
|
+
self.connections = connections
|
|
92
|
+
self.allocations = allocations
|
|
93
|
+
self.references = references
|
|
94
|
+
self.requirement_value_slots = requirement_value_slots or []
|
|
95
|
+
#: Cached ``(DependencyGraph, handlers)`` from ``compile_graph``; lazily set.
|
|
96
|
+
self._compiled_graph: tuple[Any, Any] | None = None
|
|
97
|
+
|
|
98
|
+
def handle(self, path: str) -> ElementInstance | ValueSlot:
|
|
99
|
+
"""Look up an instance or value slot by dotted path string.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
path : str
|
|
104
|
+
Instance path such as ``Rocket.tank.mass_kg``.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
ElementInstance or ValueSlot
|
|
109
|
+
Registered topology object.
|
|
110
|
+
|
|
111
|
+
Raises
|
|
112
|
+
------
|
|
113
|
+
KeyError
|
|
114
|
+
If ``path`` is not in ``path_registry``.
|
|
115
|
+
"""
|
|
116
|
+
if path not in self.path_registry:
|
|
117
|
+
raise KeyError(f"No handle found for path '{path}'")
|
|
118
|
+
return self.path_registry[path]
|
|
119
|
+
|
|
120
|
+
def evaluate(
|
|
121
|
+
self,
|
|
122
|
+
inputs: dict[Any, Any] | None = None,
|
|
123
|
+
*,
|
|
124
|
+
run_context: RunContext | None = None,
|
|
125
|
+
validate: bool = True,
|
|
126
|
+
) -> RunResult:
|
|
127
|
+
"""Run one synchronous evaluation over the compiled dependency graph.
|
|
128
|
+
|
|
129
|
+
Compiles the graph on first use (same cache as :func:`~tg_model.execution.graph_compiler.compile_graph`),
|
|
130
|
+
optionally runs :func:`~tg_model.execution.validation.validate_graph`, then delegates to
|
|
131
|
+
:class:`~tg_model.execution.evaluator.Evaluator`.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
inputs : dict, optional
|
|
136
|
+
Per-run values keyed by :class:`~tg_model.execution.value_slots.ValueSlot` handles
|
|
137
|
+
belonging to this model, or by ``str`` giving the
|
|
138
|
+
:attr:`~tg_model.execution.value_slots.ValueSlot.stable_id` of such a slot **only**
|
|
139
|
+
(not arbitrary element or part ids). Values are typically :class:`unitflow.Quantity`
|
|
140
|
+
instances.
|
|
141
|
+
run_context : RunContext, optional
|
|
142
|
+
Fresh context per call by default. Supply only for advanced testing or tooling.
|
|
143
|
+
validate : bool, default True
|
|
144
|
+
When True, runs :func:`~tg_model.execution.validation.validate_graph` before **each**
|
|
145
|
+
evaluation (static checks). For tight loops, sweeps, or optimizers, pass
|
|
146
|
+
``validate=False`` after you have validated once out-of-band, to avoid repeating that
|
|
147
|
+
work every run. On validation failure, raises :class:`~tg_model.execution.validation.GraphValidationError`.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
RunResult
|
|
152
|
+
Same aggregate type as :meth:`tg_model.execution.evaluator.Evaluator.evaluate`.
|
|
153
|
+
Missing inputs, failed constraints, and other **runtime** issues appear in
|
|
154
|
+
``failures`` / ``constraint_results`` — not as exceptions from this method.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
GraphCompilationError
|
|
159
|
+
If graph compilation fails (from :func:`~tg_model.execution.graph_compiler.compile_graph`).
|
|
160
|
+
GraphValidationError
|
|
161
|
+
If ``validate`` is True and static validation fails (subclass of :class:`Exception`).
|
|
162
|
+
KeyError
|
|
163
|
+
If a string key is not present in this model's id registry.
|
|
164
|
+
TypeError
|
|
165
|
+
Propagated from the evaluator when an async external is used in sync mode.
|
|
166
|
+
ValueError
|
|
167
|
+
If an input key is a :class:`~tg_model.execution.value_slots.ValueSlot` not registered on
|
|
168
|
+
this model, or a string id that does not refer to a :class:`~tg_model.execution.value_slots.ValueSlot`.
|
|
169
|
+
|
|
170
|
+
See Also
|
|
171
|
+
--------
|
|
172
|
+
tg_model.execution.graph_compiler.compile_graph
|
|
173
|
+
tg_model.execution.evaluator.Evaluator
|
|
174
|
+
"""
|
|
175
|
+
from tg_model.execution.evaluator import Evaluator
|
|
176
|
+
from tg_model.execution.graph_compiler import compile_graph
|
|
177
|
+
from tg_model.execution.run_context import RunContext as FreshRunContext
|
|
178
|
+
from tg_model.execution.validation import GraphValidationError, validate_graph
|
|
179
|
+
|
|
180
|
+
graph, handlers = compile_graph(self)
|
|
181
|
+
if validate:
|
|
182
|
+
val = validate_graph(graph, configured_model=self)
|
|
183
|
+
if not val.passed:
|
|
184
|
+
msg = "; ".join(f"{f.category}: {f.message}" for f in val.failures)
|
|
185
|
+
raise GraphValidationError(
|
|
186
|
+
f"Graph validation failed: {msg}",
|
|
187
|
+
result=val,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
ctx = run_context if run_context is not None else FreshRunContext()
|
|
191
|
+
bound = _normalize_evaluate_inputs(self, inputs or {})
|
|
192
|
+
ev = Evaluator(graph, compute_handlers=handlers)
|
|
193
|
+
return ev.evaluate(ctx, inputs=bound)
|
|
194
|
+
|
|
195
|
+
def __getattr__(self, name: str) -> Any:
|
|
196
|
+
if name.startswith("_"):
|
|
197
|
+
raise AttributeError(name)
|
|
198
|
+
return getattr(self.root, name)
|
|
199
|
+
|
|
200
|
+
def __repr__(self) -> str:
|
|
201
|
+
return (
|
|
202
|
+
f"<ConfiguredModel: {self.root.path_string} "
|
|
203
|
+
f"({len(self.path_registry)} handles, {len(self.connections)} connections, "
|
|
204
|
+
f"{len(self.references)} references)>"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def instantiate(root_type: type) -> ConfiguredModel:
|
|
209
|
+
"""Build a :class:`ConfiguredModel` from a compiled root type.
|
|
210
|
+
|
|
211
|
+
Walks the compiled definition depth-first, creating
|
|
212
|
+
:class:`~tg_model.execution.instances.PartInstance`,
|
|
213
|
+
:class:`~tg_model.execution.instances.PortInstance`,
|
|
214
|
+
:class:`~tg_model.execution.instances.RequirementPackageInstance` (composable requirement
|
|
215
|
+
packages with package-level value slots),
|
|
216
|
+
:class:`~tg_model.execution.value_slots.ValueSlot`, connection bindings, and
|
|
217
|
+
allocation bindings. Registers handles then freezes all parts.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
root_type : type
|
|
222
|
+
Compiled :class:`~tg_model.model.elements.System` / :class:`~tg_model.model.elements.Part` subclass.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
ConfiguredModel
|
|
227
|
+
Frozen topology ready for :func:`~tg_model.execution.graph_compiler.compile_graph`.
|
|
228
|
+
|
|
229
|
+
Notes
|
|
230
|
+
-----
|
|
231
|
+
Stable IDs derive from the configured root type plus full instance path so identities
|
|
232
|
+
stay unique regardless of which intermediate type owns a declaration.
|
|
233
|
+
|
|
234
|
+
See Also
|
|
235
|
+
--------
|
|
236
|
+
tg_model.execution.graph_compiler.compile_graph
|
|
237
|
+
tg_model.execution.evaluator.Evaluator
|
|
238
|
+
"""
|
|
239
|
+
compiled = root_type.compile()
|
|
240
|
+
path_registry: dict[str, ElementInstance | ValueSlot] = {}
|
|
241
|
+
id_registry: dict[str, ElementInstance | ValueSlot] = {}
|
|
242
|
+
requirement_value_slots: list[ValueSlot] = []
|
|
243
|
+
|
|
244
|
+
root_path = (root_type.__name__,)
|
|
245
|
+
root_id = derive_declaration_id(root_type, *root_path)
|
|
246
|
+
root_instance = PartInstance(
|
|
247
|
+
stable_id=root_id,
|
|
248
|
+
definition_type=root_type,
|
|
249
|
+
definition_path=(),
|
|
250
|
+
instance_path=root_path,
|
|
251
|
+
)
|
|
252
|
+
_register(root_instance, path_registry, id_registry)
|
|
253
|
+
|
|
254
|
+
ref_accumulator: list[ReferenceBinding] = []
|
|
255
|
+
_instantiate_children(
|
|
256
|
+
root_instance,
|
|
257
|
+
compiled,
|
|
258
|
+
root_type,
|
|
259
|
+
path_registry,
|
|
260
|
+
id_registry,
|
|
261
|
+
ref_accumulator,
|
|
262
|
+
requirement_value_slots,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
connections = _instantiate_connections(compiled, root_instance, path_registry, root_type)
|
|
266
|
+
allocations = _instantiate_allocations(compiled, root_instance, path_registry, root_type)
|
|
267
|
+
references = _instantiate_all_references(root_instance, path_registry, root_type) + ref_accumulator
|
|
268
|
+
|
|
269
|
+
root_instance.freeze()
|
|
270
|
+
|
|
271
|
+
return ConfiguredModel(
|
|
272
|
+
root=root_instance,
|
|
273
|
+
path_registry=path_registry,
|
|
274
|
+
id_registry=id_registry,
|
|
275
|
+
connections=connections,
|
|
276
|
+
allocations=allocations,
|
|
277
|
+
references=references,
|
|
278
|
+
requirement_value_slots=requirement_value_slots,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _instantiate_children(
|
|
283
|
+
parent: PartInstance,
|
|
284
|
+
compiled: dict[str, Any],
|
|
285
|
+
root_type: type,
|
|
286
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
287
|
+
id_registry: dict[str, ElementInstance | ValueSlot],
|
|
288
|
+
ref_accumulator: list[ReferenceBinding] | None = None,
|
|
289
|
+
requirement_value_slots: list[ValueSlot] | None = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Walk compiled nodes and create child instances under parent."""
|
|
292
|
+
type_registry: dict[str, type] = compiled.get("_type_registry", {})
|
|
293
|
+
|
|
294
|
+
for name, node in compiled["nodes"].items():
|
|
295
|
+
kind = node["kind"]
|
|
296
|
+
metadata = node.get("metadata", {})
|
|
297
|
+
child_path = (*parent.instance_path, name)
|
|
298
|
+
child_id = derive_declaration_id(root_type, *child_path)
|
|
299
|
+
|
|
300
|
+
if kind == "part":
|
|
301
|
+
child_type = type_registry.get(name)
|
|
302
|
+
child_instance = PartInstance(
|
|
303
|
+
stable_id=child_id,
|
|
304
|
+
definition_type=child_type or type(parent),
|
|
305
|
+
definition_path=(name,),
|
|
306
|
+
instance_path=child_path,
|
|
307
|
+
metadata=metadata,
|
|
308
|
+
)
|
|
309
|
+
parent.add_child(name, child_instance)
|
|
310
|
+
_register(child_instance, path_registry, id_registry)
|
|
311
|
+
|
|
312
|
+
if child_type is not None:
|
|
313
|
+
child_compiled = child_type.compile()
|
|
314
|
+
_instantiate_children(
|
|
315
|
+
child_instance,
|
|
316
|
+
child_compiled,
|
|
317
|
+
root_type,
|
|
318
|
+
path_registry,
|
|
319
|
+
id_registry,
|
|
320
|
+
ref_accumulator,
|
|
321
|
+
requirement_value_slots,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
elif kind == "port":
|
|
325
|
+
port_instance = PortInstance(
|
|
326
|
+
stable_id=child_id,
|
|
327
|
+
definition_type=parent.definition_type,
|
|
328
|
+
definition_path=(name,),
|
|
329
|
+
instance_path=child_path,
|
|
330
|
+
metadata=metadata,
|
|
331
|
+
)
|
|
332
|
+
parent.add_port(name, port_instance)
|
|
333
|
+
_register(port_instance, path_registry, id_registry)
|
|
334
|
+
|
|
335
|
+
elif kind in ("attribute", "parameter"):
|
|
336
|
+
slot = ValueSlot(
|
|
337
|
+
stable_id=child_id,
|
|
338
|
+
instance_path=child_path,
|
|
339
|
+
kind=kind,
|
|
340
|
+
definition_type=parent.definition_type,
|
|
341
|
+
definition_path=(name,),
|
|
342
|
+
metadata=metadata,
|
|
343
|
+
has_expr="_expr" in metadata,
|
|
344
|
+
has_computed_by="_computed_by" in metadata,
|
|
345
|
+
)
|
|
346
|
+
parent.add_value_slot(name, slot)
|
|
347
|
+
_register_slot(slot, path_registry, id_registry)
|
|
348
|
+
|
|
349
|
+
elif kind == "requirement":
|
|
350
|
+
req_instance = ElementInstance(
|
|
351
|
+
stable_id=child_id,
|
|
352
|
+
definition_type=parent.definition_type,
|
|
353
|
+
definition_path=(name,),
|
|
354
|
+
instance_path=child_path,
|
|
355
|
+
kind="requirement",
|
|
356
|
+
metadata=metadata,
|
|
357
|
+
)
|
|
358
|
+
_register(req_instance, path_registry, id_registry)
|
|
359
|
+
|
|
360
|
+
elif kind == "requirement_block":
|
|
361
|
+
block_type = type_registry.get(name)
|
|
362
|
+
if block_type is None:
|
|
363
|
+
raise ValueError(f"requirement_block {name!r} has no target_type (internal compile error)")
|
|
364
|
+
pkg_inst = RequirementPackageInstance(
|
|
365
|
+
stable_id=child_id,
|
|
366
|
+
definition_type=root_type,
|
|
367
|
+
definition_path=(name,),
|
|
368
|
+
instance_path=child_path,
|
|
369
|
+
package_type=block_type,
|
|
370
|
+
metadata=metadata,
|
|
371
|
+
)
|
|
372
|
+
parent.add_requirement_package(name, pkg_inst)
|
|
373
|
+
_register(pkg_inst, path_registry, id_registry)
|
|
374
|
+
_instantiate_requirement_block_children(
|
|
375
|
+
pkg_inst,
|
|
376
|
+
_requirement_block_compiled_artifact(block_type),
|
|
377
|
+
root_type,
|
|
378
|
+
root_type,
|
|
379
|
+
path_registry,
|
|
380
|
+
id_registry,
|
|
381
|
+
ref_accumulator,
|
|
382
|
+
requirement_value_slots,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
elif kind == "constraint":
|
|
386
|
+
constraint_instance = ElementInstance(
|
|
387
|
+
stable_id=child_id,
|
|
388
|
+
definition_type=parent.definition_type,
|
|
389
|
+
definition_path=(name,),
|
|
390
|
+
instance_path=child_path,
|
|
391
|
+
kind="constraint",
|
|
392
|
+
metadata=metadata,
|
|
393
|
+
)
|
|
394
|
+
_register(constraint_instance, path_registry, id_registry)
|
|
395
|
+
|
|
396
|
+
elif kind == "citation":
|
|
397
|
+
cite_instance = ElementInstance(
|
|
398
|
+
stable_id=child_id,
|
|
399
|
+
definition_type=parent.definition_type,
|
|
400
|
+
definition_path=(name,),
|
|
401
|
+
instance_path=child_path,
|
|
402
|
+
kind="citation",
|
|
403
|
+
metadata=metadata,
|
|
404
|
+
)
|
|
405
|
+
_register(cite_instance, path_registry, id_registry)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _instantiate_requirement_block_children(
|
|
409
|
+
package: RequirementPackageInstance,
|
|
410
|
+
compiled: dict[str, Any],
|
|
411
|
+
definition_root_type: type,
|
|
412
|
+
root_type: type,
|
|
413
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
414
|
+
id_registry: dict[str, ElementInstance | ValueSlot],
|
|
415
|
+
ref_accumulator: list[ReferenceBinding] | None = None,
|
|
416
|
+
requirement_value_slots: list[ValueSlot] | None = None,
|
|
417
|
+
) -> None:
|
|
418
|
+
"""Materialize members under a composable requirement package (dot-access under the root part)."""
|
|
419
|
+
type_registry: dict[str, type] = compiled.get("_type_registry", {})
|
|
420
|
+
prefix_path = package.instance_path
|
|
421
|
+
|
|
422
|
+
for name, node in compiled["nodes"].items():
|
|
423
|
+
kind = node["kind"]
|
|
424
|
+
metadata = node.get("metadata", {})
|
|
425
|
+
child_path = (*prefix_path, name)
|
|
426
|
+
child_id = derive_declaration_id(root_type, *child_path)
|
|
427
|
+
|
|
428
|
+
if kind == "requirement":
|
|
429
|
+
req_instance = ElementInstance(
|
|
430
|
+
stable_id=child_id,
|
|
431
|
+
definition_type=definition_root_type,
|
|
432
|
+
definition_path=tuple(child_path[1:]),
|
|
433
|
+
instance_path=child_path,
|
|
434
|
+
kind="requirement",
|
|
435
|
+
metadata=metadata,
|
|
436
|
+
)
|
|
437
|
+
package.add_member(name, req_instance)
|
|
438
|
+
_register(req_instance, path_registry, id_registry)
|
|
439
|
+
elif kind == "citation":
|
|
440
|
+
cite_instance = ElementInstance(
|
|
441
|
+
stable_id=child_id,
|
|
442
|
+
definition_type=definition_root_type,
|
|
443
|
+
definition_path=tuple(child_path[1:]),
|
|
444
|
+
instance_path=child_path,
|
|
445
|
+
kind="citation",
|
|
446
|
+
metadata=metadata,
|
|
447
|
+
)
|
|
448
|
+
package.add_member(name, cite_instance)
|
|
449
|
+
_register(cite_instance, path_registry, id_registry)
|
|
450
|
+
elif kind == "requirement_block":
|
|
451
|
+
block_type = type_registry.get(name)
|
|
452
|
+
if block_type is None:
|
|
453
|
+
raise ValueError(f"requirement_block {name!r} has no target_type (internal compile error)")
|
|
454
|
+
inner_pkg = RequirementPackageInstance(
|
|
455
|
+
stable_id=child_id,
|
|
456
|
+
definition_type=definition_root_type,
|
|
457
|
+
definition_path=tuple(child_path[1:]),
|
|
458
|
+
instance_path=child_path,
|
|
459
|
+
package_type=block_type,
|
|
460
|
+
metadata=metadata,
|
|
461
|
+
)
|
|
462
|
+
package.add_member(name, inner_pkg)
|
|
463
|
+
_register(inner_pkg, path_registry, id_registry)
|
|
464
|
+
_instantiate_requirement_block_children(
|
|
465
|
+
inner_pkg,
|
|
466
|
+
_requirement_block_compiled_artifact(block_type),
|
|
467
|
+
definition_root_type,
|
|
468
|
+
root_type,
|
|
469
|
+
path_registry,
|
|
470
|
+
id_registry,
|
|
471
|
+
ref_accumulator,
|
|
472
|
+
requirement_value_slots,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
elif kind in ("parameter", "attribute"):
|
|
476
|
+
slot = ValueSlot(
|
|
477
|
+
stable_id=child_id,
|
|
478
|
+
instance_path=child_path,
|
|
479
|
+
kind=kind,
|
|
480
|
+
definition_type=definition_root_type,
|
|
481
|
+
definition_path=tuple(child_path[1:]),
|
|
482
|
+
metadata=metadata,
|
|
483
|
+
has_expr="_expr" in metadata,
|
|
484
|
+
has_computed_by="_computed_by" in metadata,
|
|
485
|
+
)
|
|
486
|
+
package.add_member(name, slot)
|
|
487
|
+
_register_slot(slot, path_registry, id_registry)
|
|
488
|
+
|
|
489
|
+
elif kind == "constraint":
|
|
490
|
+
constraint_instance = ElementInstance(
|
|
491
|
+
stable_id=child_id,
|
|
492
|
+
definition_type=definition_root_type,
|
|
493
|
+
definition_path=tuple(child_path[1:]),
|
|
494
|
+
instance_path=child_path,
|
|
495
|
+
kind="constraint",
|
|
496
|
+
metadata=metadata,
|
|
497
|
+
)
|
|
498
|
+
package.add_member(name, constraint_instance)
|
|
499
|
+
_register(constraint_instance, path_registry, id_registry)
|
|
500
|
+
|
|
501
|
+
elif kind == "requirement_attribute":
|
|
502
|
+
if requirement_value_slots is None:
|
|
503
|
+
raise ValueError("requirement_attribute nodes require requirement_value_slots accumulator")
|
|
504
|
+
req_key = metadata["_requirement_key"]
|
|
505
|
+
aname = metadata["_attr_name"]
|
|
506
|
+
slot_path = (*prefix_path, req_key, aname)
|
|
507
|
+
meta = dict(metadata)
|
|
508
|
+
meta["_requirement_derived"] = True
|
|
509
|
+
slot = ValueSlot(
|
|
510
|
+
stable_id=child_id,
|
|
511
|
+
instance_path=slot_path,
|
|
512
|
+
kind="attribute",
|
|
513
|
+
definition_type=definition_root_type,
|
|
514
|
+
definition_path=tuple(slot_path[1:]),
|
|
515
|
+
metadata=meta,
|
|
516
|
+
has_expr="_expr" in meta,
|
|
517
|
+
)
|
|
518
|
+
_register_slot(slot, path_registry, id_registry)
|
|
519
|
+
requirement_value_slots.append(slot)
|
|
520
|
+
|
|
521
|
+
if ref_accumulator is not None:
|
|
522
|
+
_wire_requirement_block_references(
|
|
523
|
+
prefix_path,
|
|
524
|
+
compiled,
|
|
525
|
+
root_type,
|
|
526
|
+
path_registry,
|
|
527
|
+
ref_accumulator,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _wire_requirement_block_references(
|
|
532
|
+
block_instance_path: tuple[str, ...],
|
|
533
|
+
compiled: dict[str, Any],
|
|
534
|
+
root_type: type,
|
|
535
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
536
|
+
out: list[ReferenceBinding],
|
|
537
|
+
) -> None:
|
|
538
|
+
"""Bind ``references`` edges authored inside a :class:`~tg_model.model.elements.Requirement` package."""
|
|
539
|
+
for edge in compiled.get("edges", []):
|
|
540
|
+
if edge.get("kind") != "references":
|
|
541
|
+
continue
|
|
542
|
+
src_path = block_instance_path + tuple(edge["source"]["path"])
|
|
543
|
+
tgt_path = block_instance_path + tuple(edge["target"]["path"])
|
|
544
|
+
src_key = ".".join(src_path)
|
|
545
|
+
tgt_key = ".".join(tgt_path)
|
|
546
|
+
src = path_registry.get(src_key)
|
|
547
|
+
tgt = path_registry.get(tgt_key)
|
|
548
|
+
if src is None:
|
|
549
|
+
raise ValueError(f"references source '{src_key}' not found in registry")
|
|
550
|
+
if tgt is None:
|
|
551
|
+
raise ValueError(f"references citation '{tgt_key}' not found in registry")
|
|
552
|
+
if not isinstance(tgt, ElementInstance) or tgt.kind != "citation":
|
|
553
|
+
raise ValueError(f"references target '{tgt_key}' is not a citation ElementInstance")
|
|
554
|
+
ref_id = derive_declaration_id(
|
|
555
|
+
root_type,
|
|
556
|
+
"references",
|
|
557
|
+
*[str(x) for x in src_path],
|
|
558
|
+
*[str(x) for x in tgt_path],
|
|
559
|
+
)
|
|
560
|
+
out.append(
|
|
561
|
+
ReferenceBinding(
|
|
562
|
+
stable_id=ref_id,
|
|
563
|
+
source=src,
|
|
564
|
+
citation=tgt,
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _instantiate_connections(
|
|
570
|
+
compiled: dict[str, Any],
|
|
571
|
+
root: PartInstance,
|
|
572
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
573
|
+
root_type: type,
|
|
574
|
+
) -> list[ConnectionBinding]:
|
|
575
|
+
"""Resolve compiled connection edges into ConnectionBindings."""
|
|
576
|
+
connections: list[ConnectionBinding] = []
|
|
577
|
+
|
|
578
|
+
for edge in compiled.get("edges", []):
|
|
579
|
+
if edge["kind"] != "connect":
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
src_path = root.instance_path + tuple(edge["source"]["path"])
|
|
583
|
+
tgt_path = root.instance_path + tuple(edge["target"]["path"])
|
|
584
|
+
src_key = ".".join(src_path)
|
|
585
|
+
tgt_key = ".".join(tgt_path)
|
|
586
|
+
|
|
587
|
+
src = path_registry.get(src_key)
|
|
588
|
+
tgt = path_registry.get(tgt_key)
|
|
589
|
+
|
|
590
|
+
if not isinstance(src, PortInstance):
|
|
591
|
+
raise ValueError(f"Connection source '{src_key}' is not a PortInstance")
|
|
592
|
+
if not isinstance(tgt, PortInstance):
|
|
593
|
+
raise ValueError(f"Connection target '{tgt_key}' is not a PortInstance")
|
|
594
|
+
|
|
595
|
+
conn_id = derive_declaration_id(root_type, "connect", *edge["source"]["path"], *edge["target"]["path"])
|
|
596
|
+
connections.append(
|
|
597
|
+
ConnectionBinding(
|
|
598
|
+
stable_id=conn_id,
|
|
599
|
+
source=src,
|
|
600
|
+
target=tgt,
|
|
601
|
+
carrying=edge.get("carrying"),
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return connections
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _instantiate_allocations(
|
|
609
|
+
compiled: dict[str, Any],
|
|
610
|
+
root: PartInstance,
|
|
611
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
612
|
+
root_type: type,
|
|
613
|
+
) -> list[AllocationBinding]:
|
|
614
|
+
"""Resolve compiled allocation edges into AllocationBindings."""
|
|
615
|
+
allocations: list[AllocationBinding] = []
|
|
616
|
+
|
|
617
|
+
for edge in compiled.get("edges", []):
|
|
618
|
+
if edge["kind"] != "allocate":
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
req_path = root.instance_path + tuple(edge["source"]["path"])
|
|
622
|
+
tgt_path = root.instance_path + tuple(edge["target"]["path"])
|
|
623
|
+
req_key = ".".join(req_path)
|
|
624
|
+
tgt_key = ".".join(tgt_path)
|
|
625
|
+
|
|
626
|
+
req = path_registry.get(req_key)
|
|
627
|
+
tgt = path_registry.get(tgt_key)
|
|
628
|
+
|
|
629
|
+
if req is None:
|
|
630
|
+
raise ValueError(f"Allocation requirement '{req_key}' not found in registry")
|
|
631
|
+
if tgt is None:
|
|
632
|
+
raise ValueError(f"Allocation target '{tgt_key}' not found in registry")
|
|
633
|
+
if not isinstance(req, ElementInstance):
|
|
634
|
+
raise ValueError(f"Allocation requirement '{req_key}' is not an ElementInstance")
|
|
635
|
+
if not isinstance(tgt, ElementInstance):
|
|
636
|
+
raise ValueError(f"Allocation target '{tgt_key}' is not an ElementInstance")
|
|
637
|
+
|
|
638
|
+
input_bindings: dict[str, ValueSlot] = {}
|
|
639
|
+
raw_inputs = edge.get("_allocate_inputs")
|
|
640
|
+
if raw_inputs:
|
|
641
|
+
for iname, spec in raw_inputs.items():
|
|
642
|
+
rel = tuple(spec["path"])
|
|
643
|
+
slot_key = ".".join((root.path_string, *rel))
|
|
644
|
+
slot = path_registry.get(slot_key)
|
|
645
|
+
if not isinstance(slot, ValueSlot):
|
|
646
|
+
raise ValueError(f"allocate inputs[{iname!r}] path {slot_key!r} is not a ValueSlot in registry")
|
|
647
|
+
input_bindings[str(iname)] = slot
|
|
648
|
+
|
|
649
|
+
alloc_id = derive_declaration_id(root_type, "allocate", *edge["source"]["path"], *edge["target"]["path"])
|
|
650
|
+
allocations.append(
|
|
651
|
+
AllocationBinding(
|
|
652
|
+
stable_id=alloc_id,
|
|
653
|
+
requirement=req,
|
|
654
|
+
target=tgt,
|
|
655
|
+
input_bindings=input_bindings,
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return allocations
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _instantiate_all_references(
|
|
663
|
+
root: PartInstance,
|
|
664
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
665
|
+
root_type: type,
|
|
666
|
+
) -> list[ReferenceBinding]:
|
|
667
|
+
"""Resolve ``references`` edges from every part type in the instance tree (Phase 8)."""
|
|
668
|
+
out: list[ReferenceBinding] = []
|
|
669
|
+
stack: list[PartInstance] = [root]
|
|
670
|
+
while stack:
|
|
671
|
+
part = stack.pop()
|
|
672
|
+
compiled = part.definition_type.compile()
|
|
673
|
+
for edge in compiled.get("edges", []):
|
|
674
|
+
if edge["kind"] != "references":
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
src_path = part.instance_path + tuple(edge["source"]["path"])
|
|
678
|
+
tgt_path = part.instance_path + tuple(edge["target"]["path"])
|
|
679
|
+
src_key = ".".join(src_path)
|
|
680
|
+
tgt_key = ".".join(tgt_path)
|
|
681
|
+
|
|
682
|
+
src = path_registry.get(src_key)
|
|
683
|
+
tgt = path_registry.get(tgt_key)
|
|
684
|
+
|
|
685
|
+
if src is None:
|
|
686
|
+
raise ValueError(f"references source '{src_key}' not found in registry")
|
|
687
|
+
if tgt is None:
|
|
688
|
+
raise ValueError(f"references citation '{tgt_key}' not found in registry")
|
|
689
|
+
if not isinstance(tgt, ElementInstance) or tgt.kind != "citation":
|
|
690
|
+
raise ValueError(f"references target '{tgt_key}' is not a citation ElementInstance")
|
|
691
|
+
|
|
692
|
+
ref_id = derive_declaration_id(
|
|
693
|
+
root_type,
|
|
694
|
+
"references",
|
|
695
|
+
*[str(x) for x in src_path],
|
|
696
|
+
*[str(x) for x in tgt_path],
|
|
697
|
+
)
|
|
698
|
+
out.append(
|
|
699
|
+
ReferenceBinding(
|
|
700
|
+
stable_id=ref_id,
|
|
701
|
+
source=src,
|
|
702
|
+
citation=tgt,
|
|
703
|
+
)
|
|
704
|
+
)
|
|
705
|
+
stack.extend(part.children)
|
|
706
|
+
|
|
707
|
+
return out
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _register(
|
|
711
|
+
instance: ElementInstance,
|
|
712
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
713
|
+
id_registry: dict[str, ElementInstance | ValueSlot],
|
|
714
|
+
) -> None:
|
|
715
|
+
path_registry[instance.path_string] = instance
|
|
716
|
+
id_registry[instance.stable_id] = instance
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _register_slot(
|
|
720
|
+
slot: ValueSlot,
|
|
721
|
+
path_registry: dict[str, ElementInstance | ValueSlot],
|
|
722
|
+
id_registry: dict[str, ElementInstance | ValueSlot],
|
|
723
|
+
) -> None:
|
|
724
|
+
path_registry[slot.path_string] = slot
|
|
725
|
+
id_registry[slot.stable_id] = slot
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _normalize_evaluate_inputs(model: ConfiguredModel, inputs: dict[Any, Any]) -> dict[str, Any]:
|
|
729
|
+
"""Map ``ValueSlot`` / slot-id ``str`` keys to ``stable_id`` strings for ``Evaluator``."""
|
|
730
|
+
out: dict[str, Any] = {}
|
|
731
|
+
for key, value in inputs.items():
|
|
732
|
+
if isinstance(key, ValueSlot):
|
|
733
|
+
reg = model.id_registry.get(key.stable_id)
|
|
734
|
+
if reg is not key:
|
|
735
|
+
raise ValueError(
|
|
736
|
+
f"ValueSlot {key.path_string!r} is not registered on this ConfiguredModel "
|
|
737
|
+
"(foreign slot or stale handle).",
|
|
738
|
+
)
|
|
739
|
+
out[key.stable_id] = value
|
|
740
|
+
elif isinstance(key, str):
|
|
741
|
+
reg = model.id_registry.get(key)
|
|
742
|
+
if reg is None:
|
|
743
|
+
raise KeyError(f"Unknown stable_id {key!r} for this ConfiguredModel")
|
|
744
|
+
if not isinstance(reg, ValueSlot):
|
|
745
|
+
raise ValueError(
|
|
746
|
+
f"String key {key!r} refers to {type(reg).__name__}, not a ValueSlot; "
|
|
747
|
+
"use ValueSlot handles or the stable_id of a parameter/attribute slot.",
|
|
748
|
+
)
|
|
749
|
+
out[key] = value
|
|
750
|
+
else:
|
|
751
|
+
raise TypeError(
|
|
752
|
+
f"Input keys must be ValueSlot or str (slot stable_id), got {type(key).__name__}",
|
|
753
|
+
)
|
|
754
|
+
return out
|