"""Frozen configured topology: :class:`ConfiguredModel` and :func:`instantiate`.
A configured model holds the root :class:`~tg_model.execution.instances.PartInstance`,
registries of handles, structural connections, allocations, and references.
Per-run **values** (slot state for one evaluation) live in
:class:`~tg_model.execution.run_context.RunContext`, not on the model. The model may cache a
compiled dependency graph and handlers (see :meth:`ConfiguredModel.evaluate` and
:func:`~tg_model.execution.graph_compiler.compile_graph`) for reuse; that cache is **not**
per-run scenario data.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from tg_model.execution.evaluator import RunResult
from tg_model.execution.run_context import RunContext
from tg_model.execution.connection_bindings import (
AllocationBinding,
ConnectionBinding,
ReferenceBinding,
)
from tg_model.execution.instances import (
ElementInstance,
PartInstance,
PortInstance,
RequirementPackageInstance,
)
from tg_model.execution.value_slots import ValueSlot
from tg_model.model.compile_types import _requirement_block_compiled_artifact
from tg_model.model.identity import derive_declaration_id
[docs]
def instantiate(root_type: type) -> ConfiguredModel:
"""Build a :class:`ConfiguredModel` from a compiled root type.
Walks the compiled definition depth-first, creating
:class:`~tg_model.execution.instances.PartInstance`,
:class:`~tg_model.execution.instances.PortInstance`,
:class:`~tg_model.execution.instances.RequirementPackageInstance` (composable requirement
packages with package-level value slots),
:class:`~tg_model.execution.value_slots.ValueSlot`, connection bindings, and
allocation bindings. Registers handles then freezes all parts.
Parameters
----------
root_type : type
Compiled :class:`~tg_model.model.elements.System` / :class:`~tg_model.model.elements.Part` subclass.
Returns
-------
ConfiguredModel
Frozen topology ready for :func:`~tg_model.execution.graph_compiler.compile_graph`.
Notes
-----
Stable IDs derive from the configured root type plus full instance path so identities
stay unique regardless of which intermediate type owns a declaration.
See Also
--------
tg_model.execution.graph_compiler.compile_graph
tg_model.execution.evaluator.Evaluator
"""
compiled = root_type.compile()
path_registry: dict[str, ElementInstance | ValueSlot] = {}
id_registry: dict[str, ElementInstance | ValueSlot] = {}
requirement_value_slots: list[ValueSlot] = []
root_path = (root_type.__name__,)
root_id = derive_declaration_id(root_type, *root_path)
root_instance = PartInstance(
stable_id=root_id,
definition_type=root_type,
definition_path=(),
instance_path=root_path,
)
_register(root_instance, path_registry, id_registry)
ref_accumulator: list[ReferenceBinding] = []
_instantiate_children(
root_instance,
compiled,
root_type,
path_registry,
id_registry,
ref_accumulator,
requirement_value_slots,
)
connections = _instantiate_connections(compiled, root_instance, path_registry, root_type)
allocations = _instantiate_allocations(compiled, root_instance, path_registry, root_type)
references = _instantiate_all_references(root_instance, path_registry, root_type) + ref_accumulator
root_instance.freeze()
return ConfiguredModel(
root=root_instance,
path_registry=path_registry,
id_registry=id_registry,
connections=connections,
allocations=allocations,
references=references,
requirement_value_slots=requirement_value_slots,
)
def _instantiate_children(
parent: PartInstance,
compiled: dict[str, Any],
root_type: type,
path_registry: dict[str, ElementInstance | ValueSlot],
id_registry: dict[str, ElementInstance | ValueSlot],
ref_accumulator: list[ReferenceBinding] | None = None,
requirement_value_slots: list[ValueSlot] | None = None,
) -> None:
"""Walk compiled nodes and create child instances under parent."""
type_registry: dict[str, type] = compiled.get("_type_registry", {})
for name, node in compiled["nodes"].items():
kind = node["kind"]
metadata = node.get("metadata", {})
child_path = (*parent.instance_path, name)
child_id = derive_declaration_id(root_type, *child_path)
if kind == "part":
child_type = type_registry.get(name)
child_instance = PartInstance(
stable_id=child_id,
definition_type=child_type or type(parent),
definition_path=(name,),
instance_path=child_path,
metadata=metadata,
)
parent.add_child(name, child_instance)
_register(child_instance, path_registry, id_registry)
if child_type is not None:
child_compiled = child_type.compile()
_instantiate_children(
child_instance,
child_compiled,
root_type,
path_registry,
id_registry,
ref_accumulator,
requirement_value_slots,
)
elif kind == "port":
port_instance = PortInstance(
stable_id=child_id,
definition_type=parent.definition_type,
definition_path=(name,),
instance_path=child_path,
metadata=metadata,
)
parent.add_port(name, port_instance)
_register(port_instance, path_registry, id_registry)
elif kind in ("attribute", "parameter"):
slot = ValueSlot(
stable_id=child_id,
instance_path=child_path,
kind=kind,
definition_type=parent.definition_type,
definition_path=(name,),
metadata=metadata,
has_expr="_expr" in metadata,
has_computed_by="_computed_by" in metadata,
)
parent.add_value_slot(name, slot)
_register_slot(slot, path_registry, id_registry)
elif kind == "requirement":
req_instance = ElementInstance(
stable_id=child_id,
definition_type=parent.definition_type,
definition_path=(name,),
instance_path=child_path,
kind="requirement",
metadata=metadata,
)
_register(req_instance, path_registry, id_registry)
elif kind == "requirement_block":
block_type = type_registry.get(name)
if block_type is None:
raise ValueError(f"requirement_block {name!r} has no target_type (internal compile error)")
pkg_inst = RequirementPackageInstance(
stable_id=child_id,
definition_type=root_type,
definition_path=(name,),
instance_path=child_path,
package_type=block_type,
metadata=metadata,
)
parent.add_requirement_package(name, pkg_inst)
_register(pkg_inst, path_registry, id_registry)
_instantiate_requirement_block_children(
pkg_inst,
_requirement_block_compiled_artifact(block_type),
root_type,
root_type,
path_registry,
id_registry,
ref_accumulator,
requirement_value_slots,
)
elif kind == "constraint":
constraint_instance = ElementInstance(
stable_id=child_id,
definition_type=parent.definition_type,
definition_path=(name,),
instance_path=child_path,
kind="constraint",
metadata=metadata,
)
_register(constraint_instance, path_registry, id_registry)
elif kind == "citation":
cite_instance = ElementInstance(
stable_id=child_id,
definition_type=parent.definition_type,
definition_path=(name,),
instance_path=child_path,
kind="citation",
metadata=metadata,
)
_register(cite_instance, path_registry, id_registry)
def _instantiate_requirement_block_children(
package: RequirementPackageInstance,
compiled: dict[str, Any],
definition_root_type: type,
root_type: type,
path_registry: dict[str, ElementInstance | ValueSlot],
id_registry: dict[str, ElementInstance | ValueSlot],
ref_accumulator: list[ReferenceBinding] | None = None,
requirement_value_slots: list[ValueSlot] | None = None,
) -> None:
"""Materialize members under a composable requirement package (dot-access under the root part)."""
type_registry: dict[str, type] = compiled.get("_type_registry", {})
prefix_path = package.instance_path
for name, node in compiled["nodes"].items():
kind = node["kind"]
metadata = node.get("metadata", {})
child_path = (*prefix_path, name)
child_id = derive_declaration_id(root_type, *child_path)
if kind == "requirement":
req_instance = ElementInstance(
stable_id=child_id,
definition_type=definition_root_type,
definition_path=tuple(child_path[1:]),
instance_path=child_path,
kind="requirement",
metadata=metadata,
)
package.add_member(name, req_instance)
_register(req_instance, path_registry, id_registry)
elif kind == "citation":
cite_instance = ElementInstance(
stable_id=child_id,
definition_type=definition_root_type,
definition_path=tuple(child_path[1:]),
instance_path=child_path,
kind="citation",
metadata=metadata,
)
package.add_member(name, cite_instance)
_register(cite_instance, path_registry, id_registry)
elif kind == "requirement_block":
block_type = type_registry.get(name)
if block_type is None:
raise ValueError(f"requirement_block {name!r} has no target_type (internal compile error)")
inner_pkg = RequirementPackageInstance(
stable_id=child_id,
definition_type=definition_root_type,
definition_path=tuple(child_path[1:]),
instance_path=child_path,
package_type=block_type,
metadata=metadata,
)
package.add_member(name, inner_pkg)
_register(inner_pkg, path_registry, id_registry)
_instantiate_requirement_block_children(
inner_pkg,
_requirement_block_compiled_artifact(block_type),
definition_root_type,
root_type,
path_registry,
id_registry,
ref_accumulator,
requirement_value_slots,
)
elif kind in ("parameter", "attribute"):
slot = ValueSlot(
stable_id=child_id,
instance_path=child_path,
kind=kind,
definition_type=definition_root_type,
definition_path=tuple(child_path[1:]),
metadata=metadata,
has_expr="_expr" in metadata,
has_computed_by="_computed_by" in metadata,
)
package.add_member(name, slot)
_register_slot(slot, path_registry, id_registry)
elif kind == "constraint":
constraint_instance = ElementInstance(
stable_id=child_id,
definition_type=definition_root_type,
definition_path=tuple(child_path[1:]),
instance_path=child_path,
kind="constraint",
metadata=metadata,
)
package.add_member(name, constraint_instance)
_register(constraint_instance, path_registry, id_registry)
elif kind == "requirement_attribute":
if requirement_value_slots is None:
raise ValueError("requirement_attribute nodes require requirement_value_slots accumulator")
req_key = metadata["_requirement_key"]
aname = metadata["_attr_name"]
slot_path = (*prefix_path, req_key, aname)
meta = dict(metadata)
meta["_requirement_derived"] = True
slot = ValueSlot(
stable_id=child_id,
instance_path=slot_path,
kind="attribute",
definition_type=definition_root_type,
definition_path=tuple(slot_path[1:]),
metadata=meta,
has_expr="_expr" in meta,
)
_register_slot(slot, path_registry, id_registry)
requirement_value_slots.append(slot)
if ref_accumulator is not None:
_wire_requirement_block_references(
prefix_path,
compiled,
root_type,
path_registry,
ref_accumulator,
)
def _wire_requirement_block_references(
block_instance_path: tuple[str, ...],
compiled: dict[str, Any],
root_type: type,
path_registry: dict[str, ElementInstance | ValueSlot],
out: list[ReferenceBinding],
) -> None:
"""Bind ``references`` edges authored inside a :class:`~tg_model.model.elements.Requirement` package."""
for edge in compiled.get("edges", []):
if edge.get("kind") != "references":
continue
src_path = block_instance_path + tuple(edge["source"]["path"])
tgt_path = block_instance_path + tuple(edge["target"]["path"])
src_key = ".".join(src_path)
tgt_key = ".".join(tgt_path)
src = path_registry.get(src_key)
tgt = path_registry.get(tgt_key)
if src is None:
raise ValueError(f"references source '{src_key}' not found in registry")
if tgt is None:
raise ValueError(f"references citation '{tgt_key}' not found in registry")
if not isinstance(tgt, ElementInstance) or tgt.kind != "citation":
raise ValueError(f"references target '{tgt_key}' is not a citation ElementInstance")
ref_id = derive_declaration_id(
root_type,
"references",
*[str(x) for x in src_path],
*[str(x) for x in tgt_path],
)
out.append(
ReferenceBinding(
stable_id=ref_id,
source=src,
citation=tgt,
)
)
def _instantiate_connections(
compiled: dict[str, Any],
root: PartInstance,
path_registry: dict[str, ElementInstance | ValueSlot],
root_type: type,
) -> list[ConnectionBinding]:
"""Resolve compiled connection edges into ConnectionBindings."""
connections: list[ConnectionBinding] = []
for edge in compiled.get("edges", []):
if edge["kind"] != "connect":
continue
src_path = root.instance_path + tuple(edge["source"]["path"])
tgt_path = root.instance_path + tuple(edge["target"]["path"])
src_key = ".".join(src_path)
tgt_key = ".".join(tgt_path)
src = path_registry.get(src_key)
tgt = path_registry.get(tgt_key)
if not isinstance(src, PortInstance):
raise ValueError(f"Connection source '{src_key}' is not a PortInstance")
if not isinstance(tgt, PortInstance):
raise ValueError(f"Connection target '{tgt_key}' is not a PortInstance")
conn_id = derive_declaration_id(root_type, "connect", *edge["source"]["path"], *edge["target"]["path"])
connections.append(
ConnectionBinding(
stable_id=conn_id,
source=src,
target=tgt,
carrying=edge.get("carrying"),
)
)
return connections
def _instantiate_allocations(
compiled: dict[str, Any],
root: PartInstance,
path_registry: dict[str, ElementInstance | ValueSlot],
root_type: type,
) -> list[AllocationBinding]:
"""Resolve compiled allocation edges into AllocationBindings."""
allocations: list[AllocationBinding] = []
for edge in compiled.get("edges", []):
if edge["kind"] != "allocate":
continue
req_path = root.instance_path + tuple(edge["source"]["path"])
tgt_path = root.instance_path + tuple(edge["target"]["path"])
req_key = ".".join(req_path)
tgt_key = ".".join(tgt_path)
req = path_registry.get(req_key)
tgt = path_registry.get(tgt_key)
if req is None:
raise ValueError(f"Allocation requirement '{req_key}' not found in registry")
if tgt is None:
raise ValueError(f"Allocation target '{tgt_key}' not found in registry")
if not isinstance(req, ElementInstance):
raise ValueError(f"Allocation requirement '{req_key}' is not an ElementInstance")
if not isinstance(tgt, ElementInstance):
raise ValueError(f"Allocation target '{tgt_key}' is not an ElementInstance")
input_bindings: dict[str, ValueSlot] = {}
raw_inputs = edge.get("_allocate_inputs")
if raw_inputs:
for iname, spec in raw_inputs.items():
rel = tuple(spec["path"])
slot_key = ".".join((root.path_string, *rel))
slot = path_registry.get(slot_key)
if not isinstance(slot, ValueSlot):
raise ValueError(f"allocate inputs[{iname!r}] path {slot_key!r} is not a ValueSlot in registry")
input_bindings[str(iname)] = slot
alloc_id = derive_declaration_id(root_type, "allocate", *edge["source"]["path"], *edge["target"]["path"])
allocations.append(
AllocationBinding(
stable_id=alloc_id,
requirement=req,
target=tgt,
input_bindings=input_bindings,
)
)
return allocations
def _instantiate_all_references(
root: PartInstance,
path_registry: dict[str, ElementInstance | ValueSlot],
root_type: type,
) -> list[ReferenceBinding]:
"""Resolve ``references`` edges from every part type in the instance tree (Phase 8)."""
out: list[ReferenceBinding] = []
stack: list[PartInstance] = [root]
while stack:
part = stack.pop()
compiled = part.definition_type.compile()
for edge in compiled.get("edges", []):
if edge["kind"] != "references":
continue
src_path = part.instance_path + tuple(edge["source"]["path"])
tgt_path = part.instance_path + tuple(edge["target"]["path"])
src_key = ".".join(src_path)
tgt_key = ".".join(tgt_path)
src = path_registry.get(src_key)
tgt = path_registry.get(tgt_key)
if src is None:
raise ValueError(f"references source '{src_key}' not found in registry")
if tgt is None:
raise ValueError(f"references citation '{tgt_key}' not found in registry")
if not isinstance(tgt, ElementInstance) or tgt.kind != "citation":
raise ValueError(f"references target '{tgt_key}' is not a citation ElementInstance")
ref_id = derive_declaration_id(
root_type,
"references",
*[str(x) for x in src_path],
*[str(x) for x in tgt_path],
)
out.append(
ReferenceBinding(
stable_id=ref_id,
source=src,
citation=tgt,
)
)
stack.extend(part.children)
return out
def _register(
instance: ElementInstance,
path_registry: dict[str, ElementInstance | ValueSlot],
id_registry: dict[str, ElementInstance | ValueSlot],
) -> None:
path_registry[instance.path_string] = instance
id_registry[instance.stable_id] = instance
def _register_slot(
slot: ValueSlot,
path_registry: dict[str, ElementInstance | ValueSlot],
id_registry: dict[str, ElementInstance | ValueSlot],
) -> None:
path_registry[slot.path_string] = slot
id_registry[slot.stable_id] = slot
def _normalize_evaluate_inputs(model: ConfiguredModel, inputs: dict[Any, Any]) -> dict[str, Any]:
"""Map ``ValueSlot`` / slot-id ``str`` keys to ``stable_id`` strings for ``Evaluator``."""
out: dict[str, Any] = {}
for key, value in inputs.items():
if isinstance(key, ValueSlot):
reg = model.id_registry.get(key.stable_id)
if reg is not key:
raise ValueError(
f"ValueSlot {key.path_string!r} is not registered on this ConfiguredModel "
"(foreign slot or stale handle).",
)
out[key.stable_id] = value
elif isinstance(key, str):
reg = model.id_registry.get(key)
if reg is None:
raise KeyError(f"Unknown stable_id {key!r} for this ConfiguredModel")
if not isinstance(reg, ValueSlot):
raise ValueError(
f"String key {key!r} refers to {type(reg).__name__}, not a ValueSlot; "
"use ValueSlot handles or the stable_id of a parameter/attribute slot.",
)
out[key] = value
else:
raise TypeError(
f"Input keys must be ValueSlot or str (slot stable_id), got {type(key).__name__}",
)
return out