Files
SystemSimulationApp/app/simulation/ir/compiler.py
T
2026-09-02 19:17:55 +08:00

2247 lines
88 KiB
Python

"""Compile :class:`GenericFluidSystem` into callback-free System IR v2.
The current component implementations are Python reference kernels. This
compiler therefore records their complete, stable numeric layout and execution
plans, but deliberately marks every component kernel ``reference_only`` until
the C-02 pure numeric kernel contract exists. Object references and callbacks
are used only while inspecting the already-constructed system; none are stored
in the returned program.
"""
from __future__ import annotations
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import fields, is_dataclass, replace
from math import isfinite
from typing import TYPE_CHECKING, Any
from app.simulation.core.base import Component, DynamicComponent
from app.simulation.solvers.causal_ir import compile_causal_numeric_ir
from app.simulation.solvers.mechanical import MechanicalConstraintGroup
from .schema import (
CURRENT_SYSTEM_IR_VERSION,
SYSTEM_NUMERIC_IR_COMPILER_ID,
SYSTEM_NUMERIC_IR_COMPILER_VERSION,
IRAlgebraicUnknown,
IRBlockKind,
IRBufferKind,
IRBufferSpec,
IRCSRMatrix,
IRCSRPattern,
IRCacheKind,
IRCapabilityIssue,
IRCapabilityLevel,
IRCapabilityReport,
IRCausalEffortStageRef,
IRCausalPlan,
IRCheckFiniteOperation,
IRComponentCapability,
IRComponentInstance,
IRConnectionRule,
IRConnectionSpec,
IRConnectionVariable,
IRConvergenceSpec,
IRCopyOperation,
IRDType,
IRDiagnosticSeverity,
IREffortBroadcastOperation,
IREntryPoint,
IREntryPointKind,
IREquationOwner,
IREquationRelation,
IREventDirection,
IREventSpec,
IRExecutionBlock,
IRFailurePolicy,
IRFillOperation,
IRFiniteDifferenceColumn,
IRFlowAssignmentOperation,
IRJacobianPlan,
IRKernelAvailability,
IRKernelCallOperation,
IRKernelPhase,
IRKernelPhaseSpec,
IRKernelSpec,
IRLinearCombinationOperation,
IRMediumSpec,
IRModeSpec,
IRModeValueSpec,
IROutputSpec,
IRPortKind,
IRPortNominalRole,
IRPortSpec,
IRPortVariable,
IRPositiveFlowDirection,
IRPressureFlowBlock,
IRPressureFlowEquation,
IRPressureFlowPlan,
IRPressureFlowScope,
IRPressureFlowScopeKind,
IRSlotRef,
IRStage,
IRStageKind,
IRStateMapKind,
IRStateMapOperation,
IRStateReducer,
IRStepKind,
IRStepRef,
IRStreamPlan,
IRStreamSCC,
IRThermofluidPlan,
IRTransactionPlan,
IRValueSpec,
IRVariableRole,
SystemIR,
operation_read_slots,
operation_write_slots,
)
if TYPE_CHECKING:
from app.simulation.systems.generic import GenericFluidSystem
_FLOAT_BUFFERS = frozenset(
kind
for kind in IRBufferKind
if kind not in {IRBufferKind.MODE, IRBufferKind.WORK_INT}
)
def _unique_slots(slots: Iterable[IRSlotRef]) -> tuple[IRSlotRef, ...]:
return tuple(dict.fromkeys(slots))
class _Slots:
"""Deterministic buffer allocator plus value dictionary builder."""
def __init__(self) -> None:
self.initial: dict[IRBufferKind, list[float | int]] = {
kind: [] for kind in IRBufferKind
}
self.values: list[IRValueSpec] = []
def add(
self,
buffer: IRBufferKind,
value_id: str,
*,
initial: float | int = 0.0,
semantic: str,
role: str,
quantity: str = "dimensionless",
unit: str = "",
scale: float = 1.0,
lower_bound: float | None = None,
upper_bound: float | None = None,
owner_component_index: int | None = None,
) -> IRSlotRef:
values = self.initial[buffer]
slot = IRSlotRef(buffer, len(values))
if buffer in _FLOAT_BUFFERS:
value = float(initial)
if not isfinite(value):
raise ValueError(f"Initial IR value {value_id!r} must be finite.")
values.append(value)
else:
values.append(int(initial))
numeric_scale = float(scale)
if not isfinite(numeric_scale) or numeric_scale <= 0.0:
numeric_scale = 1.0
self.values.append(
IRValueSpec(
value_id=value_id,
slot=slot,
semantic=semantic,
role=role,
quantity=quantity or "dimensionless",
unit=unit or "",
scale=numeric_scale,
lower_bound=lower_bound,
upper_bound=upper_bound,
owner_component_index=owner_component_index,
)
)
return slot
def buffers(self) -> tuple[IRBufferSpec, ...]:
result: list[IRBufferSpec] = []
for kind in IRBufferKind:
values = self.initial[kind]
if kind in _FLOAT_BUFFERS:
result.append(
IRBufferSpec(
kind=kind,
dtype=IRDType.FLOAT64,
size=len(values),
initial_float_values=tuple(float(item) for item in values),
)
)
else:
result.append(
IRBufferSpec(
kind=kind,
dtype=IRDType.INT32,
size=len(values),
initial_int_values=tuple(int(item) for item in values),
)
)
return tuple(result)
def _csr_matrix_from_rows(
row_count: int,
column_count: int,
rows: Sequence[Mapping[int, float]],
) -> IRCSRMatrix:
if len(rows) != row_count:
raise ValueError("CSR row data does not match the declared row count.")
row_pointers = [0]
column_indices: list[int] = []
values: list[float] = []
for row in rows:
for column, value in sorted(row.items()):
if not 0 <= int(column) < column_count:
raise ValueError("CSR column is outside the declared matrix shape.")
numeric = float(value)
if numeric == 0.0:
continue
column_indices.append(int(column))
values.append(numeric)
row_pointers.append(len(column_indices))
return IRCSRMatrix(
IRCSRPattern(
row_count=row_count,
column_count=column_count,
row_pointers=tuple(row_pointers),
column_indices=tuple(column_indices),
),
tuple(values),
)
def _csr_pattern(matrix: object) -> IRCSRPattern:
csr = matrix.tocsr().astype(bool)
csr.sort_indices()
return IRCSRPattern(
row_count=int(csr.shape[0]),
column_count=int(csr.shape[1]),
row_pointers=tuple(int(item) for item in csr.indptr),
column_indices=tuple(int(item) for item in csr.indices),
)
def _state_names(component: DynamicComponent) -> tuple[str, ...]:
size = int(component.state_size)
model_type = str(component.model_type)
if model_type == "amesim_mecmas21" and size == 2:
return ("v", "x")
if hasattr(component, "medium") and size >= 2 and size % 2 == 0:
if size == 2:
return ("m", "U")
names: list[str] = []
for partition in range(size // 2):
names.extend((f"m_{partition + 1}", f"U_{partition + 1}"))
return tuple(names)
return tuple(f"state_{index}" for index in range(size))
def _state_metadata(name: str) -> tuple[str, str]:
if name == "v":
return "velocity", "m/s"
if name == "x":
return "length", "m"
if name == "m" or name.startswith("m_"):
return "mass", "kg"
if name == "U" or name.startswith("U_"):
return "internal_energy", "J"
return "dimensionless", ""
def _mechanical_group_key(
group: MechanicalConstraintGroup,
) -> tuple[str, ...]:
"""Return the topology-stable identity used only during compilation."""
return tuple(sorted(component.name for component in group.components))
def _component_phases(component: Component) -> tuple[IRKernelPhase, ...]:
phases = [IRKernelPhase.PRIMAL]
if component.pressure_flow_equation_residuals():
phases.append(IRKernelPhase.RESIDUAL)
if isinstance(component, DynamicComponent):
phases.extend((IRKernelPhase.PROPERTY, IRKernelPhase.DERIVATIVE))
if getattr(component, "signal_event_times", None) is not None:
phases.extend((IRKernelPhase.EVENT, IRKernelPhase.RESET))
if component.RESULT_VARIABLES or any(
variable.result_visible
for port in component.active_port_definitions
for variable in port.variables
):
phases.append(IRKernelPhase.OUTPUT)
return tuple(dict.fromkeys(phases))
def _numeric_constants(value: object, prefix: str = "") -> tuple[tuple[str, float], ...]:
"""Flatten stable finite numeric dataclass constants without object identity."""
result: list[tuple[str, float]] = []
if is_dataclass(value) and not isinstance(value, type):
for item in fields(value):
current = getattr(value, item.name)
name = f"{prefix}.{item.name}" if prefix else item.name
if isinstance(current, bool):
continue
if isinstance(current, (int, float)) and isfinite(float(current)):
result.append((name, float(current)))
elif is_dataclass(current) and not isinstance(current, type):
result.extend(_numeric_constants(current, name))
fluid = getattr(value, "fluid", None)
if fluid is not None and fluid is not value and is_dataclass(fluid):
result.extend(_numeric_constants(fluid, "fluid"))
return tuple(dict.fromkeys(result))
def _medium_identity(medium: object) -> tuple[str, str, str]:
medium_type = type(medium)
substance = str(getattr(medium_type, "SUBSTANCE_ID", medium_type.__name__))
method = str(getattr(medium_type, "PROPERTY_METHOD_ID", "reference"))
implementation = f"{medium_type.__module__}.{medium_type.__qualname__}"
return f"{substance}:{method}", implementation, str(
getattr(medium_type, "IMPLEMENTATION_VERSION", "python-reference-1")
)
def _stage(
stage_id: str,
kind: IRStageKind,
operations: Sequence[Any],
) -> IRStage:
operation_tuple = tuple(operations)
return IRStage(
stage_id=stage_id,
kind=kind,
operations=operation_tuple,
declared_read_slots=_unique_slots(
slot
for operation in operation_tuple
for slot in operation_read_slots(operation)
),
declared_write_slots=_unique_slots(
slot
for operation in operation_tuple
for slot in operation_write_slots(operation)
),
)
def _step(index: int, kind: IRStepKind = IRStepKind.STAGE) -> IRStepRef:
return IRStepRef(kind, int(index))
def compile_system_ir(
system: "GenericFluidSystem",
*,
model_version: str = "unversioned",
) -> SystemIR:
"""Compile an already-constructed generic system into validated IR v2."""
# Local import keeps the public IR package independent of GenericFluidSystem
# construction and prevents a systems.generic -> ir -> systems.generic cycle.
from app.simulation.systems.generic import GenericFluidSystem
if not isinstance(system, GenericFluidSystem):
raise TypeError("compile_system_ir() requires a GenericFluidSystem instance.")
network = system.network
components = tuple(network.components.values())
component_index = {component.name: index for index, component in enumerate(components)}
connections = tuple(network.connections)
connection_index = {connection.id: index for index, connection in enumerate(connections)}
slots = _Slots()
capability_issues: list[IRCapabilityIssue] = []
time_slot = slots.add(
IRBufferKind.TIME,
"time",
semantic="independent_variable",
role="input",
quantity="time",
unit="s",
)
# The reducer's initialization is the authoritative state order and also
# performs the same supported initial consistency projection as simulate().
initial_state = tuple(float(item) for item in system.initial_state_vector())
state_input_slots: list[IRSlotRef] = []
derivative_output_slots: list[IRSlotRef] = []
for index, value in enumerate(initial_state):
state_input_slots.append(
slots.add(
IRBufferKind.STATE_INPUT,
f"solver.state.{index}",
initial=value,
semantic="solver_state",
role="state",
)
)
derivative_output_slots.append(
slots.add(
IRBufferKind.DERIVATIVE_OUTPUT,
f"solver.derivative.{index}",
semantic="solver_derivative",
role="derivative",
)
)
dynamic_components = tuple(system.dynamic_components)
local_state_by_component: dict[str, tuple[IRSlotRef, ...]] = {}
local_derivative_by_component: dict[str, tuple[IRSlotRef, ...]] = {}
local_state_row_by_component: dict[str, tuple[int, ...]] = {}
local_derivative_column_by_component: dict[str, tuple[int, ...]] = {}
local_state_slots: list[IRSlotRef] = []
local_derivative_slots: list[IRSlotRef] = []
for component in dynamic_components:
state_values = tuple(float(item) for item in component.get_state_vector())
names = _state_names(component)
component_states: list[IRSlotRef] = []
component_derivatives: list[IRSlotRef] = []
state_rows: list[int] = []
derivative_columns: list[int] = []
for local_index, (name, value) in enumerate(zip(names, state_values)):
quantity, unit = _state_metadata(name)
state_rows.append(len(local_state_slots))
derivative_columns.append(len(local_derivative_slots))
state_slot = slots.add(
IRBufferKind.LOCAL_STATE,
f"{component.name}.state.{name}",
initial=value,
semantic="component_state",
role="state",
quantity=quantity,
unit=unit,
scale=max(abs(value), 1.0),
owner_component_index=component_index[component.name],
)
derivative_slot = slots.add(
IRBufferKind.LOCAL_DERIVATIVE,
f"{component.name}.derivative.{name}",
semantic="component_derivative",
role="derivative",
quantity=quantity,
unit=unit,
owner_component_index=component_index[component.name],
)
component_states.append(state_slot)
component_derivatives.append(derivative_slot)
local_state_slots.append(state_slot)
local_derivative_slots.append(derivative_slot)
local_state_by_component[component.name] = tuple(component_states)
local_derivative_by_component[component.name] = tuple(component_derivatives)
local_state_row_by_component[component.name] = tuple(state_rows)
local_derivative_column_by_component[component.name] = tuple(derivative_columns)
# Build exact scatter/gather matrices from the real reduced state entries.
scatter_rows: list[dict[int, float]] = [
{} for _ in range(len(local_state_slots))
]
gather_rows: list[dict[int, float]] = [
{} for _ in range(len(initial_state))
]
solver_cursor = 0
group_solver_offset: dict[tuple[str, ...], int] = {}
for entry in system.mechanical_state_reducer.state_entries:
if isinstance(entry, MechanicalConstraintGroup):
entry_size = 2
group_solver_offset[_mechanical_group_key(entry)] = solver_cursor
representative = entry.representative
for member in entry.components:
for local_row, solver_column in zip(
local_state_row_by_component[member.name],
range(solver_cursor, solver_cursor + entry_size),
):
scatter_rows[local_row][solver_column] = 1.0
for output_row, local_column in zip(
range(solver_cursor, solver_cursor + entry_size),
local_derivative_column_by_component[representative.name],
):
gather_rows[output_row][local_column] = 1.0
else:
entry_size = int(entry.state_size)
for local_row, solver_column in zip(
local_state_row_by_component[entry.name],
range(solver_cursor, solver_cursor + entry_size),
):
scatter_rows[local_row][solver_column] = 1.0
for output_row, local_column in zip(
range(solver_cursor, solver_cursor + entry_size),
local_derivative_column_by_component[entry.name],
):
gather_rows[output_row][local_column] = 1.0
solver_cursor += entry_size
if solver_cursor != len(initial_state):
raise RuntimeError("Reduced state entries do not cover the solver vector.")
# Ideal C-C storage coupling keeps public state coordinates but projects
# derivatives by physical volume. Fold that exact projection into gather.
for group in system.pneumatic_storage_reducer.groups:
offsets = [
int(system.pneumatic_storage_reducer._global_offset(partition))
for partition in group.partitions
]
volumes = [float(partition.volume) for partition in group.partitions]
total_volume = sum(volumes)
# ``GenericFluidSystem.apply_state_vector`` first projects every
# ideally connected storage partition onto one common mass/energy
# density, then scatters that projected vector into components. The
# IR matrix must include the same projection (not merely the matching
# derivative projection below), otherwise arbitrary solver trial
# vectors would reach different component states in Python and IR.
for partition, volume in zip(group.partitions, volumes):
target_rows = local_state_row_by_component[partition.component.name]
fraction = volume / total_volume
for field_offset in (0, 1):
target_row = target_rows[partition.state_offset + field_offset]
scatter_rows[target_row] = {
source_offset + field_offset: fraction
for source_offset in offsets
}
for offset, volume in zip(offsets, volumes):
fraction = volume / total_volume
for field_offset in (0, 1):
combined: dict[int, float] = {}
for source_offset in offsets:
for column, weight in gather_rows[source_offset + field_offset].items():
combined[column] = combined.get(column, 0.0) + fraction * weight
gather_rows[offset + field_offset] = combined
state_scatter = _csr_matrix_from_rows(
len(local_state_slots), len(initial_state), scatter_rows
)
derivative_gather = _csr_matrix_from_rows(
len(initial_state), len(local_derivative_slots), gather_rows
)
absolute_tolerances = tuple(
float(item)
for item in system.mechanical_state_reducer.absolute_tolerances(
1.0e-8,
mechanical=1.0e-12,
mode="legacy",
)
)
state_reducer = IRStateReducer(
solver_state_count=len(initial_state),
local_state_slots=tuple(local_state_slots),
raw_derivative_slots=tuple(local_derivative_slots),
state_scatter=state_scatter,
derivative_gather=derivative_gather,
initial_state=initial_state,
absolute_tolerances=absolute_tolerances,
)
# Normalized component parameter slots.
parameter_slots_by_component: dict[str, tuple[IRSlotRef, ...]] = {}
for component in components:
values = component.parameter_values
component_slots: list[IRSlotRef] = []
for definition in component.PARAMETERS:
value = float(values[definition.name])
component_slots.append(
slots.add(
IRBufferKind.PARAMETER,
f"{component.name}.parameter.{definition.name}",
initial=value,
semantic="component_parameter",
role="parameter",
quantity=definition.quantity,
unit=definition.unit,
scale=max(abs(value), abs(float(definition.default)), 1.0),
lower_bound=definition.minimum,
upper_bound=definition.maximum,
owner_component_index=component_index[component.name],
)
)
parameter_slots_by_component[component.name] = tuple(component_slots)
# Port and connector tables use actual active-port order from compilation.
port_specs: list[IRPortSpec] = []
port_index_by_id: dict[str, int] = {}
port_slot_by_variable_id: dict[str, IRSlotRef] = {}
port_slots_by_component: dict[str, list[IRSlotRef]] = {
component.name: [] for component in components
}
port_indices_by_component: dict[str, list[int]] = {
component.name: [] for component in components
}
for component in components:
owner = component_index[component.name]
for definition in component.active_port_definitions:
port = component.get_port(definition.name)
variables: list[IRPortVariable] = []
port_id = f"{component.name}.{definition.name}"
for variable in definition.variables:
variable_id = f"{port_id}.{variable.name}"
buffer = (
IRBufferKind.SIGNAL
if definition.kind == "signal"
else IRBufferKind.ALGEBRAIC
)
value = float(getattr(port, variable.name))
lower_bound = 0.0 if variable.name == "p" else None
slot = slots.add(
buffer,
variable_id,
initial=value,
semantic="port_variable",
role=variable.role,
quantity=variable.quantity,
unit=variable.unit,
scale=max(abs(value), 1.0),
lower_bound=lower_bound,
owner_component_index=owner,
)
port_slot_by_variable_id[variable_id] = slot
port_slots_by_component[component.name].append(slot)
variables.append(
IRPortVariable(
variable_id=variable_id,
name=variable.name,
role=IRVariableRole(variable.role),
connection_rule=IRConnectionRule(variable.connection_rule),
quantity=variable.quantity or "dimensionless",
unit=variable.unit or "",
result_visible=bool(variable.result_visible),
slot=slot,
)
)
port_index = len(port_specs)
port_index_by_id[port_id] = port_index
port_indices_by_component[component.name].append(port_index)
port_specs.append(
IRPortSpec(
port_id=port_id,
component_index=owner,
name=definition.name,
kind=IRPortKind(definition.kind),
domain=definition.domain,
nominal_role=IRPortNominalRole(definition.nominal_role),
positive_flow_direction=(
IRPositiveFlowDirection(definition.positive_flow_direction)
if definition.positive_flow_direction is not None
else None
),
variables=tuple(variables),
)
)
connection_specs: list[IRConnectionSpec] = []
for connection in connections:
endpoint_a_id = str(connection.endpoint_a)
endpoint_b_id = str(connection.endpoint_b)
endpoint_a_port = port_specs[port_index_by_id[endpoint_a_id]]
endpoint_b_port = port_specs[port_index_by_id[endpoint_b_id]]
variables = tuple(
IRConnectionVariable(
name=first.name,
rule=first.connection_rule,
endpoint_a_slot=first.slot,
endpoint_b_slot=second.slot,
)
for first, second in zip(
endpoint_a_port.variables,
endpoint_b_port.variables,
strict=True,
)
)
connection_specs.append(
IRConnectionSpec(
connection_id=connection.id,
kind=IRPortKind(connection.kind),
domain=connection.domain,
endpoint_a_port_index=port_index_by_id[endpoint_a_id],
endpoint_b_port_index=port_index_by_id[endpoint_b_id],
variables=variables,
)
)
# Project-scoped media are grouped by semantic implementation and numeric
# constants, never by Python object address.
medium_groups: dict[
tuple[str, str, str, tuple[tuple[str, float], ...]], list[int]
] = {}
medium_objects: dict[
tuple[str, str, str, tuple[tuple[str, float], ...]], object
] = {}
for index, component in enumerate(components):
medium = getattr(component, "medium", None)
if medium is None:
continue
medium_id, implementation_id, implementation_version = _medium_identity(medium)
constants = _numeric_constants(medium)
key = (medium_id, implementation_id, implementation_version, constants)
medium_groups.setdefault(key, []).append(index)
medium_objects.setdefault(key, medium)
medium_specs: list[IRMediumSpec] = []
medium_variant_counts: dict[str, int] = {}
for medium_id, _implementation_id, _implementation_version, _constants in medium_groups:
medium_variant_counts[medium_id] = medium_variant_counts.get(medium_id, 0) + 1
medium_variant_offsets: dict[str, int] = {}
for key, member_indices in medium_groups.items():
medium_id, implementation_id, implementation_version, constants = key
variant_offset = medium_variant_offsets.get(medium_id, 0)
medium_variant_offsets[medium_id] = variant_offset + 1
compiled_medium_id = (
medium_id
if medium_variant_counts[medium_id] == 1
else f"{medium_id}:variant-{variant_offset + 1}"
)
parameter_slots = tuple(
slots.add(
IRBufferKind.CONSTANT,
f"medium.{compiled_medium_id}.{name}",
initial=value,
semantic="medium_constant",
role="constant",
scale=max(abs(value), 1.0),
)
for name, value in constants
)
medium = medium_objects[key]
medium_specs.append(
IRMediumSpec(
medium_id=compiled_medium_id,
name=str(getattr(medium, "name", medium_id)),
implementation_id=implementation_id,
implementation_version=implementation_version,
parameter_slots=parameter_slots,
component_indices=tuple(member_indices),
)
)
# Discrete mechanical modes are shared by every inertia in one rigid group.
modes: list[IRModeSpec] = []
mode_slots_by_component: dict[str, list[IRSlotRef]] = {
component.name: [] for component in components
}
mode_slot_by_group: dict[tuple[str, ...], IRSlotRef] = {}
event_groups: list[MechanicalConstraintGroup] = []
for group in system.mechanical_state_reducer.groups:
if not group.discrete_endstop_components:
continue
member_names = tuple(sorted(component.name for component in group.components))
mode_id = "mechanical_group:" + ",".join(member_names) + ".mode"
mode_slot = slots.add(
IRBufferKind.MODE,
mode_id,
initial=-1,
semantic="mechanical_constraint_mode",
role="mode",
)
mode_slot_by_group[_mechanical_group_key(group)] = mode_slot
event_groups.append(group)
owners = tuple(component_index[name] for name in member_names)
for name in member_names:
mode_slots_by_component[name].append(mode_slot)
modes.append(
IRModeSpec(
mode_id=mode_id,
slot=mode_slot,
owner_component_indices=owners,
values=(
IRModeValueSpec(-1, "uninitialized"),
IRModeValueSpec(0, "free"),
IRModeValueSpec(1, "lower"),
IRModeValueSpec(2, "upper"),
),
initial_value=-1,
)
)
# Allocate result projection sources before component records so each
# instance can point at its immutable output indices.
result_metadata = network.result_variable_metadata()
output_indices_by_component: dict[str, list[int]] = {
component.name: [] for component in components
}
output_source_slots: list[IRSlotRef] = []
output_slots: list[IRSlotRef] = []
for output_index, metadata in enumerate(result_metadata):
output_indices_by_component[metadata.component_id].append(output_index)
if metadata.scope == "port":
source_id = f"{metadata.component_id}.{metadata.port_name}.{metadata.name}"
source_slot = port_slot_by_variable_id[source_id]
else:
source_slot = slots.add(
IRBufferKind.ALGEBRAIC,
f"{metadata.key}.output_source",
semantic="component_result_source",
role="output_source",
quantity=metadata.quantity,
unit=metadata.unit,
owner_component_index=component_index[metadata.component_id],
)
output_source_slots.append(source_slot)
output_slots.append(
slots.add(
IRBufferKind.RESULT_OUTPUT,
f"result.{metadata.key}",
semantic="result_output",
role="output",
quantity=metadata.quantity,
unit=metadata.unit,
owner_component_index=component_index[metadata.component_id],
)
)
# One reference kernel per component implementation plus an explicit
# reference-only system executor for topology-wide operations.
kernels: list[IRKernelSpec] = []
component_kernel_index: dict[str, int] = {}
components_by_kernel: dict[
tuple[str, str, str, int, int, int], list[Component]
] = {}
for component in components:
component_type = type(component)
model_type = str(component.model_type)
model_model_version = str(component_type.MODEL_VERSION or "unversioned")
implementation = f"{component_type.__module__}.{component_type.__qualname__}"
key = (
model_type,
model_model_version,
implementation,
len(component.PARAMETERS),
int(component.state_size) if isinstance(component, DynamicComponent) else 0,
len(mode_slots_by_component[component.name]),
)
components_by_kernel.setdefault(key, []).append(component)
for key, instances in components_by_kernel.items():
(
model_type,
model_model_version,
implementation,
parameter_count,
state_count,
mode_count,
) = key
phases = tuple(
dict.fromkeys(
phase for component in instances for phase in _component_phases(component)
)
)
kernel_index = len(kernels)
kernels.append(
IRKernelSpec(
kernel_id=(
f"reference:{model_type}@{model_model_version}"
f":{implementation}:p{parameter_count}:s{state_count}:m{mode_count}"
),
model_type=model_type,
model_version=model_model_version,
implementation_version=implementation,
availability=IRKernelAvailability.REFERENCE_ONLY,
unavailable_reason="C-02 pure numeric kernel contract is not declared.",
phases=tuple(IRKernelPhaseSpec(phase) for phase in phases),
parameter_count=parameter_count,
state_count=state_count,
mode_count=mode_count,
workspace_float_count=0,
workspace_int_count=0,
)
)
for component in instances:
component_kernel_index[component.name] = kernel_index
system_kernel_index = len(kernels)
kernels.append(
IRKernelSpec(
kernel_id="reference:generic-fluid-system@2",
model_type="generic_fluid_system",
model_version="2.0.0",
implementation_version=SYSTEM_NUMERIC_IR_COMPILER_VERSION,
availability=IRKernelAvailability.REFERENCE_ONLY,
unavailable_reason="Whole-system native execution is not implemented.",
phases=tuple(
IRKernelPhaseSpec(phase)
for phase in (
IRKernelPhase.PRIMAL,
IRKernelPhase.RESIDUAL,
IRKernelPhase.DERIVATIVE,
IRKernelPhase.EVENT,
IRKernelPhase.RESET,
IRKernelPhase.JACOBIAN,
IRKernelPhase.OUTPUT,
)
),
parameter_count=0,
state_count=len(initial_state),
mode_count=len(modes),
workspace_float_count=0,
workspace_int_count=0,
)
)
component_instances = tuple(
IRComponentInstance(
instance_id=component.name,
kernel_index=component_kernel_index[component.name],
parameter_slots=parameter_slots_by_component[component.name],
state_slots=local_state_by_component.get(component.name, ()),
derivative_slots=local_derivative_by_component.get(component.name, ()),
mode_slots=tuple(mode_slots_by_component[component.name]),
port_indices=tuple(port_indices_by_component[component.name]),
port_slots=tuple(port_slots_by_component[component.name]),
output_indices=tuple(output_indices_by_component[component.name]),
workspace_float_slots=(),
workspace_int_slots=(),
)
for component in components
)
# Pressure-flow structure and work slots.
pressure_solver = system.pressure_flow_solver
algebraic_scales = pressure_solver._scales()
unknown_index_by_id = {
unknown.id: index for index, unknown in enumerate(pressure_solver.unknowns)
}
algebraic_unknowns: list[IRAlgebraicUnknown] = []
for unknown in pressure_solver.unknowns:
scale = float(algebraic_scales.get(unknown.variable, 1.0))
algebraic_unknowns.append(
IRAlgebraicUnknown(
unknown_id=unknown.id,
component_index=component_index[unknown.component],
port_index=port_index_by_id[f"{unknown.component}.{unknown.port}"],
variable=unknown.variable,
role=IRVariableRole(unknown.role),
slot=port_slot_by_variable_id[unknown.id],
scale=max(scale, 1.0e-300),
lower_bound=0.0 if unknown.variable == "p" else None,
upper_bound=None,
)
)
equation_index_by_id = {
equation.id: index
for index, equation in enumerate(pressure_solver.equation_templates)
}
residual_slots: list[IRSlotRef] = []
pressure_equations: list[IRPressureFlowEquation] = []
for equation_index_value, equation in enumerate(pressure_solver.equation_templates):
residual_slot = slots.add(
IRBufferKind.WORK_FLOAT,
f"pressure_flow.residual.{equation.id}",
semantic="pressure_flow_residual",
role="residual",
)
residual_slots.append(residual_slot)
variable_slots: list[IRSlotRef] = []
for variable_id in equation.variables:
slot = port_slot_by_variable_id.get(variable_id)
if slot is not None:
variable_slots.append(slot)
continue
if variable_id.endswith(".state"):
owner_name = variable_id[: -len(".state")]
variable_slots.extend(local_state_by_component.get(owner_name, ()))
continue
capability_issues.append(
IRCapabilityIssue(
code="IR_EQUATION_VARIABLE_UNRESOLVED",
severity=IRDiagnosticSeverity.ERROR,
scope_id=equation.id,
message=f"Equation variable {variable_id!r} has no numeric slot.",
)
)
if equation.owner == "component":
owner = component_index[equation.owner_id]
else:
owner = connection_index[equation.owner_id]
if equation.role == "flow":
scale = algebraic_scales.get("m_flow", 1.0)
if any(variable.endswith(".f") for variable in equation.variables):
scale = algebraic_scales.get("f", 1.0)
elif equation.role == "effort":
suffixes = {item.rsplit(".", 1)[-1] for item in equation.variables}
scale = (
algebraic_scales.get("x", 1.0)
if "x" in suffixes
else algebraic_scales.get("v", 1.0)
if "v" in suffixes
else algebraic_scales.get("p", 1.0)
)
else:
scale = 1.0
pressure_equations.append(
IRPressureFlowEquation(
equation_id=equation.id,
owner=IREquationOwner(equation.owner),
owner_index=owner,
relation=IREquationRelation(equation.relation),
role=IRVariableRole(equation.role) if equation.role is not None else None,
variable_slots=tuple(variable_slots),
residual_slot=residual_slot,
scale=max(float(scale), 1.0e-300),
)
)
pressure_blocks: list[IRPressureFlowBlock] = []
block_key_to_index: dict[tuple[tuple[int, ...], tuple[int, ...]], int] = {}
def add_pressure_block(
unknown_indices: Sequence[int],
equation_indices: Sequence[int],
pattern: object,
) -> int:
unknown_tuple = tuple(int(item) for item in unknown_indices)
equation_tuple = tuple(int(item) for item in equation_indices)
key = (unknown_tuple, equation_tuple)
existing = block_key_to_index.get(key)
if existing is not None:
return existing
index = len(pressure_blocks)
block_key_to_index[key] = index
pressure_blocks.append(
IRPressureFlowBlock(
block_id=f"pressure_flow.block.{index}",
unknown_indices=unknown_tuple,
equation_indices=equation_tuple,
jacobian_pattern=_csr_pattern(pattern),
)
)
return index
for block in pressure_solver.equation_blocks:
pattern = pressure_solver.jacobian_sparsity[
list(block.equation_indices), :
][:, list(block.unknown_indices)]
add_pressure_block(block.unknown_indices, block.equation_indices, pattern)
# Preserve the callback-free structural half of causal IR v1.
causal_compilation = compile_causal_numeric_ir(pressure_solver)
causal_plans: list[IRCausalPlan] = []
causal_program = causal_compilation.ir.program if causal_compilation.supported else None
causal_canonical_slots: list[IRSlotRef] = []
if causal_program is not None:
for canonical in causal_program.canonical_slots:
causal_canonical_slots.append(
slots.add(
IRBufferKind.WORK_FLOAT,
f"causal_v1.canonical.{canonical.id}",
semantic="causal_coordinate",
role=canonical.kind,
)
)
# Main and causal stages are assembled together so stage references in the
# causal plan are ordinary v2 indices.
stages: list[IRStage] = []
def add_stage(stage_id: str, kind: IRStageKind, operations: Sequence[Any]) -> int:
index = len(stages)
stages.append(_stage(stage_id, kind, operations))
return index
state_stage = add_stage(
"rhs.state_scatter",
IRStageKind.STATE_REDUCE,
(
IRStateMapOperation(
IRStateMapKind.SCATTER,
tuple(state_input_slots),
tuple(local_state_slots),
),
),
)
# Signal output functions, followed by directed connection propagation.
signal_operations: list[Any] = []
for binding in system.signal_resolver._output_bindings:
component = binding.component
writes = tuple(
variable.slot
for port_index_value in port_indices_by_component[component.name]
for variable in port_specs[port_index_value].variables
if port_specs[port_index_value].kind is IRPortKind.SIGNAL
and port_specs[port_index_value].nominal_role is IRPortNominalRole.OUTPUT
)
signal_operations.append(
IRKernelCallOperation(
kernel_index=component_kernel_index[component.name],
component_index=component_index[component.name],
phase=IRKernelPhase.PRIMAL,
read_slots=(time_slot, *parameter_slots_by_component[component.name]),
write_slots=writes,
)
)
for connection in connections:
if connection.kind != "signal":
continue
endpoint_a = port_specs[port_index_by_id[str(connection.endpoint_a)]]
endpoint_b = port_specs[port_index_by_id[str(connection.endpoint_b)]]
source, target = (
(endpoint_a, endpoint_b)
if endpoint_a.nominal_role is IRPortNominalRole.OUTPUT
else (endpoint_b, endpoint_a)
)
signal_operations.append(IRCopyOperation(source.variables[0].slot, target.variables[0].slot))
signal_stage = add_stage("rhs.signal", IRStageKind.SIGNAL, signal_operations)
effort_slots = tuple(
unknown.slot
for unknown in algebraic_unknowns
if unknown.variable in {"x", "v"}
)
mechanical_equivalence_stage = add_stage(
"rhs.mechanical_equivalence",
IRStageKind.MECHANICAL_EQUIVALENCE,
(
IRKernelCallOperation(
system_kernel_index,
None,
IRKernelPhase.PRIMAL,
tuple(local_state_slots),
effort_slots,
),
),
)
pneumatic_volume_slots = tuple(
variable.slot
for port in port_specs
if port.kind is IRPortKind.PHYSICAL and port.domain == "pneumatic"
for variable in port.variables
if variable.name in {"volume", "volume_flow"}
)
volume_operations: list[Any] = [IRFillOperation(pneumatic_volume_slots, 0.0)]
volume_output_names: dict[str, tuple[str, ...]] = {}
for component in system.pneumatic_volume_resolver._output_components:
output_names = tuple(component.pneumatic_volume_outputs())
volume_output_names[component.name] = output_names
writes = tuple(
port_slot_by_variable_id[f"{component.name}.{port_name}.{variable}"]
for port_name in output_names
for variable in ("volume", "volume_flow")
)
volume_operations.append(
IRKernelCallOperation(
component_kernel_index[component.name],
component_index[component.name],
IRKernelPhase.PRIMAL,
(
*local_state_by_component.get(component.name, ()),
*parameter_slots_by_component[component.name],
*port_slots_by_component[component.name],
),
writes,
)
)
connected_endpoint: dict[str, str] = {}
for connection in connections:
if connection.kind == "physical" and connection.domain == "pneumatic":
connected_endpoint[str(connection.endpoint_a)] = str(connection.endpoint_b)
connected_endpoint[str(connection.endpoint_b)] = str(connection.endpoint_a)
for component_name, port_names in volume_output_names.items():
for port_name in port_names:
source_endpoint = f"{component_name}.{port_name}"
target_endpoint = connected_endpoint.get(source_endpoint)
if target_endpoint is None:
continue
for variable in ("volume", "volume_flow"):
volume_operations.append(
IRCopyOperation(
port_slot_by_variable_id[f"{source_endpoint}.{variable}"],
port_slot_by_variable_id[f"{target_endpoint}.{variable}"],
)
)
volume_stage = add_stage("rhs.dynamic_volume", IRStageKind.DYNAMIC_VOLUME, volume_operations)
property_operations: list[Any] = []
for component in dynamic_components:
writes = tuple(
port_slot_by_variable_id[f"{component.name}.{definition.name}.{variable.name}"]
for definition in component.active_port_definitions
if definition.kind == "physical" and definition.domain == "pneumatic"
for variable in definition.variables
if variable.name in {"p", "h_outflow"}
)
property_operations.append(
IRKernelCallOperation(
component_kernel_index[component.name],
component_index[component.name],
IRKernelPhase.PROPERTY,
(
*local_state_by_component[component.name],
*parameter_slots_by_component[component.name],
*port_slots_by_component[component.name],
),
writes,
)
)
property_stage = add_stage("rhs.property_bundle", IRStageKind.PROPERTY, property_operations)
all_unknown_slots = tuple(item.slot for item in algebraic_unknowns)
# Reference pressure-flow evaluation still reaches through the object model
# to parameters, modes, medium constants and every physical port value. C-01
# records a conservative dependency superset so cache invalidation remains
# safe until C-02 replaces this system call with pure component signatures.
pressure_flow_model_read_slots = _unique_slots(
IRSlotRef(kind, index)
for kind in (
IRBufferKind.TIME,
IRBufferKind.LOCAL_STATE,
IRBufferKind.ALGEBRAIC,
IRBufferKind.SIGNAL,
IRBufferKind.PARAMETER,
IRBufferKind.CONSTANT,
IRBufferKind.MODE,
IRBufferKind.RUNTIME_INPUT,
)
for index in range(len(slots.initial[kind]))
)
global_pressure_stage = add_stage(
"rhs.pressure_flow.global",
IRStageKind.PRESSURE_FLOW,
(
IRKernelCallOperation(
system_kernel_index,
None,
IRKernelPhase.RESIDUAL,
pressure_flow_model_read_slots,
(*all_unknown_slots, *residual_slots),
tuple(range(len(pressure_equations))),
),
IRCheckFiniteOperation(all_unknown_slots, "PRESSURE_FLOW_NONFINITE"),
),
)
# Causal v1 structural stages are intentionally not Python bindings.
causal_effort_refs: list[IRCausalEffortStageRef] = []
causal_flow_stage_indices: list[int] = []
if causal_program is not None:
for source_stage in causal_program.effort_stages:
operations = tuple(
IREffortBroadcastOperation(
variable=operation.variable,
anchor_slot=port_slot_by_variable_id[
causal_program.compatibility_slots[
operation.anchor_compatibility_slot
].id
],
residual_slot=residual_slots[equation_index_by_id[operation.equation_id]],
result_slot=causal_canonical_slots[operation.result_slot],
scatter_slots=tuple(
port_slot_by_variable_id[
causal_program.compatibility_slots[index].id
]
for index in operation.scatter_compatibility_slots
),
equation_id=operation.equation_id,
lower_bound=0.0 if operation.variable == "p" else None,
)
for operation in source_stage.operations
)
stage_index = add_stage(
f"causal_v1.effort.{source_stage.variable}",
IRStageKind.PRESSURE_FLOW,
operations,
)
causal_effort_refs.append(
IRCausalEffortStageRef(source_stage.variable, stage_index)
)
for flow_index, source_stage in enumerate(causal_program.flow_stages):
operations = tuple(
IRFlowAssignmentOperation(
value_slot=residual_slots[equation_index_by_id[equation_id]],
result_slot=causal_canonical_slots[target],
scatter_slots=(
port_slot_by_variable_id[
causal_program.compatibility_slots[compatibility].id
],
),
equation_id=equation_id,
)
for target, compatibility, equation_id in zip(
source_stage.target_slots,
source_stage.scatter_compatibility_slots,
source_stage.equation_ids,
strict=True,
)
)
causal_flow_stage_indices.append(
add_stage(
f"causal_v1.flow.{flow_index}",
IRStageKind.PRESSURE_FLOW,
operations,
)
)
causal_plans.append(
IRCausalPlan(
plan_id="pressure_flow.global.causal_v1",
scope_component_indices=tuple(range(len(components))),
source_schema_version=int(causal_program.schema_version),
source_structural_signature=causal_program.structural_signature,
fallback_reason=None,
canonical_slots=tuple(causal_canonical_slots),
compatibility_slots=tuple(
port_slot_by_variable_id[item.id]
for item in causal_program.compatibility_slots
),
reset_slots=tuple(
port_slot_by_variable_id[
causal_program.compatibility_slots[index].id
]
for index in causal_program.reset_compatibility_slots
),
external_effort_slots=tuple(
port_slot_by_variable_id[
causal_program.compatibility_slots[index].id
]
for index in causal_program.external_effort_compatibility_slots
),
effort_stages=tuple(causal_effort_refs),
flow_stage_indices=tuple(causal_flow_stage_indices),
)
)
else:
causal_plans.append(
IRCausalPlan(
plan_id="pressure_flow.global.causal_v1",
scope_component_indices=tuple(range(len(components))),
source_schema_version=1,
source_structural_signature=None,
fallback_reason=causal_compilation.fallback_reason,
canonical_slots=(),
compatibility_slots=all_unknown_slots,
reset_slots=(),
external_effort_slots=(),
effort_stages=(),
flow_stage_indices=(),
)
)
capability_issues.append(
IRCapabilityIssue(
code="IR_CAUSAL_PRESSURE_FLOW_FALLBACK",
severity=IRDiagnosticSeverity.WARNING,
scope_id="pressure_flow.global",
message=(
"Causal IR v1 is unavailable; the Python reference path may "
"fall back to finite-difference least_squares: "
f"{causal_compilation.fallback_reason or 'unspecified reason'}."
),
)
)
# Stream operations mirror the current resolver: non-dynamic components
# iterate to an enthalpy fixed point; dynamic stream hooks run afterwards.
connected_h_slots_by_component: dict[str, list[IRSlotRef]] = {
component.name: [] for component in components
}
for connection in connections:
if connection.kind != "physical":
continue
first, second = str(connection.endpoint_a), str(connection.endpoint_b)
first_h = port_slot_by_variable_id.get(f"{first}.h_outflow")
second_h = port_slot_by_variable_id.get(f"{second}.h_outflow")
if first_h is not None and second_h is not None:
connected_h_slots_by_component[connection.endpoint_a.component].append(second_h)
connected_h_slots_by_component[connection.endpoint_b.component].append(first_h)
stream_node_slots = tuple(
variable.slot
for port in port_specs
if port.kind is IRPortKind.PHYSICAL
for variable in port.variables
if variable.name == "h_outflow"
)
def stream_operations(selected: Sequence[Component]) -> list[Any]:
operations: list[Any] = []
for component in selected:
writes = tuple(
variable.slot
for port_index_value in port_indices_by_component[component.name]
for variable in port_specs[port_index_value].variables
if variable.name == "h_outflow"
)
operations.append(
IRKernelCallOperation(
component_kernel_index[component.name],
component_index[component.name],
IRKernelPhase.PRIMAL,
tuple(connected_h_slots_by_component[component.name]),
writes,
)
)
return operations
stream_nondynamic_stage = add_stage(
"rhs.stream.reference_fixed_point",
IRStageKind.STREAM,
stream_operations(system.stream_resolver._non_dynamic_components),
)
stream_dynamic_stage = add_stage(
"rhs.stream.dynamic_outflows",
IRStageKind.STREAM,
stream_operations(dynamic_components),
)
temperature_reference_stage = add_stage(
"rhs.stream.temperature_reference",
IRStageKind.TEMPERATURE_REFERENCE,
tuple(
IRKernelCallOperation(
component_kernel_index[component.name],
component_index[component.name],
IRKernelPhase.PRIMAL,
tuple(connected_h_slots_by_component[component.name]),
tuple(port_slots_by_component[component.name]),
)
for component in system.stream_resolver._flow_temperature_reference_components
),
)
# Pressure scopes: global plus exact selected blocks used after stream.
scopes: list[IRPressureFlowScope] = []
scopes.append(
IRPressureFlowScope(
scope_id="pressure_flow.global",
kind=IRPressureFlowScopeKind.NETWORK,
component_indices=tuple(range(len(components))),
unknown_indices=tuple(range(len(algebraic_unknowns))),
equation_indices=tuple(range(len(pressure_equations))),
block_indices=tuple(range(len(pressure_blocks))),
causal_plan_index=0,
residual_tolerance=float(pressure_solver.residual_tolerance),
max_evaluations=int(pressure_solver.max_evaluations),
sparse_pattern_trusted=bool(pressure_solver.jacobian_sparsity_is_trusted),
sparse_fallback_reason=pressure_solver.jacobian_sparsity_fallback_reason,
)
)
if not pressure_solver.jacobian_sparsity_is_trusted:
capability_issues.append(
IRCapabilityIssue(
code="IR_PRESSURE_FLOW_FINITE_DIFFERENCE_FALLBACK",
severity=IRDiagnosticSeverity.WARNING,
scope_id="pressure_flow.global",
message=(
"The reference nonlinear solve cannot trust its structural "
"Jacobian and may use finite-difference least_squares: "
f"{pressure_solver.jacobian_sparsity_fallback_reason or 'unspecified reason'}."
),
)
)
secondary_scope_indices: list[int] = []
closure_plan = system._thermofluid_closure_plan
if closure_plan.uses_conservative_global_solver:
secondary_scope_indices.append(0)
capability_issues.append(
IRCapabilityIssue(
code="IR_THERMOFLUID_CONSERVATIVE_GLOBAL_FALLBACK",
severity=IRDiagnosticSeverity.WARNING,
scope_id="thermofluid",
message=(
"Stream-sensitive closure reuses the whole-network pressure-flow "
"solver: "
f"{closure_plan.conservative_fallback_reason or 'unspecified reason'}."
),
)
)
else:
for block_solver in closure_plan.secondary_block_solvers:
if not block_solver.available:
continue
for block in block_solver.blocks:
unknown_indices = tuple(
unknown_index_by_id[unknown.id] for unknown in block.unknowns
)
equation_indices = tuple(
equation_index_by_id[equation.id] for equation in block.equations
)
# block.jacobian_entries is already local row/column structure.
rows: list[dict[int, float]] = [
{} for _ in range(len(equation_indices))
]
for row, column in block.jacobian_entries:
rows[int(row)][int(column)] = 1.0
local_pattern = _csr_matrix_from_rows(
len(equation_indices), len(unknown_indices), rows
).pattern
block_index = len(pressure_blocks)
key = (unknown_indices, equation_indices)
existing = block_key_to_index.get(key)
if existing is None:
block_key_to_index[key] = block_index
pressure_blocks.append(
IRPressureFlowBlock(
f"pressure_flow.secondary_block.{block_index}",
unknown_indices,
equation_indices,
local_pattern,
)
)
else:
block_index = existing
scope_index = len(scopes)
secondary_scope_indices.append(scope_index)
scopes.append(
IRPressureFlowScope(
scope_id=f"pressure_flow.secondary.{scope_index}",
kind=IRPressureFlowScopeKind.EQUATION_BLOCK,
component_indices=tuple(
component_index[name] for name in block.scope_components
),
unknown_indices=unknown_indices,
equation_indices=equation_indices,
block_indices=(block_index,),
causal_plan_index=None,
residual_tolerance=float(
block_solver.pressure_flow_solver.residual_tolerance
),
max_evaluations=int(
block_solver.pressure_flow_solver.max_evaluations
),
sparse_pattern_trusted=True,
sparse_fallback_reason=None,
)
)
if not secondary_scope_indices:
for pressure_scope_solver, names in zip(
closure_plan.secondary_pressure_solvers,
closure_plan.secondary_component_groups,
strict=True,
):
selected = frozenset(names)
unknown_indices = tuple(
index
for index, unknown in enumerate(pressure_solver.unknowns)
if unknown.component in selected
)
equation_indices = tuple(
index
for index, equation in enumerate(pressure_solver.equation_templates)
if (
equation.owner == "component" and equation.owner_id in selected
)
or (
equation.owner == "connection"
and connection_specs[connection_index[equation.owner_id]].endpoint_a_port_index
in {
port_index
for name in selected
for port_index in port_indices_by_component[name]
}
)
)
scope_index = len(scopes)
secondary_scope_indices.append(scope_index)
scopes.append(
IRPressureFlowScope(
scope_id=f"pressure_flow.physical_island.{scope_index}",
kind=IRPressureFlowScopeKind.PHYSICAL_ISLAND,
component_indices=tuple(component_index[name] for name in names),
unknown_indices=unknown_indices,
equation_indices=equation_indices,
block_indices=(),
causal_plan_index=None,
residual_tolerance=float(pressure_scope_solver.residual_tolerance),
max_evaluations=int(pressure_scope_solver.max_evaluations),
sparse_pattern_trusted=bool(
pressure_scope_solver.jacobian_sparsity_is_trusted
),
sparse_fallback_reason=(
pressure_scope_solver.jacobian_sparsity_fallback_reason
),
)
)
if not pressure_scope_solver.jacobian_sparsity_is_trusted:
capability_issues.append(
IRCapabilityIssue(
code="IR_PRESSURE_FLOW_FINITE_DIFFERENCE_FALLBACK",
severity=IRDiagnosticSeverity.WARNING,
scope_id=f"pressure_flow.physical_island.{scope_index}",
message=(
"This secondary nonlinear scope cannot trust its "
"structural Jacobian and may use finite-difference "
"least_squares: "
f"{pressure_scope_solver.jacobian_sparsity_fallback_reason or 'unspecified reason'}."
),
)
)
secondary_pressure_stage = add_stage(
"rhs.pressure_flow.stream_sensitive",
IRStageKind.PRESSURE_FLOW,
(
IRKernelCallOperation(
system_kernel_index,
None,
IRKernelPhase.RESIDUAL,
pressure_flow_model_read_slots,
(*all_unknown_slots, *residual_slots),
tuple(range(len(pressure_equations))),
),
)
if secondary_scope_indices
else (),
)
mechanical_acceleration_slots: dict[tuple[str, ...], IRSlotRef] = {}
acceleration_operations: list[Any] = []
for group in system.mechanical_state_reducer.groups:
names = tuple(sorted(component.name for component in group.components))
acceleration_slot = slots.add(
IRBufferKind.WORK_FLOAT,
"mechanical_group:" + ",".join(names) + ".acceleration",
semantic="mechanical_acceleration",
role="workspace",
quantity="acceleration",
unit="m/s2",
)
group_key = _mechanical_group_key(group)
mechanical_acceleration_slots[group_key] = acceleration_slot
reads = tuple(
slot
for name in names
for slot in port_slots_by_component[name]
)
mode_slot = mode_slot_by_group.get(group_key)
if mode_slot is not None:
reads = (*reads, mode_slot)
acceleration_operations.append(
IRKernelCallOperation(
system_kernel_index,
None,
IRKernelPhase.PRIMAL,
reads,
(acceleration_slot,),
)
)
acceleration_stage = add_stage(
"rhs.mechanical_acceleration",
IRStageKind.MECHANICAL_ACCELERATION,
acceleration_operations,
)
derivative_operations: list[Any] = []
for entry in system.mechanical_state_reducer.state_entries:
component = entry.representative if isinstance(entry, MechanicalConstraintGroup) else entry
extra = (
(mechanical_acceleration_slots[_mechanical_group_key(entry)],)
if isinstance(entry, MechanicalConstraintGroup)
else ()
)
derivative_operations.append(
IRKernelCallOperation(
component_kernel_index[component.name],
component_index[component.name],
IRKernelPhase.DERIVATIVE,
(
*local_state_by_component[component.name],
*port_slots_by_component[component.name],
*connected_h_slots_by_component[component.name],
*parameter_slots_by_component[component.name],
*extra,
),
local_derivative_by_component[component.name],
)
)
derivative_operations.append(
IRStateMapOperation(
IRStateMapKind.DERIVATIVE_GATHER,
tuple(local_derivative_slots),
tuple(derivative_output_slots),
)
)
derivative_stage = add_stage(
"rhs.derivative_gather",
IRStageKind.DERIVATIVE_REDUCE,
derivative_operations,
)
# Blocks encode the nested current implementation: stream converges first,
# then the outer stream/temperature/pressure loop monitors mass flows.
blocks: list[IRExecutionBlock] = []
stream_block_index = len(blocks)
blocks.append(
IRExecutionBlock(
block_id="stream.reference_global_fixed_point",
kind=IRBlockKind.STREAM_SCC,
steps=(_step(stream_nondynamic_stage),),
convergence=IRConvergenceSpec(
monitor_slots=stream_node_slots,
absolute_tolerance=0.0,
relative_tolerance=float(system.stream_resolver.relative_tolerance),
max_iterations=int(system.stream_resolver.max_iterations),
relaxation=1.0,
rollback_slots=stream_node_slots,
failure_policy=IRFailurePolicy.RETRY_SMALLER_STEP,
),
)
)
flow_slots = tuple(
unknown.slot for unknown in algebraic_unknowns if unknown.variable == "m_flow"
)
transaction_plan_object = system._thermofluid_transaction_plan
transaction_snapshot_slots = _unique_slots(
port_slot_by_variable_id[
f"{binding.component_name}.{binding.port_name}.{binding.variable}"
]
for binding in transaction_plan_object.port_value_bindings
)
thermofluid_block_index = len(blocks)
blocks.append(
IRExecutionBlock(
block_id="thermofluid.reference_fixed_point",
kind=IRBlockKind.FIXED_POINT,
steps=(
_step(stream_block_index, IRStepKind.BLOCK),
_step(stream_dynamic_stage),
_step(temperature_reference_stage),
_step(secondary_pressure_stage),
),
convergence=IRConvergenceSpec(
monitor_slots=flow_slots,
absolute_tolerance=0.0,
relative_tolerance=1.0e-12,
max_iterations=25,
relaxation=1.0,
rollback_slots=transaction_snapshot_slots,
failure_policy=IRFailurePolicy.RETRY_SMALLER_STEP,
),
)
)
stream_plans = (
IRStreamPlan(
plan_id="stream.reference_global",
node_slots=stream_node_slots,
strongly_connected_components=(
IRStreamSCC(
"stream.reference_global.scc",
stream_node_slots,
stream_block_index,
),
)
if stream_node_slots
else (),
condensed_edges=(),
topological_scc_indices=(0,) if stream_node_slots else (),
),
)
sensitive_names = tuple(
dict.fromkeys(
name
for block_solver in closure_plan.secondary_block_solvers
for name in block_solver.sensitive_components
)
)
thermofluid = IRThermofluidPlan(
physical_port_indices=tuple(
index
for index, port in enumerate(port_specs)
if port.kind is IRPortKind.PHYSICAL
),
global_component_indices=tuple(
component_index[name] for name in closure_plan.global_component_group
),
stream_plan_index=0,
secondary_pressure_scope_indices=tuple(secondary_scope_indices),
sensitive_component_indices=tuple(
component_index[name] for name in sensitive_names
),
maximum_iterations=25,
flow_relative_tolerance=1.0e-12,
uses_conservative_global_solver=bool(
closure_plan.uses_conservative_global_solver
),
conservative_fallback_reason=closure_plan.conservative_fallback_reason,
)
pressure_flow = IRPressureFlowPlan(
unknowns=tuple(algebraic_unknowns),
equations=tuple(pressure_equations),
blocks=tuple(pressure_blocks),
scopes=tuple(scopes),
global_scope_index=0,
secondary_scope_indices=tuple(secondary_scope_indices),
pressure_lower_bound=0.0,
)
rhs_steps = (
_step(state_stage),
_step(signal_stage),
_step(mechanical_equivalence_stage),
_step(volume_stage),
_step(property_stage),
_step(global_pressure_stage),
_step(thermofluid_block_index, IRStepKind.BLOCK),
_step(acceleration_stage),
_step(derivative_stage),
)
# Event roots and reset plans. Signal event entries are event-source
# families because concrete breakpoint times belong to the run plan.
events: list[IREventSpec] = []
event_operations: list[Any] = []
pending_mechanical_events: list[
tuple[str, MechanicalConstraintGroup, str, IRSlotRef]
] = []
for group in event_groups:
representative = group.representative
position_slot = local_state_by_component[representative.name][1]
owners = tuple(component_index[item.name] for item in group.components)
for side, bound, direction in (
("lower", group.lower_bound, IREventDirection.DECREASING),
("upper", group.upper_bound, IREventDirection.INCREASING),
):
if bound is None:
continue
event_id = (
"mechanical_group:"
+ ",".join(sorted(item.name for item in group.components))
+ f".{side}_impact"
)
root_slot = slots.add(
IRBufferKind.EVENT_OUTPUT,
event_id,
semantic="event_root",
role="event",
quantity="length",
unit="m",
)
event_operations.append(
IRLinearCombinationOperation((position_slot,), (1.0,), root_slot, -float(bound))
)
pending_mechanical_events.append((event_id, group, side, root_slot))
for component_name, _event_times in system.signal_resolver._event_sources:
event_id = f"{component_name}.signal_breakpoint_source"
root_slot = slots.add(
IRBufferKind.EVENT_OUTPUT,
event_id,
semantic="time_breakpoint_family",
role="event",
quantity="time",
unit="s",
owner_component_index=component_index[component_name],
)
event_operations.append(
IRKernelCallOperation(
component_kernel_index[component_name],
component_index[component_name],
IRKernelPhase.EVENT,
(time_slot, *parameter_slots_by_component[component_name]),
(root_slot,),
)
)
events.append(
IREventSpec(
event_id=event_id,
event_kind="known_time_breakpoint_source",
owner_component_indices=(component_index[component_name],),
root_slot=root_slot,
direction=IREventDirection.ANY,
terminal=False,
priority=100,
mode_guards=(),
reset_steps=(),
invalidated_caches=(
IRCacheKind.PRESSURE_FLOW,
IRCacheKind.STREAM,
IRCacheKind.JACOBIAN,
IRCacheKind.OUTPUT,
),
restarts_integrator=True,
)
)
event_stage = add_stage("events.evaluate", IRStageKind.EVENT, event_operations)
for event_id, group, side, root_slot in pending_mechanical_events:
group_key = _mechanical_group_key(group)
state_offset = group_solver_offset[group_key]
mode_slot = mode_slot_by_group[group_key]
reset_stage = add_stage(
f"reset.{event_id}",
IRStageKind.RESET,
(
IRKernelCallOperation(
system_kernel_index,
None,
IRKernelPhase.RESET,
(
state_input_slots[state_offset],
state_input_slots[state_offset + 1],
mode_slot,
),
(
state_input_slots[state_offset],
state_input_slots[state_offset + 1],
mode_slot,
),
),
),
)
events.append(
IREventSpec(
event_id=event_id,
event_kind=f"mechanical_{side}_impact",
owner_component_indices=tuple(
component_index[item.name] for item in group.components
),
root_slot=root_slot,
direction=(
IREventDirection.DECREASING
if side == "lower"
else IREventDirection.INCREASING
),
terminal=False,
priority=10,
mode_guards=(),
reset_steps=(_step(reset_stage),),
invalidated_caches=(
IRCacheKind.PRESSURE_FLOW,
IRCacheKind.STREAM,
IRCacheKind.JACOBIAN,
IRCacheKind.OUTPUT,
),
restarts_integrator=True,
)
)
# Fixed CSR state Jacobian and deterministic seed-0 coloring.
jacobian_pattern = _csr_pattern(system.jacobian_sparsity())
jacobian_value_slots = tuple(
slots.add(
IRBufferKind.JACOBIAN_VALUE,
f"jacobian.value.{index}",
semantic="jacobian_value",
role="jacobian",
)
for index in range(jacobian_pattern.nonzero_count)
)
try:
from scipy.optimize._numdiff import group_columns
groups = group_columns(system.jacobian_sparsity(), order=0)
color_count = int(groups.max(initial=-1)) + 1
color_groups = tuple(
tuple(int(index) for index, color in enumerate(groups) if int(color) == group)
for group in range(color_count)
)
except (ImportError, AttributeError, TypeError):
color_groups = tuple((index,) for index in range(len(initial_state)))
capability_issues.append(
IRCapabilityIssue(
code="IR_JACOBIAN_COLORING_UNAVAILABLE",
severity=IRDiagnosticSeverity.WARNING,
scope_id="jacobian",
message="Deterministic SciPy column coloring was unavailable.",
)
)
positions_by_column: list[list[int]] = [
[] for _ in range(jacobian_pattern.column_count)
]
for row in range(jacobian_pattern.row_count):
for position in range(
jacobian_pattern.row_pointers[row],
jacobian_pattern.row_pointers[row + 1],
):
positions_by_column[jacobian_pattern.column_indices[position]].append(position)
exact_rows = system._exact_ode_jacobian_rows()
analytic_positions: list[int] = []
for row, columns in exact_rows.items():
for position in range(
jacobian_pattern.row_pointers[row],
jacobian_pattern.row_pointers[row + 1],
):
if jacobian_pattern.column_indices[position] in columns:
analytic_positions.append(position)
analytic_position_set = set(analytic_positions)
finite_difference_columns = tuple(
IRFiniteDifferenceColumn(
column_index=column,
value_indices=tuple(
position for position in positions if position not in analytic_position_set
),
relative_step=1.4901161193847656e-08,
)
for column, positions in enumerate(positions_by_column)
if any(position not in analytic_position_set for position in positions)
)
jacobian_stage = add_stage(
"jacobian.fill",
IRStageKind.JACOBIAN,
(
IRKernelCallOperation(
system_kernel_index,
None,
IRKernelPhase.JACOBIAN,
(time_slot, *state_input_slots),
jacobian_value_slots,
),
),
)
jacobian = IRJacobianPlan(
pattern=jacobian_pattern,
value_slots=jacobian_value_slots,
color_groups=color_groups,
fill_steps=(*rhs_steps, _step(jacobian_stage)),
analytic_value_indices=tuple(sorted(set(analytic_positions))),
local_finite_difference_columns=finite_difference_columns,
)
output_operations: list[Any] = []
for component in components:
derived_sources = tuple(
output_source_slots[index]
for index in output_indices_by_component[component.name]
if result_metadata[index].scope == "component"
)
if derived_sources:
output_operations.append(
IRKernelCallOperation(
component_kernel_index[component.name],
component_index[component.name],
IRKernelPhase.OUTPUT,
(
*local_state_by_component.get(component.name, ()),
*port_slots_by_component[component.name],
*parameter_slots_by_component[component.name],
),
derived_sources,
)
)
output_operations.extend(
IRCopyOperation(source, target)
for source, target in zip(output_source_slots, output_slots, strict=True)
)
output_stage = add_stage("outputs.project", IRStageKind.OUTPUT, output_operations)
output_specs = tuple(
IROutputSpec(
output_id=metadata.key,
component_index=component_index[metadata.component_id],
scope=metadata.scope,
port_name=metadata.port_name,
name=metadata.name,
label=metadata.label,
category=metadata.category,
quantity=metadata.quantity,
unit=metadata.unit,
order=int(metadata.order),
source_slot=output_source_slots[index],
output_slot=output_slots[index],
)
for index, metadata in enumerate(result_metadata)
)
# C-01 can only describe the current object kernels as reference calls. Use
# conservative, explicit input supersets so dependency slicing and cache
# invalidation never omit parameters, modes, medium constants, or port
# values that those Python methods may reach indirectly. C-02 will replace
# these supersets with independently declared, fixed kernel signatures.
medium_slots_by_component: dict[int, list[IRSlotRef]] = {
index: [] for index in range(len(components))
}
for medium in medium_specs:
for owner_index in medium.component_indices:
medium_slots_by_component[owner_index].extend(medium.parameter_slots)
component_reference_reads = tuple(
_unique_slots(
(
*component.parameter_slots,
*component.state_slots,
*component.mode_slots,
*component.port_slots,
*component.workspace_float_slots,
*component.workspace_int_slots,
*medium_slots_by_component[owner_index],
)
)
for owner_index, component in enumerate(component_instances)
)
system_reference_reads = _unique_slots(
IRSlotRef(kind, index)
for kind in (
IRBufferKind.TIME,
IRBufferKind.STATE_INPUT,
IRBufferKind.LOCAL_STATE,
IRBufferKind.ALGEBRAIC,
IRBufferKind.SIGNAL,
IRBufferKind.PARAMETER,
IRBufferKind.CONSTANT,
IRBufferKind.MODE,
IRBufferKind.RUNTIME_INPUT,
)
for index in range(len(slots.initial[kind]))
)
completed_stages: list[IRStage] = []
for stage in stages:
completed_operations: list[Any] = []
for operation in stage.operations:
if isinstance(operation, IRKernelCallOperation):
extra_reads = (
system_reference_reads
if operation.component_index is None
else component_reference_reads[operation.component_index]
)
operation = replace(
operation,
read_slots=_unique_slots((*operation.read_slots, *extra_reads)),
)
completed_operations.append(operation)
completed_stages.append(
_stage(stage.stage_id, stage.kind, completed_operations)
)
stages = completed_stages
# The Python PortState object has an ``m_flow`` attribute even for
# mechanical ports, and the reference transaction helper consequently
# carries inert bindings for those ports. They are always hidden zeroes:
# mechanical active metadata exposes ``f``, not ``m_flow``. The numeric IR
# records only the physical thermofluid closure quantities that can change
# (active pneumatic ``m_flow`` slots); inventing hidden slots or treating
# force as mass flow would give the native contract different semantics.
transaction_flow_slots = _unique_slots(
slot
for item in transaction_plan_object.flow_bindings
for slot in (
port_slot_by_variable_id.get(
f"{item.component_name}.{item.port_name}.m_flow"
),
)
if slot is not None
)
transaction = IRTransactionPlan(
snapshot_slots=transaction_snapshot_slots,
flow_slots=transaction_flow_slots,
cache_component_indices=tuple(
component_index[item.component.name]
for item in transaction_plan_object.component_cache_bindings
),
cache_attribute_ids=tuple(
f"{item.component.name}.{name}"
for item in transaction_plan_object.component_cache_bindings
for name in item.attribute_names
),
diagnostic_owner_ids=tuple(
f"{type(owner).__module__}.{type(owner).__qualname__}:{index}"
for index, owner in enumerate(transaction_plan_object.diagnostic_owners)
),
restores_on_recoverable_failure=True,
restores_on_fatal_failure=True,
)
component_capabilities = tuple(
IRComponentCapability(
component_index=index,
level=IRCapabilityLevel.REFERENCE_ONLY,
supported_phases=_component_phases(component),
missing_features=("c02_pure_numeric_kernel", "native_implementation_version"),
)
for index, component in enumerate(components)
)
capability_issues.insert(
0,
IRCapabilityIssue(
code="IR_NATIVE_KERNELS_NOT_DECLARED",
severity=IRDiagnosticSeverity.WARNING,
scope_id=network.name,
message=(
"System structure and conservative reference-kernel dependencies "
"are described, but Python kernel effects remain opaque until C-02 "
"declares pure numeric call signatures."
),
),
)
capabilities = IRCapabilityReport(
system_level=(
IRCapabilityLevel.UNSUPPORTED
if any(issue.severity is IRDiagnosticSeverity.ERROR for issue in capability_issues)
else IRCapabilityLevel.REFERENCE_ONLY
),
components=component_capabilities,
issues=tuple(capability_issues),
)
entry_points = (
IREntryPoint(
IREntryPointKind.RHS,
rhs_steps,
(time_slot, *state_input_slots),
tuple(derivative_output_slots),
),
IREntryPoint(
IREntryPointKind.EVENTS,
(_step(state_stage), _step(event_stage)),
(time_slot, *state_input_slots),
tuple(event.root_slot for event in events),
),
IREntryPoint(
IREntryPointKind.JACOBIAN,
(*rhs_steps, _step(jacobian_stage)),
(time_slot, *state_input_slots),
jacobian_value_slots,
),
IREntryPoint(
IREntryPointKind.OUTPUTS,
(
_step(state_stage),
_step(signal_stage),
_step(mechanical_equivalence_stage),
_step(volume_stage),
_step(property_stage),
_step(global_pressure_stage),
_step(thermofluid_block_index, IRStepKind.BLOCK),
_step(acceleration_stage),
_step(output_stage),
),
(time_slot, *state_input_slots),
tuple(output_slots),
),
)
required_features = (
"callback_free",
"independent_entry_points",
"transactional_closure",
"fixed_csr_jacobian",
"reference_kernel_dispatch",
)
program = SystemIR(
version=CURRENT_SYSTEM_IR_VERSION,
model_id=network.name,
model_version=str(model_version),
compiler_id=SYSTEM_NUMERIC_IR_COMPILER_ID,
compiler_version=SYSTEM_NUMERIC_IR_COMPILER_VERSION,
numeric_dtype=IRDType.FLOAT64,
buffers=slots.buffers(),
values=tuple(slots.values),
kernels=tuple(kernels),
components=component_instances,
mediums=tuple(medium_specs),
ports=tuple(port_specs),
connections=tuple(connection_specs),
state_reducer=state_reducer,
causal_plans=tuple(causal_plans),
pressure_flow=pressure_flow,
stream_plans=stream_plans,
thermofluid=thermofluid,
stages=tuple(stages),
blocks=tuple(blocks),
entry_points=entry_points,
transaction=transaction,
modes=tuple(modes),
jacobian=jacobian,
events=tuple(events),
outputs=output_specs,
capabilities=capabilities,
required_features=required_features,
)
# Validation is mandatory at the compiler boundary. Validation owns the
# exception type and complete invariant list; callers never receive an
# unchecked program.
from .validation import require_valid_system_ir
return require_valid_system_ir(program)
__all__ = ["compile_system_ir"]