thundergraph-model 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. tg_model/__init__.py +46 -0
  2. tg_model/analysis/__init__.py +38 -0
  3. tg_model/analysis/_coherence.py +91 -0
  4. tg_model/analysis/compare_variants.py +197 -0
  5. tg_model/analysis/impact.py +101 -0
  6. tg_model/analysis/sweep.py +199 -0
  7. tg_model/execution/__init__.py +122 -0
  8. tg_model/execution/behavior.py +826 -0
  9. tg_model/execution/configured_model.py +754 -0
  10. tg_model/execution/connection_bindings.py +87 -0
  11. tg_model/execution/dependency_graph.py +217 -0
  12. tg_model/execution/evaluator.py +419 -0
  13. tg_model/execution/external_ops.py +153 -0
  14. tg_model/execution/graph_compiler.py +1110 -0
  15. tg_model/execution/instances.py +274 -0
  16. tg_model/execution/requirements.py +104 -0
  17. tg_model/execution/rollups.py +60 -0
  18. tg_model/execution/run_context.py +304 -0
  19. tg_model/execution/solve_groups.py +104 -0
  20. tg_model/execution/validation.py +211 -0
  21. tg_model/execution/value_slots.py +63 -0
  22. tg_model/export/__init__.py +7 -0
  23. tg_model/integrations/__init__.py +36 -0
  24. tg_model/integrations/external_compute.py +122 -0
  25. tg_model/model/__init__.py +39 -0
  26. tg_model/model/compile_types.py +896 -0
  27. tg_model/model/declarations/__init__.py +8 -0
  28. tg_model/model/declarations/behavior.py +37 -0
  29. tg_model/model/declarations/values.py +53 -0
  30. tg_model/model/definition_context.py +1742 -0
  31. tg_model/model/elements.py +159 -0
  32. tg_model/model/expr.py +75 -0
  33. tg_model/model/identity.py +63 -0
  34. tg_model/model/refs.py +319 -0
  35. thundergraph_model-1.0.0.dist-info/METADATA +82 -0
  36. thundergraph_model-1.0.0.dist-info/RECORD +38 -0
  37. thundergraph_model-1.0.0.dist-info/WHEEL +4 -0
  38. thundergraph_model-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -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