2247 lines
88 KiB
Python
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"]
|