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

1584 lines
88 KiB
Python

"""Fail-closed static validation for System Numeric IR v2."""
from __future__ import annotations
from dataclasses import dataclass, fields, is_dataclass
from math import isfinite
from types import UnionType
from typing import Union, get_args, get_origin, get_type_hints
from app.simulation.ir import schema as ir
SUPPORTED_REQUIRED_FEATURES = frozenset(
{
"callback_free",
"independent_entry_points",
"transactional_closure",
"fixed_csr_jacobian",
"reference_kernel_dispatch",
}
)
@dataclass(frozen=True, slots=True)
class IRValidationIssue:
code: str
path: str
message: str
@dataclass(frozen=True, slots=True)
class IRValidationReport:
issues: tuple[IRValidationIssue, ...]
@property
def valid(self) -> bool:
return not self.issues
class SystemIRValidationError(ValueError):
def __init__(self, report: IRValidationReport) -> None:
self.report = report
summary = "; ".join(
f"{issue.code} at {issue.path}: {issue.message}"
for issue in report.issues[:8]
)
if len(report.issues) > 8:
summary += f"; and {len(report.issues) - 8} more issue(s)"
super().__init__(f"System IR validation failed: {summary}")
class _Issues:
def __init__(self) -> None:
self.items: list[IRValidationIssue] = []
def add(self, code: str, path: str, message: str) -> None:
self.items.append(IRValidationIssue(code, path, message))
def unique_ids(self, values: tuple[object, ...], attribute: str, path: str) -> None:
seen: dict[str, int] = {}
for index, value in enumerate(values):
identity = str(getattr(value, attribute))
if not identity:
self.add("EMPTY_ID", f"{path}[{index}].{attribute}", "ID must not be empty.")
if identity in seen:
self.add(
"DUPLICATE_ID",
f"{path}[{index}].{attribute}",
f"Duplicates {path}[{seen[identity]}].",
)
else:
seen[identity] = index
def _finite(value: float | None) -> bool:
return value is None or isfinite(float(value))
_TYPE_HINT_CACHE: dict[type[object], dict[str, object]] = {}
def _matches_runtime_type(value: object, annotation: object) -> bool:
origin = get_origin(annotation)
if origin in {UnionType, Union}:
return any(_matches_runtime_type(value, item) for item in get_args(annotation))
if origin is tuple:
return type(value) is tuple
if annotation is type(None):
return value is None
if isinstance(annotation, type):
return type(value) is annotation
return False
def _validate_runtime_types(
value: object,
annotation: object,
path: str,
issues: _Issues,
seen: set[tuple[int, object]],
) -> None:
"""Reject values that would serialize differently from their schema type."""
origin = get_origin(annotation)
if origin in {UnionType, Union}:
matching = tuple(
item for item in get_args(annotation) if _matches_runtime_type(value, item)
)
if len(matching) != 1:
issues.add(
"RUNTIME_TYPE_MISMATCH",
path,
f"Value has type {type(value).__name__}, which does not match the declared union.",
)
return
_validate_runtime_types(value, matching[0], path, issues, seen)
return
if origin is tuple:
if type(value) is not tuple:
issues.add(
"RUNTIME_TYPE_MISMATCH",
path,
"Schema arrays must be immutable tuples before serialization.",
)
return
arguments = get_args(annotation)
if len(arguments) == 2 and arguments[1] is Ellipsis:
for index, item in enumerate(value):
_validate_runtime_types(
item,
arguments[0],
f"{path}[{index}]",
issues,
seen,
)
return
if len(value) != len(arguments):
issues.add(
"RUNTIME_TYPE_MISMATCH",
path,
"Tuple length differs from its declared schema type.",
)
return
for index, (item, item_type) in enumerate(zip(value, arguments)):
_validate_runtime_types(
item,
item_type,
f"{path}[{index}]",
issues,
seen,
)
return
if annotation is type(None):
if value is not None:
issues.add("RUNTIME_TYPE_MISMATCH", path, "Expected null.")
return
if not isinstance(annotation, type) or type(value) is not annotation:
expected_name = getattr(annotation, "__name__", str(annotation))
issues.add(
"RUNTIME_TYPE_MISMATCH",
path,
f"Expected {expected_name}, received {type(value).__name__}.",
)
return
if not is_dataclass(value):
return
marker = (id(value), annotation)
if marker in seen:
return
seen.add(marker)
hints = _TYPE_HINT_CACHE.get(annotation)
if hints is None:
hints = get_type_hints(annotation)
_TYPE_HINT_CACHE[annotation] = hints
for item in fields(value):
_validate_runtime_types(
getattr(value, item.name),
hints[item.name],
f"{path}.{item.name}",
issues,
seen,
)
def _validate_csr(
pattern: ir.IRCSRPattern,
path: str,
issues: _Issues,
) -> None:
if pattern.row_count < 0 or pattern.column_count < 0:
issues.add("CSR_NEGATIVE_SHAPE", path, "CSR dimensions must be non-negative.")
pointers = pattern.row_pointers
columns = pattern.column_indices
if len(pointers) != pattern.row_count + 1:
issues.add("CSR_ROW_POINTER_LENGTH", f"{path}.row_pointers", "Length must be row_count + 1.")
return
if not pointers or pointers[0] != 0:
issues.add("CSR_ROW_POINTER_START", f"{path}.row_pointers", "First pointer must be zero.")
return
if any(first > second for first, second in zip(pointers, pointers[1:])):
issues.add("CSR_ROW_POINTER_ORDER", f"{path}.row_pointers", "Pointers must be monotonic.")
return
if pointers[-1] != len(columns):
issues.add("CSR_NONZERO_COUNT", path, "Final row pointer must equal column count length.")
return
for row in range(pattern.row_count):
row_columns = columns[pointers[row] : pointers[row + 1]]
if any(column < 0 or column >= pattern.column_count for column in row_columns):
issues.add("CSR_COLUMN_BOUNDS", f"{path}.row[{row}]", "Column index is out of bounds.")
if tuple(sorted(set(row_columns))) != row_columns:
issues.add("CSR_ROW_CANONICAL", f"{path}.row[{row}]", "Columns must be sorted and unique.")
def _operation_slots(operation: ir.IROperation) -> tuple[ir.IRSlotRef, ...]:
return (*ir.operation_read_slots(operation), *ir.operation_write_slots(operation))
def validate_system_ir(program: ir.SystemIR) -> IRValidationReport:
"""Validate every cross-reference and numeric invariant in one program."""
issues = _Issues()
_validate_runtime_types(program, ir.SystemIR, "$", issues, set())
if issues.items:
return IRValidationReport(tuple(issues.items))
if program.version.schema_id != ir.SYSTEM_NUMERIC_IR_SCHEMA_ID:
issues.add("SCHEMA_ID_UNSUPPORTED", "version.schema_id", "Unknown schema ID.")
if program.version.major != ir.SYSTEM_NUMERIC_IR_SCHEMA_MAJOR:
issues.add("SCHEMA_MAJOR_UNSUPPORTED", "version.major", "Unknown schema major version.")
if program.version.minor < 0:
issues.add("SCHEMA_MINOR_INVALID", "version.minor", "Schema minor version cannot be negative.")
elif program.version.minor > ir.SYSTEM_NUMERIC_IR_SCHEMA_MINOR:
issues.add("SCHEMA_MINOR_UNSUPPORTED", "version.minor", "Schema minor version is newer than this reader.")
if program.numeric_dtype is not ir.IRDType.FLOAT64:
issues.add("NUMERIC_DTYPE_UNSUPPORTED", "numeric_dtype", "Only float64 programs are supported.")
for name, value in (
("model_id", program.model_id),
("model_version", program.model_version),
("compiler_id", program.compiler_id),
("compiler_version", program.compiler_version),
):
if not value:
issues.add("EMPTY_ID", name, f"{name} must not be empty.")
unknown_features = sorted(set(program.required_features) - SUPPORTED_REQUIRED_FEATURES)
if unknown_features:
issues.add("REQUIRED_FEATURE_UNSUPPORTED", "required_features", ", ".join(unknown_features))
if len(set(program.required_features)) != len(program.required_features):
issues.add("DUPLICATE_REQUIRED_FEATURE", "required_features", "Features must be unique.")
buffer_by_kind: dict[ir.IRBufferKind, ir.IRBufferSpec] = {}
expected_buffer_dtypes = {
kind: (
ir.IRDType.INT32
if kind in {ir.IRBufferKind.MODE, ir.IRBufferKind.WORK_INT}
else ir.IRDType.FLOAT64
)
for kind in ir.IRBufferKind
}
for index, buffer in enumerate(program.buffers):
path = f"buffers[{index}]"
if buffer.kind in buffer_by_kind:
issues.add("DUPLICATE_BUFFER", path, f"Buffer {buffer.kind.value} is declared twice.")
buffer_by_kind[buffer.kind] = buffer
if buffer.size < 0:
issues.add("BUFFER_NEGATIVE_SIZE", f"{path}.size", "Size must be non-negative.")
if buffer.dtype is not expected_buffer_dtypes[buffer.kind]:
issues.add(
"BUFFER_DTYPE_INVALID",
f"{path}.dtype",
f"{buffer.kind.value} requires {expected_buffer_dtypes[buffer.kind].value}.",
)
if buffer.initial_float_values and buffer.initial_int_values:
issues.add("BUFFER_MIXED_INITIAL_VALUES", path, "A buffer cannot have both float and integer initial values.")
if buffer.initial_float_values and len(buffer.initial_float_values) != buffer.size:
issues.add("BUFFER_INITIAL_SIZE", path, "Float initial values must cover the full buffer.")
if buffer.initial_int_values and len(buffer.initial_int_values) != buffer.size:
issues.add("BUFFER_INITIAL_SIZE", path, "Integer initial values must cover the full buffer.")
if any(not isfinite(float(value)) for value in buffer.initial_float_values):
issues.add("NONFINITE_VALUE", path, "Buffer initial values must be finite.")
if buffer.dtype is ir.IRDType.FLOAT64 and buffer.initial_int_values:
issues.add("BUFFER_INITIAL_TYPE", path, "float64 buffer cannot use integer initial values.")
if buffer.dtype is not ir.IRDType.FLOAT64 and buffer.initial_float_values:
issues.add("BUFFER_INITIAL_TYPE", path, "Integer buffer cannot use float initial values.")
if any(value < -(2**31) or value > 2**31 - 1 for value in buffer.initial_int_values):
issues.add("BUFFER_INT32_RANGE", path, "Integer initial values must fit signed int32.")
required_buffers = set(ir.IRBufferKind)
for kind in sorted(required_buffers - set(buffer_by_kind), key=lambda value: value.value):
issues.add("MISSING_BUFFER", "buffers", f"Missing {kind.value} buffer.")
time_buffer = buffer_by_kind.get(ir.IRBufferKind.TIME)
if time_buffer is not None and time_buffer.size != 1:
issues.add("TIME_BUFFER_SIZE", "buffers", "The time buffer must contain exactly one float64 value.")
def valid_slot(slot: ir.IRSlotRef, path: str) -> bool:
buffer = buffer_by_kind.get(slot.buffer)
if buffer is None:
issues.add("SLOT_BUFFER_MISSING", path, f"Buffer {slot.buffer.value} is not declared.")
return False
if slot.index < 0 or slot.index >= buffer.size:
issues.add("SLOT_INDEX_BOUNDS", path, f"Index {slot.index} is outside buffer size {buffer.size}.")
return False
return True
issues.unique_ids(program.values, "value_id", "values")
value_slots: set[ir.IRSlotRef] = set()
for index, value in enumerate(program.values):
path = f"values[{index}]"
valid_slot(value.slot, f"{path}.slot")
if value.slot in value_slots:
issues.add("DUPLICATE_VALUE_SLOT", f"{path}.slot", "A numeric slot has more than one value descriptor.")
value_slots.add(value.slot)
if not value.semantic or not value.role or not value.quantity:
issues.add(
"VALUE_METADATA_INCOMPLETE",
path,
"Value semantic, role and quantity must not be empty.",
)
if not isfinite(value.scale) or value.scale <= 0.0:
issues.add("VALUE_SCALE_INVALID", f"{path}.scale", "Scale must be finite and positive.")
if not _finite(value.lower_bound) or not _finite(value.upper_bound):
issues.add("NONFINITE_VALUE", path, "Bounds must be finite or null.")
if value.lower_bound is not None and value.upper_bound is not None and value.lower_bound > value.upper_bound:
issues.add("VALUE_BOUNDS_ORDER", path, "Lower bound exceeds upper bound.")
if value.owner_component_index is not None and not 0 <= value.owner_component_index < len(program.components):
issues.add("COMPONENT_INDEX_BOUNDS", f"{path}.owner_component_index", "Component index is invalid.")
expected_value_slots = {
ir.IRSlotRef(buffer.kind, index)
for buffer in program.buffers
for index in range(max(0, buffer.size))
}
if value_slots != expected_value_slots:
issues.add(
"VALUE_SLOT_COVERAGE",
"values",
"Value descriptors must cover every declared buffer slot exactly once.",
)
issues.unique_ids(program.kernels, "kernel_id", "kernels")
for index, kernel in enumerate(program.kernels):
path = f"kernels[{index}]"
if not kernel.model_type or not kernel.model_version or not kernel.implementation_version:
issues.add("KERNEL_IDENTITY_INCOMPLETE", path, "Kernel type and versions are required.")
counts = (
kernel.parameter_count,
kernel.state_count,
kernel.mode_count,
kernel.workspace_float_count,
kernel.workspace_int_count,
)
if any(count < 0 for count in counts):
issues.add("KERNEL_NEGATIVE_ARITY", path, "Kernel arities must be non-negative.")
phases = tuple(phase.phase for phase in kernel.phases)
if not phases:
issues.add("KERNEL_PHASE_MISSING", f"{path}.phases", "Every kernel must declare at least one phase.")
if len(set(phases)) != len(phases):
issues.add("KERNEL_DUPLICATE_PHASE", f"{path}.phases", "Kernel phases must be unique.")
if kernel.availability is ir.IRKernelAvailability.NATIVE and kernel.unavailable_reason is not None:
issues.add("KERNEL_AVAILABILITY_CONFLICT", path, "Native kernel cannot have an unavailable reason.")
if kernel.availability is ir.IRKernelAvailability.REFERENCE_ONLY and not kernel.unavailable_reason:
issues.add("KERNEL_AVAILABILITY_REASON_MISSING", path, "Reference-only kernel needs a reason.")
issues.unique_ids(program.components, "instance_id", "components")
for index, component in enumerate(program.components):
path = f"components[{index}]"
if not 0 <= component.kernel_index < len(program.kernels):
issues.add("KERNEL_INDEX_BOUNDS", f"{path}.kernel_index", "Kernel index is invalid.")
continue
kernel = program.kernels[component.kernel_index]
for slot_index, slot in enumerate(
(*component.parameter_slots, *component.state_slots, *component.derivative_slots,
*component.mode_slots, *component.port_slots, *component.workspace_float_slots,
*component.workspace_int_slots)
):
valid_slot(slot, f"{path}.slots[{slot_index}]")
binding_kinds = (
(component.parameter_slots, ir.IRBufferKind.PARAMETER, "parameter_slots"),
(component.state_slots, ir.IRBufferKind.LOCAL_STATE, "state_slots"),
(
component.derivative_slots,
ir.IRBufferKind.LOCAL_DERIVATIVE,
"derivative_slots",
),
(component.mode_slots, ir.IRBufferKind.MODE, "mode_slots"),
(
component.workspace_float_slots,
ir.IRBufferKind.WORK_FLOAT,
"workspace_float_slots",
),
(
component.workspace_int_slots,
ir.IRBufferKind.WORK_INT,
"workspace_int_slots",
),
)
for bound_slots, expected_kind, label in binding_kinds:
if any(slot.buffer is not expected_kind for slot in bound_slots):
issues.add(
"COMPONENT_BINDING_BUFFER",
f"{path}.{label}",
f"Bindings must use the {expected_kind.value} buffer.",
)
if len(set(bound_slots)) != len(bound_slots):
issues.add(
"COMPONENT_BINDING_DUPLICATE",
f"{path}.{label}",
"Bindings within one component must be unique.",
)
if len(component.parameter_slots) != kernel.parameter_count:
issues.add("KERNEL_PARAMETER_ARITY", path, "Component parameter binding count differs from kernel.")
if len(component.state_slots) != kernel.state_count or len(component.derivative_slots) != kernel.state_count:
issues.add("KERNEL_STATE_ARITY", path, "Component state bindings differ from kernel.")
if len(component.mode_slots) != kernel.mode_count:
issues.add("KERNEL_MODE_ARITY", path, "Component mode bindings differ from kernel.")
if len(component.workspace_float_slots) != kernel.workspace_float_count:
issues.add("KERNEL_WORKSPACE_ARITY", path, "Float workspace bindings differ from kernel.")
if len(component.workspace_int_slots) != kernel.workspace_int_count:
issues.add("KERNEL_WORKSPACE_ARITY", path, "Integer workspace bindings differ from kernel.")
if any(port < 0 or port >= len(program.ports) for port in component.port_indices):
issues.add("PORT_INDEX_BOUNDS", f"{path}.port_indices", "Port index is invalid.")
if any(output < 0 or output >= len(program.outputs) for output in component.output_indices):
issues.add("OUTPUT_INDEX_BOUNDS", f"{path}.output_indices", "Output index is invalid.")
issues.unique_ids(program.ports, "port_id", "ports")
port_variable_ids: set[str] = set()
for index, port in enumerate(program.ports):
path = f"ports[{index}]"
if not port.name or not port.domain:
issues.add("PORT_METADATA_INCOMPLETE", path, "Port name and domain must not be empty.")
if not 0 <= port.component_index < len(program.components):
issues.add("COMPONENT_INDEX_BOUNDS", f"{path}.component_index", "Component index is invalid.")
if port.kind is ir.IRPortKind.PHYSICAL and port.positive_flow_direction is None:
issues.add("PORT_FLOW_DIRECTION_MISSING", path, "Physical ports need a positive-flow direction.")
if port.kind is ir.IRPortKind.SIGNAL and port.positive_flow_direction is not None:
issues.add("PORT_FLOW_DIRECTION_INVALID", path, "Signal ports cannot define physical flow direction.")
names: set[str] = set()
for variable_index, variable in enumerate(port.variables):
variable_path = f"{path}.variables[{variable_index}]"
if not variable.variable_id or not variable.name:
issues.add("PORT_VARIABLE_ID_INVALID", variable_path, "Port variable names and IDs must be non-empty.")
if not variable.quantity:
issues.add("PORT_VARIABLE_METADATA_INCOMPLETE", variable_path, "Port variable quantity must not be empty.")
if variable.variable_id in port_variable_ids:
issues.add("PORT_VARIABLE_ID_INVALID", variable_path, "Port variable ID must be globally unique.")
port_variable_ids.add(variable.variable_id)
if variable.name in names:
issues.add("PORT_VARIABLE_ID_INVALID", variable_path, "Port variable name must be unique within its port.")
names.add(variable.name)
valid_slot(variable.slot, f"{variable_path}.slot")
expected_rule = {
ir.IRVariableRole.EFFORT: ir.IRConnectionRule.EQUAL,
ir.IRVariableRole.FLOW: ir.IRConnectionRule.SUM_TO_ZERO,
ir.IRVariableRole.STREAM: ir.IRConnectionRule.STREAM_MIX,
ir.IRVariableRole.SIGNAL: ir.IRConnectionRule.DIRECTED,
}[variable.role]
if variable.connection_rule is not expected_rule:
issues.add(
"PORT_VARIABLE_RULE_INVALID",
variable_path,
f"{variable.role.value} variables require {expected_rule.value}.",
)
if 0 <= port.component_index < len(program.components) and index not in program.components[port.component_index].port_indices:
issues.add("COMPONENT_PORT_BACKREF", path, "Owning component does not reference this port.")
for component_index, component in enumerate(program.components):
expected_port_indices = tuple(
index
for index, port in enumerate(program.ports)
if port.component_index == component_index
)
if component.port_indices != expected_port_indices:
issues.add(
"COMPONENT_PORT_COVERAGE",
f"components[{component_index}].port_indices",
"Component port indices must exactly cover its owned ports in program order.",
)
if all(0 <= index < len(program.ports) for index in component.port_indices):
expected_port_slots = tuple(
variable.slot
for port_index in component.port_indices
for variable in program.ports[port_index].variables
)
if component.port_slots != expected_port_slots:
issues.add(
"COMPONENT_PORT_SLOT_COVERAGE",
f"components[{component_index}].port_slots",
"Component port slots must match its ordered port-variable bindings.",
)
issues.unique_ids(program.connections, "connection_id", "connections")
connected_port_indices: list[int] = []
endpoint_pairs: set[tuple[int, int]] = set()
for index, connection in enumerate(program.connections):
path = f"connections[{index}]"
endpoints = (connection.endpoint_a_port_index, connection.endpoint_b_port_index)
if endpoints[0] == endpoints[1]:
issues.add("CONNECTION_SELF_LOOP", path, "Connection endpoints must be distinct ports.")
if any(endpoint < 0 or endpoint >= len(program.ports) for endpoint in endpoints):
issues.add("PORT_INDEX_BOUNDS", path, "Connection endpoint is invalid.")
continue
connected_port_indices.extend(endpoints)
endpoint_pair = tuple(sorted(endpoints))
if endpoint_pair in endpoint_pairs:
issues.add("CONNECTION_DUPLICATE", path, "The same port pair is connected more than once.")
endpoint_pairs.add(endpoint_pair)
first, second = (program.ports[endpoint] for endpoint in endpoints)
if first.kind is not connection.kind or second.kind is not connection.kind or first.domain != connection.domain or second.domain != connection.domain:
issues.add("CONNECTION_DOMAIN_MISMATCH", path, "Connection kind/domain differs from an endpoint.")
first_variables = {variable.name: variable for variable in first.variables}
second_variables = {variable.name: variable for variable in second.variables}
connection_names = tuple(variable.name for variable in connection.variables)
if (
len(set(connection_names)) != len(connection_names)
or set(connection_names) != set(first_variables)
or set(connection_names) != set(second_variables)
):
issues.add(
"CONNECTION_VARIABLE_COVERAGE",
f"{path}.variables",
"Connection variables must cover both endpoint contracts exactly once.",
)
for variable_index, variable in enumerate(connection.variables):
variable_path = f"{path}.variables[{variable_index}]"
valid_slot(variable.endpoint_a_slot, f"{variable_path}.endpoint_a_slot")
valid_slot(variable.endpoint_b_slot, f"{variable_path}.endpoint_b_slot")
if variable.name not in first_variables or variable.name not in second_variables:
issues.add("CONNECTION_VARIABLE_MISSING", variable_path, "Variable is absent from an endpoint.")
elif (
first_variables[variable.name].slot != variable.endpoint_a_slot
or second_variables[variable.name].slot != variable.endpoint_b_slot
or first_variables[variable.name].connection_rule is not variable.rule
or second_variables[variable.name].connection_rule is not variable.rule
):
issues.add("CONNECTION_VARIABLE_MISMATCH", variable_path, "Connection variable does not match its ports.")
connection_counts: dict[int, int] = {}
for port_index in connected_port_indices:
connection_counts[port_index] = connection_counts.get(port_index, 0) + 1
for port_index, count in connection_counts.items():
port = program.ports[port_index]
allows_fan_out = (
port.kind is ir.IRPortKind.SIGNAL
and port.nominal_role is ir.IRPortNominalRole.OUTPUT
)
if count > 1 and not allows_fan_out:
issues.add(
"CONNECTION_PORT_REUSED",
f"ports[{port_index}]",
"Physical ports and signal inputs may have at most one connection.",
)
issues.unique_ids(program.mediums, "medium_id", "mediums")
for index, medium in enumerate(program.mediums):
path = f"mediums[{index}]"
if not medium.name or not medium.implementation_id or not medium.implementation_version:
issues.add("MEDIUM_IDENTITY_INCOMPLETE", path, "Medium identity is incomplete.")
if len(set(medium.parameter_slots)) != len(medium.parameter_slots):
issues.add("MEDIUM_PARAMETER_DUPLICATE", f"{path}.parameter_slots", "Medium parameter slots must be unique.")
if len(set(medium.component_indices)) != len(medium.component_indices):
issues.add("MEDIUM_COMPONENT_DUPLICATE", f"{path}.component_indices", "Medium component indices must be unique.")
for slot_index, slot in enumerate(medium.parameter_slots):
valid_slot(slot, f"{path}.parameter_slots[{slot_index}]")
if slot.buffer is not ir.IRBufferKind.CONSTANT:
issues.add("MEDIUM_PARAMETER_BUFFER", f"{path}.parameter_slots[{slot_index}]", "Medium constants must use the constant buffer.")
if any(component < 0 or component >= len(program.components) for component in medium.component_indices):
issues.add("COMPONENT_INDEX_BOUNDS", f"{path}.component_indices", "Medium component index is invalid.")
reducer = program.state_reducer
state_buffer = buffer_by_kind.get(ir.IRBufferKind.STATE_INPUT)
derivative_buffer = buffer_by_kind.get(ir.IRBufferKind.DERIVATIVE_OUTPUT)
if state_buffer is not None and state_buffer.size != reducer.solver_state_count:
issues.add("STATE_COUNT_MISMATCH", "state_reducer", "State buffer size differs from reducer count.")
if derivative_buffer is not None and derivative_buffer.size != reducer.solver_state_count:
issues.add("DERIVATIVE_COUNT_MISMATCH", "state_reducer", "Derivative buffer size differs from reducer count.")
if len(reducer.initial_state) != reducer.solver_state_count or len(reducer.absolute_tolerances) != reducer.solver_state_count:
issues.add("STATE_METADATA_LENGTH", "state_reducer", "Initial state and tolerances must cover every state.")
if any(not isfinite(value) for value in reducer.initial_state):
issues.add("NONFINITE_VALUE", "state_reducer.initial_state", "Initial state must be finite.")
if (
state_buffer is not None
and state_buffer.initial_float_values != reducer.initial_state
):
issues.add(
"STATE_INITIAL_VALUE_MISMATCH",
"state_reducer.initial_state",
"State-input buffer initial values must exactly equal the reducer initial state.",
)
if any(not isfinite(value) or value <= 0.0 for value in reducer.absolute_tolerances):
issues.add("STATE_TOLERANCE_INVALID", "state_reducer.absolute_tolerances", "Tolerances must be finite and positive.")
for index, slot in enumerate(reducer.local_state_slots):
valid_slot(slot, f"state_reducer.local_state_slots[{index}]")
if slot.buffer is not ir.IRBufferKind.LOCAL_STATE:
issues.add("STATE_REDUCER_BUFFER", f"state_reducer.local_state_slots[{index}]", "Local states must use the local_state buffer.")
for index, slot in enumerate(reducer.raw_derivative_slots):
valid_slot(slot, f"state_reducer.raw_derivative_slots[{index}]")
if slot.buffer is not ir.IRBufferKind.LOCAL_DERIVATIVE:
issues.add("STATE_REDUCER_BUFFER", f"state_reducer.raw_derivative_slots[{index}]", "Raw derivatives must use the local_derivative buffer.")
expected_local_state_slots = tuple(
slot for component in program.components for slot in component.state_slots
)
expected_raw_derivative_slots = tuple(
slot for component in program.components for slot in component.derivative_slots
)
if reducer.local_state_slots != expected_local_state_slots:
issues.add(
"STATE_REDUCER_LOCAL_COVERAGE",
"state_reducer.local_state_slots",
"Reducer local-state order must exactly match component state bindings.",
)
if reducer.raw_derivative_slots != expected_raw_derivative_slots:
issues.add(
"STATE_REDUCER_DERIVATIVE_COVERAGE",
"state_reducer.raw_derivative_slots",
"Reducer derivative order must exactly match component derivative bindings.",
)
_validate_csr(reducer.state_scatter.pattern, "state_reducer.state_scatter.pattern", issues)
_validate_csr(reducer.derivative_gather.pattern, "state_reducer.derivative_gather.pattern", issues)
if reducer.state_scatter.pattern.row_count != len(reducer.local_state_slots) or reducer.state_scatter.pattern.column_count != reducer.solver_state_count:
issues.add("STATE_SCATTER_SHAPE", "state_reducer.state_scatter", "Scatter shape must be local_state_count x solver_state_count.")
if reducer.derivative_gather.pattern.row_count != reducer.solver_state_count or reducer.derivative_gather.pattern.column_count != len(reducer.raw_derivative_slots):
issues.add("DERIVATIVE_GATHER_SHAPE", "state_reducer.derivative_gather", "Gather shape must be solver_state_count x raw_derivative_count.")
if len(reducer.state_scatter.values) != reducer.state_scatter.pattern.nonzero_count:
issues.add("CSR_VALUE_COUNT", "state_reducer.state_scatter.values", "Value count differs from pattern.")
if len(reducer.derivative_gather.values) != reducer.derivative_gather.pattern.nonzero_count:
issues.add("CSR_VALUE_COUNT", "state_reducer.derivative_gather.values", "Value count differs from pattern.")
if any(not isfinite(value) for value in (*reducer.state_scatter.values, *reducer.derivative_gather.values)):
issues.add("NONFINITE_VALUE", "state_reducer", "Reducer matrix values must be finite.")
if (
reducer.state_scatter.pattern.row_pointers
and any(
first == second
for first, second in zip(
reducer.state_scatter.pattern.row_pointers,
reducer.state_scatter.pattern.row_pointers[1:],
)
)
):
issues.add("STATE_SCATTER_EMPTY_ROW", "state_reducer.state_scatter", "Every local state must depend on at least one solver state.")
if set(reducer.state_scatter.pattern.column_indices) != set(
range(reducer.solver_state_count)
):
issues.add("STATE_SCATTER_COLUMN_COVERAGE", "state_reducer.state_scatter", "Every solver state must feed at least one local state.")
if set(reducer.derivative_gather.pattern.column_indices) != set(
range(len(reducer.raw_derivative_slots))
):
issues.add("DERIVATIVE_GATHER_COLUMN_COVERAGE", "state_reducer.derivative_gather", "Every raw derivative must contribute to a solver derivative.")
if (
reducer.derivative_gather.pattern.row_pointers
and any(
first == second
for first, second in zip(
reducer.derivative_gather.pattern.row_pointers,
reducer.derivative_gather.pattern.row_pointers[1:],
)
)
):
issues.add("DERIVATIVE_GATHER_EMPTY_ROW", "state_reducer.derivative_gather", "Every solver state must receive at least one derivative contribution.")
pressure_flow = program.pressure_flow
issues.unique_ids(pressure_flow.unknowns, "unknown_id", "pressure_flow.unknowns")
issues.unique_ids(pressure_flow.equations, "equation_id", "pressure_flow.equations")
issues.unique_ids(pressure_flow.blocks, "block_id", "pressure_flow.blocks")
issues.unique_ids(pressure_flow.scopes, "scope_id", "pressure_flow.scopes")
for index, unknown in enumerate(pressure_flow.unknowns):
path = f"pressure_flow.unknowns[{index}]"
valid_slot(unknown.slot, f"{path}.slot")
if unknown.slot.buffer is not ir.IRBufferKind.ALGEBRAIC:
issues.add("PRESSURE_FLOW_UNKNOWN_BUFFER", f"{path}.slot", "Pressure-flow unknowns must use the algebraic buffer.")
if not 0 <= unknown.component_index < len(program.components) or not 0 <= unknown.port_index < len(program.ports):
issues.add("PRESSURE_FLOW_UNKNOWN_OWNER", path, "Unknown owner is invalid.")
else:
port = program.ports[unknown.port_index]
matching_variables = tuple(
variable
for variable in port.variables
if variable.name == unknown.variable
)
if (
port.component_index != unknown.component_index
or len(matching_variables) != 1
or matching_variables[0].variable_id != unknown.unknown_id
or matching_variables[0].role is not unknown.role
or matching_variables[0].slot != unknown.slot
):
issues.add(
"PRESSURE_FLOW_UNKNOWN_OWNER",
path,
"Unknown must match one variable on a port owned by its component.",
)
if not isfinite(unknown.scale) or unknown.scale <= 0.0:
issues.add("VALUE_SCALE_INVALID", f"{path}.scale", "Scale must be finite and positive.")
if not _finite(unknown.lower_bound) or not _finite(unknown.upper_bound):
issues.add("NONFINITE_VALUE", path, "Unknown bounds must be finite or null.")
if unknown.lower_bound is not None and unknown.upper_bound is not None and unknown.lower_bound > unknown.upper_bound:
issues.add("VALUE_BOUNDS_ORDER", path, "Unknown lower bound exceeds upper bound.")
residual_slots: set[ir.IRSlotRef] = set()
for index, equation in enumerate(pressure_flow.equations):
path = f"pressure_flow.equations[{index}]"
valid_slot(equation.residual_slot, f"{path}.residual_slot")
if equation.residual_slot in residual_slots:
issues.add(
"PRESSURE_FLOW_RESIDUAL_SLOT_DUPLICATE",
f"{path}.residual_slot",
"Every pressure-flow equation requires a unique residual slot.",
)
residual_slots.add(equation.residual_slot)
if equation.residual_slot.buffer is not ir.IRBufferKind.WORK_FLOAT:
issues.add("EQUATION_RESIDUAL_BUFFER", f"{path}.residual_slot", "Equation residuals must use the float workspace.")
for slot_index, slot in enumerate(equation.variable_slots):
valid_slot(slot, f"{path}.variable_slots[{slot_index}]")
if slot.buffer not in {
ir.IRBufferKind.ALGEBRAIC,
ir.IRBufferKind.LOCAL_STATE,
ir.IRBufferKind.SIGNAL,
ir.IRBufferKind.PARAMETER,
ir.IRBufferKind.CONSTANT,
ir.IRBufferKind.MODE,
ir.IRBufferKind.RUNTIME_INPUT,
}:
issues.add(
"EQUATION_VARIABLE_BUFFER",
f"{path}.variable_slots[{slot_index}]",
"Equation variables must be model values, not time or result buffers.",
)
if len(set(equation.variable_slots)) != len(equation.variable_slots):
issues.add("EQUATION_VARIABLE_DUPLICATE", f"{path}.variable_slots", "Equation variables must be unique.")
owner_count = len(program.components) if equation.owner is ir.IREquationOwner.COMPONENT else len(program.connections)
if not 0 <= equation.owner_index < owner_count:
issues.add("EQUATION_OWNER_BOUNDS", path, "Equation owner index is invalid.")
if not isfinite(equation.scale) or equation.scale <= 0.0:
issues.add("VALUE_SCALE_INVALID", f"{path}.scale", "Equation scale must be finite and positive.")
for index, block in enumerate(pressure_flow.blocks):
path = f"pressure_flow.blocks[{index}]"
if len(set(block.unknown_indices)) != len(block.unknown_indices):
issues.add("ALGEBRAIC_BLOCK_DUPLICATE", f"{path}.unknown_indices", "Block unknowns must be unique.")
if len(set(block.equation_indices)) != len(block.equation_indices):
issues.add("ALGEBRAIC_BLOCK_DUPLICATE", f"{path}.equation_indices", "Block equations must be unique.")
if any(item < 0 or item >= len(pressure_flow.unknowns) for item in block.unknown_indices):
issues.add("UNKNOWN_INDEX_BOUNDS", path, "Block unknown index is invalid.")
if any(item < 0 or item >= len(pressure_flow.equations) for item in block.equation_indices):
issues.add("EQUATION_INDEX_BOUNDS", path, "Block equation index is invalid.")
_validate_csr(block.jacobian_pattern, f"{path}.jacobian_pattern", issues)
if block.jacobian_pattern.row_count != len(block.equation_indices) or block.jacobian_pattern.column_count != len(block.unknown_indices):
issues.add("ALGEBRAIC_BLOCK_SHAPE", path, "Block Jacobian shape differs from its rows/columns.")
block_unknowns = tuple(
item for block in pressure_flow.blocks for item in block.unknown_indices
)
block_equations = tuple(
item for block in pressure_flow.blocks for item in block.equation_indices
)
if (
tuple(sorted(block_unknowns)) != tuple(range(len(pressure_flow.unknowns)))
or len(set(block_unknowns)) != len(block_unknowns)
):
issues.add("ALGEBRAIC_BLOCK_UNKNOWN_PARTITION", "pressure_flow.blocks", "Blocks must partition all pressure-flow unknowns.")
if (
tuple(sorted(block_equations)) != tuple(range(len(pressure_flow.equations)))
or len(set(block_equations)) != len(block_equations)
):
issues.add("ALGEBRAIC_BLOCK_EQUATION_PARTITION", "pressure_flow.blocks", "Blocks must partition all pressure-flow equations.")
for index, scope in enumerate(pressure_flow.scopes):
path = f"pressure_flow.scopes[{index}]"
bounds = (
(scope.component_indices, len(program.components), "component"),
(scope.unknown_indices, len(pressure_flow.unknowns), "unknown"),
(scope.equation_indices, len(pressure_flow.equations), "equation"),
(scope.block_indices, len(pressure_flow.blocks), "block"),
)
for indexes, upper, label in bounds:
if any(item < 0 or item >= upper for item in indexes):
issues.add(f"{label.upper()}_INDEX_BOUNDS", path, f"Scope {label} index is invalid.")
if len(set(indexes)) != len(indexes):
issues.add(f"{label.upper()}_INDEX_DUPLICATE", path, f"Scope {label} indices must be unique.")
if scope.causal_plan_index is not None and not 0 <= scope.causal_plan_index < len(program.causal_plans):
issues.add("CAUSAL_PLAN_INDEX_BOUNDS", path, "Causal plan index is invalid.")
if not isfinite(scope.residual_tolerance) or scope.residual_tolerance <= 0.0 or scope.max_evaluations <= 0:
issues.add("PRESSURE_FLOW_LIMIT_INVALID", path, "Solver limits must be positive and finite.")
if scope.sparse_pattern_trusted == (scope.sparse_fallback_reason is not None):
issues.add(
"PRESSURE_FLOW_SPARSE_STATUS",
path,
"A trusted sparse pattern must have no fallback reason; an untrusted one must explain its fallback.",
)
if all(0 <= item < len(pressure_flow.blocks) for item in scope.block_indices):
scoped_unknowns = {
item
for block_index in scope.block_indices
for item in pressure_flow.blocks[block_index].unknown_indices
}
scoped_equations = {
item
for block_index in scope.block_indices
for item in pressure_flow.blocks[block_index].equation_indices
}
if scoped_unknowns != set(scope.unknown_indices):
issues.add("PRESSURE_FLOW_SCOPE_UNKNOWN_COVERAGE", path, "Scope unknowns must equal its blocks' unknowns.")
if scoped_equations != set(scope.equation_indices):
issues.add("PRESSURE_FLOW_SCOPE_EQUATION_COVERAGE", path, "Scope equations must equal its blocks' equations.")
if not pressure_flow.scopes or not 0 <= pressure_flow.global_scope_index < len(pressure_flow.scopes):
issues.add("GLOBAL_SCOPE_INDEX_BOUNDS", "pressure_flow.global_scope_index", "Global scope is invalid.")
else:
global_scope = pressure_flow.scopes[pressure_flow.global_scope_index]
if (
global_scope.kind is not ir.IRPressureFlowScopeKind.NETWORK
or set(global_scope.component_indices) != set(range(len(program.components)))
or set(global_scope.unknown_indices) != set(range(len(pressure_flow.unknowns)))
or set(global_scope.equation_indices) != set(range(len(pressure_flow.equations)))
or set(global_scope.block_indices) != set(range(len(pressure_flow.blocks)))
):
issues.add("GLOBAL_SCOPE_COVERAGE", "pressure_flow.global_scope_index", "Global scope must cover the complete network.")
if any(index < 0 or index >= len(pressure_flow.scopes) for index in pressure_flow.secondary_scope_indices):
issues.add("SECONDARY_SCOPE_INDEX_BOUNDS", "pressure_flow.secondary_scope_indices", "Secondary scope is invalid.")
if (
len(set(pressure_flow.secondary_scope_indices))
!= len(pressure_flow.secondary_scope_indices)
or pressure_flow.global_scope_index in pressure_flow.secondary_scope_indices
):
issues.add("SECONDARY_SCOPE_INDEX_INVALID", "pressure_flow.secondary_scope_indices", "Secondary scopes must be unique and exclude the global scope.")
if not isfinite(pressure_flow.pressure_lower_bound):
issues.add("NONFINITE_VALUE", "pressure_flow.pressure_lower_bound", "Pressure bound must be finite.")
issues.unique_ids(program.stages, "stage_id", "stages")
for stage_index, stage in enumerate(program.stages):
path = f"stages[{stage_index}]"
reads: list[ir.IRSlotRef] = []
writes: list[ir.IRSlotRef] = []
for operation_index, operation in enumerate(stage.operations):
operation_path = f"{path}.operations[{operation_index}]"
for slot_index, slot in enumerate(_operation_slots(operation)):
valid_slot(slot, f"{operation_path}.slots[{slot_index}]")
reads.extend(ir.operation_read_slots(operation))
writes.extend(ir.operation_write_slots(operation))
if isinstance(operation, ir.IRLinearCombinationOperation):
if len(operation.source_slots) != len(operation.weights) or any(not isfinite(value) for value in (*operation.weights, operation.bias)):
issues.add("LINEAR_COMBINATION_INVALID", operation_path, "Weights must be finite and match sources.")
if isinstance(operation, ir.IRKernelCallOperation):
if len(set(operation.read_slots)) != len(operation.read_slots):
issues.add("KERNEL_CALL_READ_DUPLICATE", operation_path, "Kernel read slots must be unique.")
if len(set(operation.write_slots)) != len(operation.write_slots):
issues.add("KERNEL_CALL_WRITE_DUPLICATE", operation_path, "Kernel write slots must be unique.")
if not 0 <= operation.kernel_index < len(program.kernels):
issues.add("KERNEL_INDEX_BOUNDS", operation_path, "Operation kernel index is invalid.")
else:
phases = {phase.phase for phase in program.kernels[operation.kernel_index].phases}
if operation.phase not in phases:
issues.add("KERNEL_PHASE_UNDECLARED", operation_path, "Operation uses an undeclared kernel phase.")
if operation.component_index is not None and not 0 <= operation.component_index < len(program.components):
issues.add("COMPONENT_INDEX_BOUNDS", operation_path, "Operation component index is invalid.")
elif (
operation.component_index is not None
and 0 <= operation.kernel_index < len(program.kernels)
and program.components[operation.component_index].kernel_index
!= operation.kernel_index
):
issues.add(
"KERNEL_COMPONENT_MISMATCH",
operation_path,
"Component-bound operation must use the component's declared kernel.",
)
if any(item < 0 or item >= len(pressure_flow.equations) for item in operation.equation_indices):
issues.add("EQUATION_INDEX_BOUNDS", operation_path, "Operation equation index is invalid.")
if len(set(operation.equation_indices)) != len(operation.equation_indices):
issues.add("EQUATION_INDEX_DUPLICATE", operation_path, "Operation equation indices must be unique.")
if operation.equation_indices and operation.phase is not ir.IRKernelPhase.RESIDUAL:
issues.add("KERNEL_EQUATION_PHASE_INVALID", operation_path, "Equation indices are only valid for residual calls.")
if (
operation.phase is ir.IRKernelPhase.RESIDUAL
and all(
0 <= item < len(pressure_flow.equations)
for item in operation.equation_indices
)
):
required_reads = {
slot
for equation_index in operation.equation_indices
for slot in pressure_flow.equations[
equation_index
].variable_slots
}
required_writes = {
pressure_flow.equations[equation_index].residual_slot
for equation_index in operation.equation_indices
}
if not required_reads.issubset(operation.read_slots):
issues.add("RESIDUAL_CALL_READ_COVERAGE", operation_path, "Residual call reads must cover every referenced equation variable.")
if not required_writes.issubset(operation.write_slots):
issues.add("RESIDUAL_CALL_WRITE_COVERAGE", operation_path, "Residual call writes must cover every referenced equation residual.")
if isinstance(operation, (ir.IREffortBroadcastOperation, ir.IRFlowAssignmentOperation)):
if operation.equation_id not in {equation.equation_id for equation in pressure_flow.equations}:
issues.add("EQUATION_ID_MISSING", operation_path, "Assignment references an unknown equation.")
if len(set(stage.declared_read_slots)) != len(stage.declared_read_slots) or set(stage.declared_read_slots) != set(reads):
issues.add("STAGE_READ_SET_MISMATCH", f"{path}.declared_read_slots", "Declared reads must equal the operation read union.")
if len(set(stage.declared_write_slots)) != len(stage.declared_write_slots) or set(stage.declared_write_slots) != set(writes):
issues.add("STAGE_WRITE_SET_MISMATCH", f"{path}.declared_write_slots", "Declared writes must equal the operation write union.")
def valid_step(step: ir.IRStepRef, path: str) -> bool:
upper = len(program.stages) if step.kind is ir.IRStepKind.STAGE else len(program.blocks)
if step.index < 0 or step.index >= upper:
issues.add("STEP_INDEX_BOUNDS", path, f"{step.kind.value} index is invalid.")
return False
return True
issues.unique_ids(program.blocks, "block_id", "blocks")
for block_index, block in enumerate(program.blocks):
path = f"blocks[{block_index}]"
for step_index, step in enumerate(block.steps):
valid_step(step, f"{path}.steps[{step_index}]")
iterative_kinds = {ir.IRBlockKind.FIXED_POINT, ir.IRBlockKind.STREAM_SCC}
if block.kind in iterative_kinds and block.convergence is None:
issues.add("FIXED_POINT_CONVERGENCE_MISSING", path, "Iterative block needs convergence settings.")
if block.kind not in iterative_kinds and block.convergence is not None:
issues.add("UNEXPECTED_CONVERGENCE", path, "Only iterative blocks may define convergence.")
if block.convergence is not None:
convergence = block.convergence
if (
not isfinite(convergence.absolute_tolerance)
or convergence.absolute_tolerance < 0.0
or not isfinite(convergence.relative_tolerance)
or convergence.relative_tolerance < 0.0
or convergence.max_iterations <= 0
or not isfinite(convergence.relaxation)
or not 0.0 < convergence.relaxation <= 1.0
):
issues.add("CONVERGENCE_LIMIT_INVALID", f"{path}.convergence", "Convergence limits are invalid.")
for slot_index, slot in enumerate((*convergence.monitor_slots, *convergence.rollback_slots)):
valid_slot(slot, f"{path}.convergence.slots[{slot_index}]")
visit_state = [0] * len(program.blocks)
def visit_block(block_index: int) -> None:
if visit_state[block_index] == 1:
issues.add("BLOCK_REFERENCE_CYCLE", f"blocks[{block_index}]", "Execution blocks must be acyclic.")
return
if visit_state[block_index] == 2:
return
visit_state[block_index] = 1
for step in program.blocks[block_index].steps:
if step.kind is ir.IRStepKind.BLOCK and 0 <= step.index < len(program.blocks):
visit_block(step.index)
visit_state[block_index] = 2
for block_index in range(len(program.blocks)):
visit_block(block_index)
def expanded_stage_indices(
steps: tuple[ir.IRStepRef, ...],
) -> tuple[int, ...]:
result: list[int] = []
def append_step(step: ir.IRStepRef, active_blocks: set[int]) -> None:
if step.kind is ir.IRStepKind.STAGE:
if 0 <= step.index < len(program.stages):
result.append(step.index)
return
if not 0 <= step.index < len(program.blocks) or step.index in active_blocks:
return
nested_active = {*active_blocks, step.index}
for nested in program.blocks[step.index].steps:
append_step(nested, nested_active)
for step in steps:
append_step(step, set())
return tuple(result)
expected_entry_inputs = tuple(
ir.IRSlotRef(kind, index)
for kind in (
ir.IRBufferKind.TIME,
ir.IRBufferKind.STATE_INPUT,
ir.IRBufferKind.RUNTIME_INPUT,
)
for buffer in (buffer_by_kind.get(kind),)
if buffer is not None
for index in range(buffer.size)
)
entry_required_stage = {
ir.IREntryPointKind.RHS: ir.IRStageKind.DERIVATIVE_REDUCE,
ir.IREntryPointKind.EVENTS: ir.IRStageKind.EVENT,
ir.IREntryPointKind.JACOBIAN: ir.IRStageKind.JACOBIAN,
ir.IREntryPointKind.OUTPUTS: ir.IRStageKind.OUTPUT,
}
entry_forbidden_stages = {
ir.IREntryPointKind.RHS: {
ir.IRStageKind.EVENT,
ir.IRStageKind.JACOBIAN,
ir.IRStageKind.OUTPUT,
ir.IRStageKind.RESET,
},
ir.IREntryPointKind.EVENTS: {
ir.IRStageKind.DERIVATIVE_REDUCE,
ir.IRStageKind.JACOBIAN,
ir.IRStageKind.OUTPUT,
ir.IRStageKind.RESET,
},
ir.IREntryPointKind.JACOBIAN: {
ir.IRStageKind.EVENT,
ir.IRStageKind.OUTPUT,
ir.IRStageKind.RESET,
},
ir.IREntryPointKind.OUTPUTS: {
ir.IRStageKind.EVENT,
ir.IRStageKind.JACOBIAN,
ir.IRStageKind.RESET,
},
}
entry_kinds = tuple(entry.kind for entry in program.entry_points)
expected_entry_kinds = set(ir.IREntryPointKind)
if len(entry_kinds) != len(expected_entry_kinds) or set(entry_kinds) != expected_entry_kinds:
issues.add("ENTRY_POINT_SET_INVALID", "entry_points", "Exactly rhs, events, jacobian and outputs are required.")
for entry_index, entry in enumerate(program.entry_points):
path = f"entry_points[{entry_index}]"
for step_index, step in enumerate(entry.steps):
valid_step(step, f"{path}.steps[{step_index}]")
for slot_index, slot in enumerate((*entry.input_slots, *entry.output_slots)):
valid_slot(slot, f"{path}.slots[{slot_index}]")
output_kinds = {slot.buffer for slot in entry.output_slots}
expected_output_kind = {
ir.IREntryPointKind.RHS: ir.IRBufferKind.DERIVATIVE_OUTPUT,
ir.IREntryPointKind.EVENTS: ir.IRBufferKind.EVENT_OUTPUT,
ir.IREntryPointKind.JACOBIAN: ir.IRBufferKind.JACOBIAN_VALUE,
ir.IREntryPointKind.OUTPUTS: ir.IRBufferKind.RESULT_OUTPUT,
}[entry.kind]
if output_kinds - {expected_output_kind}:
issues.add("ENTRY_POINT_OUTPUT_BUFFER", path, f"Outputs must use {expected_output_kind.value}.")
if entry.input_slots != expected_entry_inputs:
issues.add(
"ENTRY_POINT_INPUT_CONTRACT",
f"{path}.input_slots",
"Entry inputs must be ordered time, full solver state and runtime inputs.",
)
expected_outputs = {
ir.IREntryPointKind.RHS: tuple(
ir.IRSlotRef(ir.IRBufferKind.DERIVATIVE_OUTPUT, index)
for index in range(buffer_by_kind.get(ir.IRBufferKind.DERIVATIVE_OUTPUT).size)
)
if buffer_by_kind.get(ir.IRBufferKind.DERIVATIVE_OUTPUT) is not None
else (),
ir.IREntryPointKind.EVENTS: tuple(event.root_slot for event in program.events),
ir.IREntryPointKind.JACOBIAN: program.jacobian.value_slots,
ir.IREntryPointKind.OUTPUTS: tuple(output.output_slot for output in program.outputs),
}[entry.kind]
if entry.output_slots != expected_outputs:
issues.add(
"ENTRY_POINT_OUTPUT_COVERAGE",
f"{path}.output_slots",
"Entry outputs must exactly match its ordered public result contract.",
)
reachable_stage_indices = expanded_stage_indices(entry.steps)
reachable_kinds = {
program.stages[index].kind for index in reachable_stage_indices
}
if entry_required_stage[entry.kind] not in reachable_kinds:
issues.add(
"ENTRY_POINT_FINAL_STAGE_MISSING",
f"{path}.steps",
f"{entry.kind.value} must execute a {entry_required_stage[entry.kind].value} stage.",
)
forbidden = reachable_kinds & entry_forbidden_stages[entry.kind]
if forbidden:
issues.add(
"ENTRY_POINT_STAGE_FORBIDDEN",
f"{path}.steps",
"Entry executes incompatible stage kinds: "
+ ", ".join(sorted(kind.value for kind in forbidden)),
)
reachable_writes = {
slot
for index in reachable_stage_indices
for slot in program.stages[index].declared_write_slots
}
if not set(entry.output_slots).issubset(reachable_writes):
issues.add(
"ENTRY_POINT_OUTPUT_NOT_WRITTEN",
f"{path}.steps",
"Entry steps do not write every declared output slot.",
)
forbidden_write_buffers = {
ir.IRBufferKind.STATE_INPUT,
ir.IRBufferKind.PARAMETER,
ir.IRBufferKind.CONSTANT,
ir.IRBufferKind.MODE,
ir.IRBufferKind.RUNTIME_INPUT,
}
if any(slot.buffer in forbidden_write_buffers for slot in reachable_writes):
issues.add(
"ENTRY_POINT_PERSISTENT_WRITE",
f"{path}.steps",
"Evaluation entries may not mutate state, parameters, constants, modes or runtime inputs.",
)
issues.unique_ids(program.causal_plans, "plan_id", "causal_plans")
for index, plan in enumerate(program.causal_plans):
path = f"causal_plans[{index}]"
if plan.source_schema_version != 1:
issues.add("CAUSAL_SOURCE_VERSION_INVALID", path, "Only causal IR v1 metadata is supported.")
if plan.source_structural_signature is not None and (
len(plan.source_structural_signature) != 64
or any(character not in "0123456789abcdef" for character in plan.source_structural_signature)
):
issues.add("CAUSAL_SIGNATURE_INVALID", path, "Source signature must be lowercase SHA-256.")
if any(item < 0 or item >= len(program.components) for item in plan.scope_component_indices):
issues.add("COMPONENT_INDEX_BOUNDS", path, "Causal scope component index is invalid.")
for slot_index, slot in enumerate((*plan.canonical_slots, *plan.compatibility_slots, *plan.reset_slots, *plan.external_effort_slots)):
valid_slot(slot, f"{path}.slots[{slot_index}]")
for stage in plan.effort_stages:
if stage.stage_index < 0 or stage.stage_index >= len(program.stages):
issues.add("STAGE_INDEX_BOUNDS", path, "Causal effort stage index is invalid.")
if any(stage < 0 or stage >= len(program.stages) for stage in plan.flow_stage_indices):
issues.add("STAGE_INDEX_BOUNDS", path, "Causal flow stage index is invalid.")
issues.unique_ids(program.stream_plans, "plan_id", "stream_plans")
for plan_index, plan in enumerate(program.stream_plans):
path = f"stream_plans[{plan_index}]"
scc_ids: set[str] = set()
for slot_index, slot in enumerate(plan.node_slots):
valid_slot(slot, f"{path}.node_slots[{slot_index}]")
partition = tuple(slot for component in plan.strongly_connected_components for slot in component.node_slots)
if len(set(partition)) != len(partition) or set(partition) != set(plan.node_slots):
issues.add("STREAM_SCC_PARTITION", path, "SCCs must partition stream nodes exactly once.")
for scc_index, scc in enumerate(plan.strongly_connected_components):
if not scc.scc_id or scc.scc_id in scc_ids:
issues.add("STREAM_SCC_ID_INVALID", f"{path}.strongly_connected_components[{scc_index}]", "SCC IDs must be non-empty and unique.")
scc_ids.add(scc.scc_id)
if scc.block_index < 0 or scc.block_index >= len(program.blocks):
issues.add("BLOCK_INDEX_BOUNDS", f"{path}.strongly_connected_components[{scc_index}]", "SCC block index is invalid.")
elif program.blocks[scc.block_index].kind is not ir.IRBlockKind.STREAM_SCC:
issues.add("STREAM_SCC_BLOCK_KIND", f"{path}.strongly_connected_components[{scc_index}]", "SCCs must reference stream_scc blocks.")
elif (
program.blocks[scc.block_index].convergence is None
or set(program.blocks[scc.block_index].convergence.monitor_slots)
!= set(scc.node_slots)
):
issues.add("STREAM_SCC_MONITOR_COVERAGE", f"{path}.strongly_connected_components[{scc_index}]", "SCC convergence monitors must match its node slots.")
scc_count = len(plan.strongly_connected_components)
if tuple(sorted(plan.topological_scc_indices)) != tuple(range(scc_count)):
issues.add("STREAM_TOPOLOGICAL_ORDER", path, "Topological order must cover each SCC once.")
position = {scc: index for index, scc in enumerate(plan.topological_scc_indices)}
for edge_index, edge in enumerate(plan.condensed_edges):
if not 0 <= edge.source_scc_index < scc_count or not 0 <= edge.target_scc_index < scc_count:
issues.add("STREAM_EDGE_BOUNDS", f"{path}.condensed_edges[{edge_index}]", "SCC edge is invalid.")
elif position.get(edge.source_scc_index, -1) >= position.get(edge.target_scc_index, -1):
issues.add("STREAM_DAG_CYCLE", f"{path}.condensed_edges[{edge_index}]", "Edge contradicts topological order.")
thermofluid = program.thermofluid
if not 0 <= thermofluid.stream_plan_index < len(program.stream_plans):
issues.add("STREAM_PLAN_INDEX_BOUNDS", "thermofluid.stream_plan_index", "Stream plan is invalid.")
for indexes, upper, label in (
(thermofluid.physical_port_indices, len(program.ports), "port"),
(thermofluid.global_component_indices, len(program.components), "component"),
(thermofluid.sensitive_component_indices, len(program.components), "component"),
(thermofluid.secondary_pressure_scope_indices, len(pressure_flow.scopes), "scope"),
):
if any(item < 0 or item >= upper for item in indexes):
issues.add(f"{label.upper()}_INDEX_BOUNDS", "thermofluid", f"Thermofluid {label} index is invalid.")
if len(set(indexes)) != len(indexes):
issues.add(f"{label.upper()}_INDEX_DUPLICATE", "thermofluid", f"Thermofluid {label} indices must be unique.")
expected_physical_ports = {
index for index, port in enumerate(program.ports) if port.kind is ir.IRPortKind.PHYSICAL
}
if set(thermofluid.physical_port_indices) != expected_physical_ports:
issues.add("THERMOFLUID_PORT_COVERAGE", "thermofluid.physical_port_indices", "Thermofluid plan must cover every physical port.")
if thermofluid.secondary_pressure_scope_indices != pressure_flow.secondary_scope_indices:
issues.add("THERMOFLUID_SCOPE_MISMATCH", "thermofluid.secondary_pressure_scope_indices", "Thermofluid and pressure-flow secondary scopes must match.")
if not set(thermofluid.sensitive_component_indices).issubset(
thermofluid.global_component_indices
):
issues.add("THERMOFLUID_SENSITIVE_SCOPE", "thermofluid.sensitive_component_indices", "Sensitive components must belong to the global closure component set.")
if thermofluid.maximum_iterations <= 0 or not isfinite(thermofluid.flow_relative_tolerance) or thermofluid.flow_relative_tolerance <= 0.0:
issues.add("THERMOFLUID_LIMIT_INVALID", "thermofluid", "Fixed-point limits must be positive.")
if thermofluid.uses_conservative_global_solver != (
thermofluid.conservative_fallback_reason is not None
):
issues.add("THERMOFLUID_FALLBACK_REASON", "thermofluid", "Conservative fallback flag and reason must be declared together.")
transaction = program.transaction
for slot_index, slot in enumerate((*transaction.snapshot_slots, *transaction.flow_slots)):
valid_slot(slot, f"transaction.slots[{slot_index}]")
if len(set(transaction.snapshot_slots)) != len(transaction.snapshot_slots):
issues.add("TRANSACTION_DUPLICATE_SLOT", "transaction.snapshot_slots", "Snapshot slots must be unique.")
if len(set(transaction.flow_slots)) != len(transaction.flow_slots):
issues.add("TRANSACTION_DUPLICATE_SLOT", "transaction.flow_slots", "Flow slots must be unique.")
if not set(transaction.flow_slots).issubset(set(transaction.snapshot_slots)):
issues.add("TRANSACTION_FLOW_COVERAGE", "transaction.flow_slots", "Flow slots must be part of the snapshot.")
if any(item < 0 or item >= len(program.components) for item in transaction.cache_component_indices):
issues.add("COMPONENT_INDEX_BOUNDS", "transaction.cache_component_indices", "Cache owner is invalid.")
if len(set(transaction.cache_component_indices)) != len(transaction.cache_component_indices):
issues.add("TRANSACTION_CACHE_OWNER_DUPLICATE", "transaction.cache_component_indices", "Cache owners must be unique.")
if len(set(transaction.cache_attribute_ids)) != len(transaction.cache_attribute_ids):
issues.add("TRANSACTION_CACHE_ID_DUPLICATE", "transaction.cache_attribute_ids", "Cache attribute IDs must be unique.")
if any(not value for value in transaction.cache_attribute_ids):
issues.add("TRANSACTION_CACHE_ID_INVALID", "transaction.cache_attribute_ids", "Cache attribute IDs must not be empty.")
if len(set(transaction.diagnostic_owner_ids)) != len(transaction.diagnostic_owner_ids) or any(
not value for value in transaction.diagnostic_owner_ids
):
issues.add("TRANSACTION_DIAGNOSTIC_ID_INVALID", "transaction.diagnostic_owner_ids", "Diagnostic owner IDs must be non-empty and unique.")
if "transactional_closure" in program.required_features and not (
transaction.restores_on_recoverable_failure
and transaction.restores_on_fatal_failure
):
issues.add("TRANSACTION_RESTORE_REQUIRED", "transaction", "Transactional closure requires rollback for every failed trial.")
for block_index, block in enumerate(program.blocks):
if block.convergence is not None and not set(
block.convergence.rollback_slots
).issubset(transaction.snapshot_slots):
issues.add("TRANSACTION_ROLLBACK_COVERAGE", f"blocks[{block_index}].convergence.rollback_slots", "Iterative rollback slots must belong to the transaction snapshot.")
expected_flow_slots = {
variable.slot
for port in program.ports
if port.kind is ir.IRPortKind.PHYSICAL and port.domain == "pneumatic"
for variable in port.variables
if variable.name == "m_flow"
}
if set(transaction.flow_slots) != expected_flow_slots:
issues.add("TRANSACTION_FLOW_SEMANTICS", "transaction.flow_slots", "Transaction flow slots must exactly cover active pneumatic m_flow variables.")
expected_snapshot_slots = {
variable.slot
for port in program.ports
if port.kind is ir.IRPortKind.PHYSICAL
for variable in port.variables
}
if set(transaction.snapshot_slots) != expected_snapshot_slots:
issues.add("TRANSACTION_SNAPSHOT_COVERAGE", "transaction.snapshot_slots", "Transaction snapshots must exactly cover mutable physical port values.")
issues.unique_ids(program.modes, "mode_id", "modes")
mode_by_slot: dict[ir.IRSlotRef, ir.IRModeSpec] = {}
for index, mode in enumerate(program.modes):
path = f"modes[{index}]"
valid_slot(mode.slot, f"{path}.slot")
if mode.slot.buffer is not ir.IRBufferKind.MODE:
issues.add("MODE_BUFFER_INVALID", f"{path}.slot", "Mode must use the mode buffer.")
if mode.slot in mode_by_slot:
issues.add("MODE_SLOT_DUPLICATE", f"{path}.slot", "Each mode requires a distinct slot.")
mode_by_slot[mode.slot] = mode
values = tuple(item.value for item in mode.values)
names = tuple(item.name for item in mode.values)
if any(value < -(2**31) or value > 2**31 - 1 for value in (*values, mode.initial_value)):
issues.add("MODE_INT32_RANGE", path, "Mode values and initial value must fit signed int32.")
if len(set(values)) != len(values) or len(set(names)) != len(names) or mode.initial_value not in values:
issues.add("MODE_VALUES_INVALID", path, "Mode values/names must be unique and include the initial value.")
if any(item < 0 or item >= len(program.components) for item in mode.owner_component_indices):
issues.add("COMPONENT_INDEX_BOUNDS", path, "Mode owner is invalid.")
if len(set(mode.owner_component_indices)) != len(mode.owner_component_indices):
issues.add("MODE_OWNER_DUPLICATE", path, "Mode owners must be unique.")
mode_buffer = buffer_by_kind.get(ir.IRBufferKind.MODE)
if mode_buffer is not None and set(mode_by_slot) != {
ir.IRSlotRef(ir.IRBufferKind.MODE, index)
for index in range(mode_buffer.size)
}:
issues.add("MODE_SLOT_COVERAGE", "modes", "Modes must cover every mode buffer slot exactly once.")
if mode_buffer is not None and mode_buffer.initial_int_values:
for mode in program.modes:
if (
0 <= mode.slot.index < len(mode_buffer.initial_int_values)
and mode_buffer.initial_int_values[mode.slot.index] != mode.initial_value
):
issues.add("MODE_INITIAL_VALUE_MISMATCH", mode.mode_id, "Mode metadata and buffer initial value differ.")
for component_index, component in enumerate(program.components):
expected_mode_slots = {
mode.slot
for mode in program.modes
if component_index in mode.owner_component_indices
}
if set(component.mode_slots) != expected_mode_slots:
issues.add("COMPONENT_MODE_COVERAGE", f"components[{component_index}].mode_slots", "Component mode bindings must match mode ownership.")
jacobian = program.jacobian
_validate_csr(jacobian.pattern, "jacobian.pattern", issues)
if jacobian.pattern.row_count != reducer.solver_state_count or jacobian.pattern.column_count != reducer.solver_state_count:
issues.add("JACOBIAN_SHAPE", "jacobian.pattern", "Jacobian must be state_count square.")
if len(jacobian.value_slots) != jacobian.pattern.nonzero_count:
issues.add("JACOBIAN_VALUE_COUNT", "jacobian.value_slots", "One value slot is required per nonzero.")
for slot_index, slot in enumerate(jacobian.value_slots):
valid_slot(slot, f"jacobian.value_slots[{slot_index}]")
if slot.buffer is not ir.IRBufferKind.JACOBIAN_VALUE:
issues.add("JACOBIAN_VALUE_BUFFER", f"jacobian.value_slots[{slot_index}]", "Jacobian values need the Jacobian buffer.")
jacobian_buffer = buffer_by_kind.get(ir.IRBufferKind.JACOBIAN_VALUE)
expected_jacobian_slots = tuple(
ir.IRSlotRef(ir.IRBufferKind.JACOBIAN_VALUE, index)
for index in range(jacobian_buffer.size if jacobian_buffer is not None else 0)
)
if jacobian.value_slots != expected_jacobian_slots:
issues.add("JACOBIAN_VALUE_SLOT_COVERAGE", "jacobian.value_slots", "Jacobian value slots must be unique and cover their buffer in CSR order.")
colors = tuple(column for group in jacobian.color_groups for column in group)
if tuple(sorted(colors)) != tuple(range(reducer.solver_state_count)) or len(set(colors)) != len(colors):
issues.add("JACOBIAN_COLOR_PARTITION", "jacobian.color_groups", "Colors must partition all state columns.")
if (
len(jacobian.pattern.row_pointers) == jacobian.pattern.row_count + 1
and jacobian.pattern.row_pointers
and jacobian.pattern.row_pointers[0] == 0
and jacobian.pattern.row_pointers[-1]
== len(jacobian.pattern.column_indices)
and all(
first <= second
for first, second in zip(
jacobian.pattern.row_pointers,
jacobian.pattern.row_pointers[1:],
)
)
):
rows_by_column: dict[int, set[int]] = {
column: set() for column in range(jacobian.pattern.column_count)
}
for row in range(jacobian.pattern.row_count):
for column in jacobian.pattern.column_indices[
jacobian.pattern.row_pointers[row]
: jacobian.pattern.row_pointers[row + 1]
]:
if column in rows_by_column:
rows_by_column[column].add(row)
for color_index, group in enumerate(jacobian.color_groups):
occupied_rows: set[int] = set()
for column in group:
if column not in rows_by_column:
continue
if occupied_rows & rows_by_column[column]:
issues.add(
"JACOBIAN_COLOR_CONFLICT",
f"jacobian.color_groups[{color_index}]",
"Columns in one color may not share a nonzero row.",
)
break
occupied_rows.update(rows_by_column[column])
for step_index, step in enumerate(jacobian.fill_steps):
valid_step(step, f"jacobian.fill_steps[{step_index}]")
jacobian_entry = next(
(
entry
for entry in program.entry_points
if entry.kind is ir.IREntryPointKind.JACOBIAN
),
None,
)
if jacobian_entry is not None and jacobian.fill_steps != jacobian_entry.steps:
issues.add("JACOBIAN_FILL_STEP_MISMATCH", "jacobian.fill_steps", "Jacobian fill steps must equal the public Jacobian entry plan.")
analytic = set(jacobian.analytic_value_indices)
if len(analytic) != len(jacobian.analytic_value_indices):
issues.add("JACOBIAN_ANALYTIC_DUPLICATE", "jacobian.analytic_value_indices", "Analytic value indices must be unique.")
numerical: set[int] = set()
numerical_sequence: list[int] = []
seen_columns: set[int] = set()
for column_index, column in enumerate(jacobian.local_finite_difference_columns):
if not 0 <= column.column_index < reducer.solver_state_count or column.column_index in seen_columns:
issues.add("JACOBIAN_FD_COLUMN_INVALID", f"jacobian.local_finite_difference_columns[{column_index}]", "FD column is invalid or duplicated.")
seen_columns.add(column.column_index)
if not isfinite(column.relative_step) or column.relative_step <= 0.0:
issues.add("JACOBIAN_FD_STEP_INVALID", f"jacobian.local_finite_difference_columns[{column_index}]", "FD step must be positive.")
numerical.update(column.value_indices)
numerical_sequence.extend(column.value_indices)
if len(set(column.value_indices)) != len(column.value_indices):
issues.add("JACOBIAN_FD_VALUE_DUPLICATE", f"jacobian.local_finite_difference_columns[{column_index}]", "FD value indices within one column must be unique.")
for value_index in column.value_indices:
if (
0 <= value_index < len(jacobian.pattern.column_indices)
and jacobian.pattern.column_indices[value_index]
!= column.column_index
):
issues.add("JACOBIAN_FD_COLUMN_MISMATCH", f"jacobian.local_finite_difference_columns[{column_index}]", "FD values must belong to the declared CSR column.")
if len(set(numerical_sequence)) != len(numerical_sequence):
issues.add("JACOBIAN_FD_VALUE_DUPLICATE", "jacobian.local_finite_difference_columns", "FD value indices may be assigned only once.")
all_value_indices = set(range(jacobian.pattern.nonzero_count))
if analytic & numerical or analytic | numerical != all_value_indices:
issues.add("JACOBIAN_FILL_COVERAGE", "jacobian", "Every nonzero needs exactly one analytic or local-FD strategy.")
if any(index < 0 or index >= jacobian.pattern.nonzero_count for index in analytic | numerical):
issues.add("JACOBIAN_VALUE_INDEX_BOUNDS", "jacobian", "Jacobian strategy references an invalid nonzero.")
issues.unique_ids(program.events, "event_id", "events")
event_root_slots: set[ir.IRSlotRef] = set()
for index, event in enumerate(program.events):
path = f"events[{index}]"
valid_slot(event.root_slot, f"{path}.root_slot")
if event.root_slot.buffer is not ir.IRBufferKind.EVENT_OUTPUT:
issues.add("EVENT_BUFFER_INVALID", f"{path}.root_slot", "Event root must use the event buffer.")
if event.root_slot in event_root_slots:
issues.add("EVENT_ROOT_DUPLICATE", f"{path}.root_slot", "Each event must have a distinct root slot.")
event_root_slots.add(event.root_slot)
if any(item < 0 or item >= len(program.components) for item in event.owner_component_indices):
issues.add("COMPONENT_INDEX_BOUNDS", path, "Event owner is invalid.")
for guard_index, guard in enumerate(event.mode_guards):
if any(
value < -(2**31) or value > 2**31 - 1
for value in guard.allowed_values
):
issues.add(
"MODE_INT32_RANGE",
f"{path}.mode_guards[{guard_index}]",
"Mode guard values must fit signed int32.",
)
if guard.mode_slot not in mode_by_slot:
issues.add("EVENT_MODE_MISSING", f"{path}.mode_guards[{guard_index}]", "Guard mode is not declared.")
elif not set(guard.allowed_values).issubset({value.value for value in mode_by_slot[guard.mode_slot].values}):
issues.add("EVENT_MODE_VALUE_INVALID", f"{path}.mode_guards[{guard_index}]", "Guard value is not declared.")
if len({guard.mode_slot for guard in event.mode_guards}) != len(event.mode_guards):
issues.add("EVENT_MODE_GUARD_DUPLICATE", f"{path}.mode_guards", "An event may guard each mode only once.")
for step_index, step in enumerate(event.reset_steps):
valid_step(step, f"{path}.reset_steps[{step_index}]")
reset_stage_indices = expanded_stage_indices(event.reset_steps)
if any(
program.stages[stage_index].kind is not ir.IRStageKind.RESET
for stage_index in reset_stage_indices
):
issues.add("EVENT_RESET_STAGE_INVALID", f"{path}.reset_steps", "Event reset plans may contain only reset stages.")
if len(set(event.invalidated_caches)) != len(event.invalidated_caches):
issues.add("EVENT_CACHE_DUPLICATE", f"{path}.invalidated_caches", "Invalidated caches must be unique.")
event_buffer = buffer_by_kind.get(ir.IRBufferKind.EVENT_OUTPUT)
expected_event_slots = {
ir.IRSlotRef(ir.IRBufferKind.EVENT_OUTPUT, index)
for index in range(event_buffer.size if event_buffer is not None else 0)
}
if event_root_slots != expected_event_slots:
issues.add("EVENT_ROOT_COVERAGE", "events", "Events must cover every event output slot exactly once.")
issues.unique_ids(program.outputs, "output_id", "outputs")
output_slots: set[ir.IRSlotRef] = set()
output_order_keys: set[tuple[int, str, str | None, int]] = set()
for index, output in enumerate(program.outputs):
path = f"outputs[{index}]"
valid_slot(output.source_slot, f"{path}.source_slot")
valid_slot(output.output_slot, f"{path}.output_slot")
if output.output_slot.buffer is not ir.IRBufferKind.RESULT_OUTPUT:
issues.add("OUTPUT_BUFFER_INVALID", f"{path}.output_slot", "Output must use result buffer.")
if output.output_slot in output_slots:
issues.add("OUTPUT_SLOT_DUPLICATE", f"{path}.output_slot", "Output slot is duplicated.")
output_slots.add(output.output_slot)
if not output.scope or not output.name or not output.label or not output.category or not output.quantity:
issues.add("OUTPUT_METADATA_INCOMPLETE", path, "Output scope, name, label, category and quantity are required.")
if output.order < 0:
issues.add("OUTPUT_ORDER_INVALID", f"{path}.order", "Output order cannot be negative.")
order_key = (
output.component_index,
output.scope,
output.port_name,
output.order,
)
if order_key in output_order_keys:
issues.add("OUTPUT_ORDER_DUPLICATE", f"{path}.order", "Output order must be unique within one component scope/port.")
output_order_keys.add(order_key)
if not 0 <= output.component_index < len(program.components):
issues.add("COMPONENT_INDEX_BOUNDS", path, "Output component is invalid.")
if not isfinite(output.scale) or output.scale == 0.0 or not isfinite(output.offset):
issues.add("OUTPUT_AFFINE_INVALID", path, "Output scale/offset must be finite and scale nonzero.")
if index not in program.components[output.component_index].output_indices if 0 <= output.component_index < len(program.components) else False:
issues.add("COMPONENT_OUTPUT_BACKREF", path, "Owning component does not reference this output.")
result_buffer = buffer_by_kind.get(ir.IRBufferKind.RESULT_OUTPUT)
expected_output_slots = {
ir.IRSlotRef(ir.IRBufferKind.RESULT_OUTPUT, index)
for index in range(result_buffer.size if result_buffer is not None else 0)
}
if output_slots != expected_output_slots:
issues.add("OUTPUT_SLOT_COVERAGE", "outputs", "Outputs must cover every result buffer slot exactly once.")
for component_index, component in enumerate(program.components):
expected_output_indices = tuple(
index
for index, output in enumerate(program.outputs)
if output.component_index == component_index
)
if component.output_indices != expected_output_indices:
issues.add("COMPONENT_OUTPUT_COVERAGE", f"components[{component_index}].output_indices", "Component outputs must exactly cover its owned outputs in program order.")
capabilities = program.capabilities
component_capabilities = tuple(item.component_index for item in capabilities.components)
called_phases_by_component: dict[int, set[ir.IRKernelPhase]] = {
index: set() for index in range(len(program.components))
}
for stage in program.stages:
for operation in stage.operations:
if (
isinstance(operation, ir.IRKernelCallOperation)
and operation.component_index is not None
and 0 <= operation.component_index < len(program.components)
):
called_phases_by_component[operation.component_index].add(
operation.phase
)
if tuple(sorted(component_capabilities)) != tuple(range(len(program.components))) or len(set(component_capabilities)) != len(component_capabilities):
issues.add("CAPABILITY_COMPONENT_COVERAGE", "capabilities.components", "Capability report must cover every component once.")
for index, capability in enumerate(capabilities.components):
if not 0 <= capability.component_index < len(program.components):
issues.add("COMPONENT_INDEX_BOUNDS", f"capabilities.components[{index}]", "Capability component is invalid.")
continue
kernel_index = program.components[capability.component_index].kernel_index
if not 0 <= kernel_index < len(program.kernels):
continue
kernel = program.kernels[kernel_index]
kernel_phases = {phase.phase for phase in kernel.phases}
if len(set(capability.supported_phases)) != len(capability.supported_phases):
issues.add("CAPABILITY_PHASE_DUPLICATE", f"capabilities.components[{index}]", "Supported phases must be unique.")
if not set(capability.supported_phases).issubset(kernel_phases):
issues.add("CAPABILITY_PHASE_MISMATCH", f"capabilities.components[{index}]", "Capability phases must be declared by the component kernel.")
if (
capability.level is ir.IRCapabilityLevel.NATIVE
and not called_phases_by_component[capability.component_index].issubset(
capability.supported_phases
)
):
issues.add(
"CAPABILITY_NATIVE_PHASE_MISSING",
f"capabilities.components[{index}].supported_phases",
"A native component must support every phase used by its kernel calls.",
)
if (
capability.level is ir.IRCapabilityLevel.NATIVE
and kernel.availability is not ir.IRKernelAvailability.NATIVE
):
issues.add("CAPABILITY_KERNEL_MISMATCH", f"capabilities.components[{index}]", "A component cannot be native while its kernel is reference-only.")
if capability.level is ir.IRCapabilityLevel.NATIVE and capability.missing_features:
issues.add("CAPABILITY_MISSING_FEATURE_CONFLICT", f"capabilities.components[{index}]", "Native components cannot list missing features.")
if capability.level is not ir.IRCapabilityLevel.NATIVE and not capability.missing_features:
issues.add("CAPABILITY_MISSING_FEATURE_REQUIRED", f"capabilities.components[{index}]", "Non-native components must identify missing features.")
if len(set(capability.missing_features)) != len(capability.missing_features) or any(
not value for value in capability.missing_features
):
issues.add("CAPABILITY_MISSING_FEATURE_INVALID", f"capabilities.components[{index}]", "Missing feature IDs must be non-empty and unique.")
issue_keys: set[tuple[str, str]] = set()
for index, issue in enumerate(capabilities.issues):
key = (issue.code, issue.scope_id)
if not issue.code or not issue.scope_id or not issue.message:
issues.add("CAPABILITY_ISSUE_INCOMPLETE", f"capabilities.issues[{index}]", "Capability issues require code, scope and message.")
if key in issue_keys:
issues.add("CAPABILITY_ISSUE_DUPLICATE", f"capabilities.issues[{index}]", "Capability issue code/scope pairs must be unique.")
issue_keys.add(key)
if any(issue.severity is ir.IRDiagnosticSeverity.ERROR for issue in capabilities.issues) and capabilities.system_level is not ir.IRCapabilityLevel.UNSUPPORTED:
issues.add("CAPABILITY_LEVEL_CONFLICT", "capabilities.system_level", "Error issues require unsupported system level.")
if capabilities.system_level is ir.IRCapabilityLevel.NATIVE and any(
item.level is not ir.IRCapabilityLevel.NATIVE for item in capabilities.components
):
issues.add("CAPABILITY_LEVEL_CONFLICT", "capabilities.system_level", "Native system contains a non-native component.")
if capabilities.system_level is ir.IRCapabilityLevel.NATIVE and any(
kernel.availability is not ir.IRKernelAvailability.NATIVE
for kernel in program.kernels
):
issues.add("CAPABILITY_LEVEL_CONFLICT", "capabilities.system_level", "Native system contains a reference-only kernel.")
if (
capabilities.system_level is ir.IRCapabilityLevel.NATIVE
and transaction.cache_attribute_ids
):
issues.add(
"CAPABILITY_LEVEL_CONFLICT",
"transaction.cache_attribute_ids",
"Native systems cannot depend on opaque Python cache attributes.",
)
if (
capabilities.system_level is ir.IRCapabilityLevel.NATIVE
and "reference_kernel_dispatch" in program.required_features
):
issues.add(
"CAPABILITY_LEVEL_CONFLICT",
"required_features",
"Native systems cannot require reference-kernel dispatch.",
)
if (
capabilities.system_level is not ir.IRCapabilityLevel.UNSUPPORTED
and any(
item.level is ir.IRCapabilityLevel.UNSUPPORTED
for item in capabilities.components
)
):
issues.add(
"CAPABILITY_LEVEL_CONFLICT",
"capabilities.system_level",
"A system containing an unsupported component must be unsupported.",
)
if (
capabilities.system_level is ir.IRCapabilityLevel.UNSUPPORTED
and not any(
issue.severity is ir.IRDiagnosticSeverity.ERROR
for issue in capabilities.issues
)
):
issues.add("CAPABILITY_LEVEL_CONFLICT", "capabilities.system_level", "Unsupported systems require an error capability issue.")
try:
program.canonical_json_bytes()
except (TypeError, ValueError) as exc:
issues.add("CANONICAL_SERIALIZATION_FAILED", "$", str(exc))
return IRValidationReport(tuple(issues.items))
def require_valid_system_ir(program: ir.SystemIR) -> ir.SystemIR:
"""Return ``program`` or raise one error containing all static issues."""
report = validate_system_ir(program)
if not report.valid:
raise SystemIRValidationError(report)
return program