Merge system-optimization IR contracts and retire legacy engine adapter

This commit is contained in:
ljz committed 2026-09-10 01:21:53 +08:00
commit 743663e3a6
14 files changed
+3817 -161

No files matched your search

+49 -151
View File
@@ -1,58 +1,31 @@
name: Solver regression
name: Native backend regression
on:
push:
paths:
- "app/simulation/**"
- "app/**"
- "native/**"
- "schemas/**"
- "tests/**"
- "requirements.txt"
- "requirements*.txt"
- "constraints/**"
- ".python-version"
- ".gitattributes"
- "README.md"
- ".github/workflows/solver-regression.yml"
pull_request:
paths:
- "app/simulation/**"
- "app/**"
- "native/**"
- "schemas/**"
- "tests/**"
- "requirements.txt"
- "requirements*.txt"
- "constraints/**"
- ".python-version"
- ".gitattributes"
- "README.md"
- ".github/workflows/solver-regression.yml"
schedule:
- cron: "17 3 * * 1-6"
- cron: "17 3 * * 0"
- cron: "17 3 * * *"
workflow_dispatch:
inputs:
suite:
description: Regression tier
required: true
default: quick
type: choice
options:
- quick
- historical
- main-long
case:
description: Longest main-model horizon (predecessors run first)
required: true
default: 0.2s
type: choice
options:
- 0.2s
- 1s
- 5s
- 10s
lane:
description: Output sampling lane
required: true
default: production
type: choice
options:
- solver-only
- production
concurrency:
group: solver-regression-${{ github.ref }}-${{ github.event_name }}
@@ -62,142 +35,67 @@ permissions:
contents: read
jobs:
fixture-byte-contract:
if: >-
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'quick')
strategy:
fail-fast: false
matrix:
os:
- ubuntu-24.04
- windows-2022
runs-on: ${{ matrix.os }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version
- name: Verify portable regression fixture bytes
run: python -m unittest tests.test_regression_fixture_line_endings
quick:
if: >-
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'quick')
contracts:
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: |
requirements.txt
constraints/python312-direct.txt
constraints/python312-linux-x86_64.lock
- name: Install hashed Linux release lock
cache-dependency-path: constraints/python312-linux-x86_64.lock
- name: Install backend runtime without numerical Python packages
run: |
python -m pip install \
--force-reinstall \
-r constraints/python312-linux-x86_64.lock
python -m pip install -r constraints/python312-linux-x86_64.lock
python -m pip check
- name: Run solver foundation tests
- name: Validate portable metadata and IR contracts
env:
SYSTEM_SIMULATION_VERIFY_LOCKED_ENV: "1"
run: |
python -W error::ResourceWarning -m unittest \
tests.test_dependency_constraints \
tests.test_benchmark_regression \
tests.test_physical_state_v21 \
tests.test_test_mql_ame_contract \
tests.test_test_mql_8_regression \
tests.test_mql_full_branches_regression \
tests.test_pressure_flow_causal_execution \
tests.test_stream_pressure_block_solver \
tests.test_core_solver \
tests.test_supported_piston_tangent \
tests.test_three_piston_tangent \
tests.test_sparse_secant_jacobian \
tests.test_generic_jacobian_sparsity \
tests.test_generic_system_xml_simulation
tests.test_regression_fixture_line_endings \
tests.test_component_catalog \
tests.test_component_metadata \
tests.test_component_registry \
tests.test_medium_reference_contract \
tests.test_system_xml_v3 \
tests.test_system_numeric_ir_v2 \
tests.test_native_only_backend
historical-nightly:
if: >-
(github.event_name == 'schedule' && github.event.schedule == '17 3 * * 1-6') ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'historical')
runs-on: ubuntu-24.04
timeout-minutes: 15
native-windows:
runs-on: windows-2022
timeout-minutes: 30
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: conda-incubator/setup-miniconda@v3
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: |
requirements.txt
constraints/python312-direct.txt
constraints/python312-linux-x86_64.lock
- name: Install hashed Linux release lock
python-version: "3.12"
activate-environment: simulation-native
auto-activate-base: false
- name: Install native compiler and SUNDIALS
run: |
python -m pip install \
--force-reinstall \
-r constraints/python312-linux-x86_64.lock
conda install --yes -c conda-forge sundials=7.4.0 m2w64-gcc
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
"SUNDIALS_ROOT=$env:CONDA_PREFIX\Library" >> $env:GITHUB_ENV
"SIMULATION_NATIVE_CC=$env:CONDA_PREFIX\Library\mingw-w64\bin\gcc.exe" >> $env:GITHUB_ENV
python -m pip install -r requirements-test.txt
python -m pip check
- name: Run 0.81 and 2.10 second historical regression
- name: Run catalog, numerical, API and schema regression
run: |
mkdir -p artifacts
python -m app.simulation.benchmark_regression \
--manifest tests/baselines/simulation/test_mql_full_branches/manifest.json \
--lane production \
--output artifacts/test-mql-full-branches.json
python -c "from app.simulation.native_codegen.build import toolchain; print(toolchain())"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python -W error::ResourceWarning -m unittest discover -s tests
- name: Complete the 10 second skill fixture in native RK45
run: |
python -m app.simulation.native_codegen tests/data/native-skill-test.xml --output-dir test/ci-skill --method RK45 --max-step 0.001 --rtol 1e-7 --runs 1 --solve-only
- if: always()
uses: actions/upload-artifact@v4
with:
name: historical-solver-regression
path: artifacts/*.json
if-no-files-found: warn
main-periodic:
if: >-
(github.event_name == 'schedule' && github.event.schedule == '17 3 * * 0') ||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'main-long')
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version
cache: pip
cache-dependency-path: |
requirements.txt
constraints/python312-direct.txt
constraints/python312-linux-x86_64.lock
- name: Install hashed Linux release lock
run: |
python -m pip install \
--force-reinstall \
-r constraints/python312-linux-x86_64.lock
python -m pip check
- name: Run bounded progressive main-model regression
env:
REQUESTED_CASE: ${{ github.event_name == 'workflow_dispatch' && inputs.case || '10s' }}
REQUESTED_LANE: ${{ github.event_name == 'workflow_dispatch' && inputs.lane || 'production' }}
run: |
mkdir -p artifacts
python -m app.simulation.benchmark_regression \
--manifest tests/baselines/simulation/test_mql_8/manifest.json \
--lane "$REQUESTED_LANE" \
--case "$REQUESTED_CASE" \
--output artifacts/test-mql-8-progressive.json
- if: always()
uses: actions/upload-artifact@v4
with:
name: main-model-progressive-regression
path: artifacts/*.json
name: native-skill-regression
path: test/ci-skill/summary.json
if-no-files-found: warn
+40
View File
@@ -0,0 +1,40 @@
"""Whole-system numeric intermediate representation."""
from .compiler import compile_system_ir
from .schema import * # noqa: F403 - this package is the public schema facade.
from .schema import __dict__ as _schema_namespace
from .validation import (
IRValidationIssue,
IRValidationReport,
SystemIRValidationError,
require_valid_system_ir,
validate_system_ir,
)
__all__ = [
"compile_system_ir",
"IRValidationIssue",
"IRValidationReport",
"SystemIRValidationError",
"require_valid_system_ir",
"validate_system_ir",
*sorted(
name
for name in _schema_namespace
if name.startswith("IR")
or name.startswith("SystemIR")
or name.startswith("SYSTEM_NUMERIC_IR")
or name
in {
"CURRENT_SYSTEM_IR_VERSION",
"NATIVE_NUMERIC_ABI_VERSION",
"canonical_json_bytes",
"native_artifact_key",
"operation_read_slots",
"operation_write_slots",
}
),
]
del _schema_namespace
+14
View File
@@ -0,0 +1,14 @@
"""Retired object-engine IR adapter; the independent v2 schema is retained.
Current native execution is generated by native_codegen.compile_native_program.
It does not yet emit the separate System IR v2 execution-plan format.
"""
def compile_system_ir(*args, **kwargs):
"""Fail explicitly for callers of the removed GenericFluidSystem adapter."""
raise NotImplementedError(
"The GenericFluidSystem to System IR v2 exporter was retired with the Python "
"numerical engine. Use native_codegen.compiler.compile_native_program(network) "
"for the current C executable path. System IR v2 schema and validation remain available."
)
+973
View File
@@ -0,0 +1,973 @@
"""Callback-free data contract for the whole-system numeric IR.
System IR v2 describes a compiled simulation model. It is intentionally a
pure, immutable data graph: Python functions, model objects, object addresses,
and run-local diagnostic state are not part of this module's wire contract.
"""
from __future__ import annotations
from dataclasses import dataclass, fields, is_dataclass
from enum import StrEnum
from hashlib import sha256
import json
from math import isfinite
from struct import pack
from typing import ClassVar
from unicodedata import normalize
SYSTEM_NUMERIC_IR_SCHEMA_ID = "system-numeric-ir"
SYSTEM_NUMERIC_IR_SCHEMA_MAJOR = 2
SYSTEM_NUMERIC_IR_SCHEMA_MINOR = 0
SYSTEM_NUMERIC_IR_COMPILER_ID = "generic-fluid-system"
SYSTEM_NUMERIC_IR_COMPILER_VERSION = "2.0.0"
NATIVE_NUMERIC_ABI_VERSION = 1
class IRDType(StrEnum):
FLOAT64 = "float64"
INT32 = "int32"
class IRBufferKind(StrEnum):
TIME = "time"
STATE_INPUT = "state_input"
DERIVATIVE_OUTPUT = "derivative_output"
LOCAL_STATE = "local_state"
LOCAL_DERIVATIVE = "local_derivative"
ALGEBRAIC = "algebraic"
SIGNAL = "signal"
PARAMETER = "parameter"
CONSTANT = "constant"
MODE = "mode"
WORK_FLOAT = "work_float"
WORK_INT = "work_int"
EVENT_OUTPUT = "event_output"
JACOBIAN_VALUE = "jacobian_value"
RESULT_OUTPUT = "result_output"
RUNTIME_INPUT = "runtime_input"
class IRKernelPhase(StrEnum):
PRIMAL = "primal"
RESIDUAL = "residual"
DERIVATIVE = "derivative"
PROPERTY = "property"
EVENT = "event"
RESET = "reset"
JACOBIAN = "jacobian"
OUTPUT = "output"
class IRKernelAvailability(StrEnum):
NATIVE = "native"
REFERENCE_ONLY = "reference_only"
class IRPortKind(StrEnum):
PHYSICAL = "physical"
SIGNAL = "signal"
class IRPortNominalRole(StrEnum):
INLET = "inlet"
OUTLET = "outlet"
BIDIRECTIONAL = "bidirectional"
INPUT = "input"
OUTPUT = "output"
class IRPositiveFlowDirection(StrEnum):
INTO_COMPONENT = "intoComponent"
class IRVariableRole(StrEnum):
EFFORT = "effort"
FLOW = "flow"
STREAM = "stream"
SIGNAL = "signal"
class IRConnectionRule(StrEnum):
EQUAL = "equal"
SUM_TO_ZERO = "sumToZero"
STREAM_MIX = "streamMix"
DIRECTED = "directed"
class IREquationOwner(StrEnum):
COMPONENT = "component"
CONNECTION = "connection"
class IREquationRelation(StrEnum):
EQUAL = "equal"
SUM_TO_ZERO = "sumToZero"
CONSTITUTIVE = "constitutive"
STATE = "state"
class IRPressureFlowScopeKind(StrEnum):
NETWORK = "network"
PHYSICAL_ISLAND = "physical_island"
EQUATION_BLOCK = "equation_block"
class IRStageKind(StrEnum):
STATE_REDUCE = "state_reduce"
SIGNAL = "signal"
MECHANICAL_EQUIVALENCE = "mechanical_equivalence"
DYNAMIC_VOLUME = "dynamic_volume"
PROPERTY = "property"
PRESSURE_FLOW = "pressure_flow"
STREAM = "stream"
TEMPERATURE_REFERENCE = "temperature_reference"
THERMOFLUID_FIXED_POINT = "thermofluid_fixed_point"
MECHANICAL_ACCELERATION = "mechanical_acceleration"
DERIVATIVE_REDUCE = "derivative_reduce"
EVENT = "event"
JACOBIAN = "jacobian"
OUTPUT = "output"
RESET = "reset"
class IREntryPointKind(StrEnum):
RHS = "rhs"
EVENTS = "events"
JACOBIAN = "jacobian"
OUTPUTS = "outputs"
class IRBlockKind(StrEnum):
SEQUENCE = "sequence"
FIXED_POINT = "fixed_point"
STREAM_SCC = "stream_scc"
class IRStepKind(StrEnum):
STAGE = "stage"
BLOCK = "block"
class IRStateMapKind(StrEnum):
SCATTER = "scatter"
DERIVATIVE_GATHER = "derivative_gather"
class IREventDirection(StrEnum):
DECREASING = "decreasing"
ANY = "any"
INCREASING = "increasing"
class IRFailurePolicy(StrEnum):
FAIL = "fail"
RETRY_SMALLER_STEP = "retry_smaller_step"
class IRCacheKind(StrEnum):
PROPERTY = "property"
PRESSURE_FLOW = "pressure_flow"
STREAM = "stream"
JACOBIAN = "jacobian"
OUTPUT = "output"
class IRCapabilityLevel(StrEnum):
NATIVE = "native"
REFERENCE_ONLY = "reference_only"
UNSUPPORTED = "unsupported"
class IRDiagnosticSeverity(StrEnum):
INFO = "info"
WARNING = "warning"
ERROR = "error"
class IROpcode(StrEnum):
FILL = "fill"
COPY = "copy"
SCATTER = "scatter"
LINEAR_COMBINATION = "linear_combination"
STATE_MAP = "state_map"
KERNEL_CALL = "kernel_call"
EFFORT_BROADCAST = "effort_broadcast"
FLOW_ASSIGN = "flow_assign"
CHECK_FINITE = "check_finite"
@dataclass(frozen=True, slots=True)
class IRSchemaVersion:
schema_id: str = SYSTEM_NUMERIC_IR_SCHEMA_ID
major: int = SYSTEM_NUMERIC_IR_SCHEMA_MAJOR
minor: int = SYSTEM_NUMERIC_IR_SCHEMA_MINOR
CURRENT_SYSTEM_IR_VERSION = IRSchemaVersion()
@dataclass(frozen=True, slots=True)
class IRSlotRef:
buffer: IRBufferKind
index: int
@dataclass(frozen=True, slots=True)
class IRBufferSpec:
kind: IRBufferKind
dtype: IRDType
size: int
initial_float_values: tuple[float, ...] = ()
initial_int_values: tuple[int, ...] = ()
@dataclass(frozen=True, slots=True)
class IRValueSpec:
value_id: str
slot: IRSlotRef
semantic: str
role: str
quantity: str
unit: str
scale: float
lower_bound: float | None = None
upper_bound: float | None = None
owner_component_index: int | None = None
@dataclass(frozen=True, slots=True)
class IRKernelPhaseSpec:
"""Phase capability tag; each call's ordered slot lists define its C-01 ABI.
Fixed component-kernel signatures deliberately belong to the C-02 contract.
Recording placeholder arities here would make the current reference-only
kernels look more strictly specified than they are.
"""
phase: IRKernelPhase
@dataclass(frozen=True, slots=True)
class IRKernelSpec:
kernel_id: str
model_type: str
model_version: str
implementation_version: str
availability: IRKernelAvailability
unavailable_reason: str | None
phases: tuple[IRKernelPhaseSpec, ...]
parameter_count: int
state_count: int
mode_count: int
workspace_float_count: int
workspace_int_count: int
@dataclass(frozen=True, slots=True)
class IRComponentInstance:
instance_id: str
kernel_index: int
parameter_slots: tuple[IRSlotRef, ...]
state_slots: tuple[IRSlotRef, ...]
derivative_slots: tuple[IRSlotRef, ...]
mode_slots: tuple[IRSlotRef, ...]
port_indices: tuple[int, ...]
port_slots: tuple[IRSlotRef, ...]
output_indices: tuple[int, ...]
workspace_float_slots: tuple[IRSlotRef, ...]
workspace_int_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRPortVariable:
variable_id: str
name: str
role: IRVariableRole
connection_rule: IRConnectionRule
quantity: str
unit: str
result_visible: bool
slot: IRSlotRef
@dataclass(frozen=True, slots=True)
class IRPortSpec:
port_id: str
component_index: int
name: str
kind: IRPortKind
domain: str
nominal_role: IRPortNominalRole
positive_flow_direction: IRPositiveFlowDirection | None
variables: tuple[IRPortVariable, ...]
@dataclass(frozen=True, slots=True)
class IRConnectionVariable:
name: str
rule: IRConnectionRule
endpoint_a_slot: IRSlotRef
endpoint_b_slot: IRSlotRef
@dataclass(frozen=True, slots=True)
class IRConnectionSpec:
connection_id: str
kind: IRPortKind
domain: str
endpoint_a_port_index: int
endpoint_b_port_index: int
variables: tuple[IRConnectionVariable, ...]
@dataclass(frozen=True, slots=True)
class IRMediumSpec:
medium_id: str
name: str
implementation_id: str
implementation_version: str
parameter_slots: tuple[IRSlotRef, ...]
component_indices: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IRCSRPattern:
row_count: int
column_count: int
row_pointers: tuple[int, ...]
column_indices: tuple[int, ...]
@property
def nonzero_count(self) -> int:
return len(self.column_indices)
@dataclass(frozen=True, slots=True)
class IRCSRMatrix:
pattern: IRCSRPattern
values: tuple[float, ...]
@dataclass(frozen=True, slots=True)
class IRStateReducer:
solver_state_count: int
local_state_slots: tuple[IRSlotRef, ...]
raw_derivative_slots: tuple[IRSlotRef, ...]
state_scatter: IRCSRMatrix
derivative_gather: IRCSRMatrix
initial_state: tuple[float, ...]
absolute_tolerances: tuple[float, ...]
@dataclass(frozen=True, slots=True)
class IRFillOperation:
opcode: ClassVar[IROpcode] = IROpcode.FILL
target_slots: tuple[IRSlotRef, ...]
value: float
@dataclass(frozen=True, slots=True)
class IRCopyOperation:
opcode: ClassVar[IROpcode] = IROpcode.COPY
source_slot: IRSlotRef
target_slot: IRSlotRef
@dataclass(frozen=True, slots=True)
class IRScatterOperation:
opcode: ClassVar[IROpcode] = IROpcode.SCATTER
source_slot: IRSlotRef
target_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRLinearCombinationOperation:
opcode: ClassVar[IROpcode] = IROpcode.LINEAR_COMBINATION
source_slots: tuple[IRSlotRef, ...]
weights: tuple[float, ...]
target_slot: IRSlotRef
bias: float = 0.0
@dataclass(frozen=True, slots=True)
class IRStateMapOperation:
opcode: ClassVar[IROpcode] = IROpcode.STATE_MAP
map_kind: IRStateMapKind
source_slots: tuple[IRSlotRef, ...]
target_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRKernelCallOperation:
opcode: ClassVar[IROpcode] = IROpcode.KERNEL_CALL
kernel_index: int
component_index: int | None
phase: IRKernelPhase
read_slots: tuple[IRSlotRef, ...]
write_slots: tuple[IRSlotRef, ...]
equation_indices: tuple[int, ...] = ()
@dataclass(frozen=True, slots=True)
class IREffortBroadcastOperation:
opcode: ClassVar[IROpcode] = IROpcode.EFFORT_BROADCAST
variable: str
anchor_slot: IRSlotRef
residual_slot: IRSlotRef
result_slot: IRSlotRef
scatter_slots: tuple[IRSlotRef, ...]
equation_id: str
lower_bound: float | None = None
upper_bound: float | None = None
@dataclass(frozen=True, slots=True)
class IRFlowAssignmentOperation:
opcode: ClassVar[IROpcode] = IROpcode.FLOW_ASSIGN
value_slot: IRSlotRef
result_slot: IRSlotRef
scatter_slots: tuple[IRSlotRef, ...]
equation_id: str
@dataclass(frozen=True, slots=True)
class IRCheckFiniteOperation:
opcode: ClassVar[IROpcode] = IROpcode.CHECK_FINITE
slots: tuple[IRSlotRef, ...]
error_code: str
IROperation = (
IRFillOperation
| IRCopyOperation
| IRScatterOperation
| IRLinearCombinationOperation
| IRStateMapOperation
| IRKernelCallOperation
| IREffortBroadcastOperation
| IRFlowAssignmentOperation
| IRCheckFiniteOperation
)
def operation_read_slots(operation: IROperation) -> tuple[IRSlotRef, ...]:
if isinstance(operation, IRFillOperation):
return ()
if isinstance(operation, (IRCopyOperation, IRScatterOperation)):
return (operation.source_slot,)
if isinstance(operation, (IRLinearCombinationOperation, IRStateMapOperation)):
return operation.source_slots
if isinstance(operation, IRKernelCallOperation):
return operation.read_slots
if isinstance(operation, IREffortBroadcastOperation):
return (operation.anchor_slot, operation.residual_slot)
if isinstance(operation, IRFlowAssignmentOperation):
return (operation.value_slot,)
if isinstance(operation, IRCheckFiniteOperation):
return operation.slots
raise TypeError(f"Unsupported IR operation: {type(operation).__name__}.")
def operation_write_slots(operation: IROperation) -> tuple[IRSlotRef, ...]:
if isinstance(operation, IRFillOperation):
return operation.target_slots
if isinstance(operation, IRCopyOperation):
return (operation.target_slot,)
if isinstance(operation, IRScatterOperation):
return operation.target_slots
if isinstance(operation, IRLinearCombinationOperation):
return (operation.target_slot,)
if isinstance(operation, IRStateMapOperation):
return operation.target_slots
if isinstance(operation, IRKernelCallOperation):
return operation.write_slots
if isinstance(operation, IREffortBroadcastOperation):
return (operation.result_slot, *operation.scatter_slots)
if isinstance(operation, IRFlowAssignmentOperation):
return (operation.result_slot, *operation.scatter_slots)
if isinstance(operation, IRCheckFiniteOperation):
return ()
raise TypeError(f"Unsupported IR operation: {type(operation).__name__}.")
@dataclass(frozen=True, slots=True)
class IRStage:
stage_id: str
kind: IRStageKind
operations: tuple[IROperation, ...]
declared_read_slots: tuple[IRSlotRef, ...]
declared_write_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRStepRef:
kind: IRStepKind
index: int
@dataclass(frozen=True, slots=True)
class IRConvergenceSpec:
monitor_slots: tuple[IRSlotRef, ...]
absolute_tolerance: float
relative_tolerance: float
max_iterations: int
relaxation: float
rollback_slots: tuple[IRSlotRef, ...]
failure_policy: IRFailurePolicy
@dataclass(frozen=True, slots=True)
class IRExecutionBlock:
block_id: str
kind: IRBlockKind
steps: tuple[IRStepRef, ...]
convergence: IRConvergenceSpec | None = None
@dataclass(frozen=True, slots=True)
class IREntryPoint:
kind: IREntryPointKind
steps: tuple[IRStepRef, ...]
input_slots: tuple[IRSlotRef, ...]
output_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRCausalEffortStageRef:
variable: str
stage_index: int
@dataclass(frozen=True, slots=True)
class IRCausalPlan:
plan_id: str
scope_component_indices: tuple[int, ...]
source_schema_version: int
source_structural_signature: str | None
fallback_reason: str | None
canonical_slots: tuple[IRSlotRef, ...]
compatibility_slots: tuple[IRSlotRef, ...]
reset_slots: tuple[IRSlotRef, ...]
external_effort_slots: tuple[IRSlotRef, ...]
effort_stages: tuple[IRCausalEffortStageRef, ...]
flow_stage_indices: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IRAlgebraicUnknown:
unknown_id: str
component_index: int
port_index: int
variable: str
role: IRVariableRole
slot: IRSlotRef
scale: float
lower_bound: float | None = None
upper_bound: float | None = None
@dataclass(frozen=True, slots=True)
class IRPressureFlowEquation:
equation_id: str
owner: IREquationOwner
owner_index: int
relation: IREquationRelation
role: IRVariableRole | None
variable_slots: tuple[IRSlotRef, ...]
residual_slot: IRSlotRef
scale: float
@dataclass(frozen=True, slots=True)
class IRPressureFlowBlock:
block_id: str
unknown_indices: tuple[int, ...]
equation_indices: tuple[int, ...]
jacobian_pattern: IRCSRPattern
@dataclass(frozen=True, slots=True)
class IRPressureFlowScope:
scope_id: str
kind: IRPressureFlowScopeKind
component_indices: tuple[int, ...]
unknown_indices: tuple[int, ...]
equation_indices: tuple[int, ...]
block_indices: tuple[int, ...]
causal_plan_index: int | None
residual_tolerance: float
max_evaluations: int
sparse_pattern_trusted: bool
sparse_fallback_reason: str | None
@dataclass(frozen=True, slots=True)
class IRPressureFlowPlan:
unknowns: tuple[IRAlgebraicUnknown, ...]
equations: tuple[IRPressureFlowEquation, ...]
blocks: tuple[IRPressureFlowBlock, ...]
scopes: tuple[IRPressureFlowScope, ...]
global_scope_index: int
secondary_scope_indices: tuple[int, ...]
pressure_lower_bound: float
@dataclass(frozen=True, slots=True)
class IRStreamSCC:
scc_id: str
node_slots: tuple[IRSlotRef, ...]
block_index: int
@dataclass(frozen=True, slots=True)
class IRStreamEdge:
source_scc_index: int
target_scc_index: int
@dataclass(frozen=True, slots=True)
class IRStreamPlan:
plan_id: str
node_slots: tuple[IRSlotRef, ...]
strongly_connected_components: tuple[IRStreamSCC, ...]
condensed_edges: tuple[IRStreamEdge, ...]
topological_scc_indices: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IRThermofluidPlan:
physical_port_indices: tuple[int, ...]
global_component_indices: tuple[int, ...]
stream_plan_index: int
secondary_pressure_scope_indices: tuple[int, ...]
sensitive_component_indices: tuple[int, ...]
maximum_iterations: int
flow_relative_tolerance: float
uses_conservative_global_solver: bool
conservative_fallback_reason: str | None
@dataclass(frozen=True, slots=True)
class IRTransactionPlan:
snapshot_slots: tuple[IRSlotRef, ...]
flow_slots: tuple[IRSlotRef, ...]
cache_component_indices: tuple[int, ...]
cache_attribute_ids: tuple[str, ...]
diagnostic_owner_ids: tuple[str, ...]
restores_on_recoverable_failure: bool
restores_on_fatal_failure: bool
@dataclass(frozen=True, slots=True)
class IRModeValueSpec:
value: int
name: str
@dataclass(frozen=True, slots=True)
class IRModeSpec:
mode_id: str
slot: IRSlotRef
owner_component_indices: tuple[int, ...]
values: tuple[IRModeValueSpec, ...]
initial_value: int
@dataclass(frozen=True, slots=True)
class IRFiniteDifferenceColumn:
column_index: int
value_indices: tuple[int, ...]
relative_step: float
@dataclass(frozen=True, slots=True)
class IRJacobianPlan:
pattern: IRCSRPattern
value_slots: tuple[IRSlotRef, ...]
color_groups: tuple[tuple[int, ...], ...]
fill_steps: tuple[IRStepRef, ...]
analytic_value_indices: tuple[int, ...]
local_finite_difference_columns: tuple[IRFiniteDifferenceColumn, ...]
@dataclass(frozen=True, slots=True)
class IRModeGuard:
mode_slot: IRSlotRef
allowed_values: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IREventSpec:
event_id: str
event_kind: str
owner_component_indices: tuple[int, ...]
root_slot: IRSlotRef
direction: IREventDirection
terminal: bool
priority: int
mode_guards: tuple[IRModeGuard, ...]
reset_steps: tuple[IRStepRef, ...]
invalidated_caches: tuple[IRCacheKind, ...]
restarts_integrator: bool
@dataclass(frozen=True, slots=True)
class IROutputSpec:
output_id: str
component_index: int
scope: str
port_name: str | None
name: str
label: str
category: str
quantity: str
unit: str
order: int
source_slot: IRSlotRef
output_slot: IRSlotRef
scale: float = 1.0
offset: float = 0.0
@dataclass(frozen=True, slots=True)
class IRComponentCapability:
component_index: int
level: IRCapabilityLevel
supported_phases: tuple[IRKernelPhase, ...]
missing_features: tuple[str, ...]
@dataclass(frozen=True, slots=True)
class IRCapabilityIssue:
code: str
severity: IRDiagnosticSeverity
scope_id: str
message: str
@dataclass(frozen=True, slots=True)
class IRCapabilityReport:
system_level: IRCapabilityLevel
components: tuple[IRComponentCapability, ...]
issues: tuple[IRCapabilityIssue, ...]
@dataclass(frozen=True, slots=True)
class SystemIR:
version: IRSchemaVersion
model_id: str
model_version: str
compiler_id: str
compiler_version: str
numeric_dtype: IRDType
buffers: tuple[IRBufferSpec, ...]
values: tuple[IRValueSpec, ...]
kernels: tuple[IRKernelSpec, ...]
components: tuple[IRComponentInstance, ...]
mediums: tuple[IRMediumSpec, ...]
ports: tuple[IRPortSpec, ...]
connections: tuple[IRConnectionSpec, ...]
state_reducer: IRStateReducer
causal_plans: tuple[IRCausalPlan, ...]
pressure_flow: IRPressureFlowPlan
stream_plans: tuple[IRStreamPlan, ...]
thermofluid: IRThermofluidPlan
stages: tuple[IRStage, ...]
blocks: tuple[IRExecutionBlock, ...]
entry_points: tuple[IREntryPoint, ...]
transaction: IRTransactionPlan
modes: tuple[IRModeSpec, ...]
jacobian: IRJacobianPlan
events: tuple[IREventSpec, ...]
outputs: tuple[IROutputSpec, ...]
capabilities: IRCapabilityReport
required_features: tuple[str, ...] = ()
def canonical_json_bytes(self) -> bytes:
return canonical_json_bytes(self)
def calculate_structural_signature(self) -> str:
return sha256(self.canonical_json_bytes()).hexdigest()
@property
def structural_signature(self) -> str:
return self.calculate_structural_signature()
@dataclass(frozen=True, slots=True)
class IRNativeBuildIdentity:
abi_version: int
target_triple: str
compiler_id: str
compiler_version: str
compile_flags: tuple[str, ...]
floating_point_policy: str
kernel_library_signature: str
@dataclass(frozen=True, slots=True)
class _IRNativeArtifactKeyInput:
program_signature: str
build: IRNativeBuildIdentity
_CANONICAL_TYPE_NAMES: tuple[tuple[type[object], str], ...] = (
(IRSchemaVersion, "schema_version"),
(IRSlotRef, "slot_ref"),
(IRBufferSpec, "buffer"),
(IRValueSpec, "value"),
(IRKernelPhaseSpec, "kernel_phase"),
(IRKernelSpec, "kernel"),
(IRComponentInstance, "component"),
(IRPortVariable, "port_variable"),
(IRPortSpec, "port"),
(IRConnectionVariable, "connection_variable"),
(IRConnectionSpec, "connection"),
(IRMediumSpec, "medium"),
(IRCSRPattern, "csr_pattern"),
(IRCSRMatrix, "csr_matrix"),
(IRStateReducer, "state_reducer"),
(IRFillOperation, "operation"),
(IRCopyOperation, "operation"),
(IRScatterOperation, "operation"),
(IRLinearCombinationOperation, "operation"),
(IRStateMapOperation, "operation"),
(IRKernelCallOperation, "operation"),
(IREffortBroadcastOperation, "operation"),
(IRFlowAssignmentOperation, "operation"),
(IRCheckFiniteOperation, "operation"),
(IRStage, "stage"),
(IRStepRef, "step_ref"),
(IRConvergenceSpec, "convergence"),
(IRExecutionBlock, "block"),
(IREntryPoint, "entry_point"),
(IRCausalEffortStageRef, "causal_effort_stage"),
(IRCausalPlan, "causal_plan"),
(IRAlgebraicUnknown, "algebraic_unknown"),
(IRPressureFlowEquation, "pressure_flow_equation"),
(IRPressureFlowBlock, "pressure_flow_block"),
(IRPressureFlowScope, "pressure_flow_scope"),
(IRPressureFlowPlan, "pressure_flow_plan"),
(IRStreamSCC, "stream_scc"),
(IRStreamEdge, "stream_edge"),
(IRStreamPlan, "stream_plan"),
(IRThermofluidPlan, "thermofluid_plan"),
(IRTransactionPlan, "transaction_plan"),
(IRModeValueSpec, "mode_value"),
(IRModeSpec, "mode"),
(IRFiniteDifferenceColumn, "finite_difference_column"),
(IRJacobianPlan, "jacobian_plan"),
(IRModeGuard, "mode_guard"),
(IREventSpec, "event"),
(IROutputSpec, "output"),
(IRComponentCapability, "component_capability"),
(IRCapabilityIssue, "capability_issue"),
(IRCapabilityReport, "capability_report"),
(SystemIR, "system_ir"),
(IRNativeBuildIdentity, "native_build"),
(_IRNativeArtifactKeyInput, "native_artifact_key_input"),
)
_OPERATION_TYPES = (
IRFillOperation,
IRCopyOperation,
IRScatterOperation,
IRLinearCombinationOperation,
IRStateMapOperation,
IRKernelCallOperation,
IREffortBroadcastOperation,
IRFlowAssignmentOperation,
IRCheckFiniteOperation,
)
def _canonical_type_name(value: object) -> str:
value_type = type(value)
for candidate, name in _CANONICAL_TYPE_NAMES:
if value_type is candidate:
return name
raise TypeError(f"Unsupported IR schema object: {value_type.__name__}.")
def _canonical_float(value: float) -> object:
numeric = float(value)
if not isfinite(numeric):
raise ValueError("Canonical IR JSON does not permit NaN or infinity.")
if numeric == 0.0:
numeric = 0.0
return {"$float64": pack(">d", numeric).hex()}
def _canonical_value(value: object) -> object:
if value is None or isinstance(value, bool):
return value
if isinstance(value, StrEnum):
return value.value
if isinstance(value, int):
return value
if isinstance(value, float):
return _canonical_float(value)
if isinstance(value, str):
return normalize("NFC", value)
if isinstance(value, tuple):
return [_canonical_value(item) for item in value]
if is_dataclass(value) and not isinstance(value, type):
payload: dict[str, object] = {"$type": _canonical_type_name(value)}
if isinstance(value, _OPERATION_TYPES):
payload["opcode"] = value.opcode.value
for item in fields(value):
payload[item.name] = _canonical_value(getattr(value, item.name))
return payload
raise TypeError(
"Canonical IR JSON accepts only schema dataclasses, tuples, enums, and "
f"scalar values; received {type(value).__name__}."
)
def canonical_json_bytes(value: object) -> bytes:
"""Return the exact platform-independent canonical JSON byte sequence."""
return json.dumps(
_canonical_value(value),
ensure_ascii=True,
allow_nan=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def native_artifact_key(
program: SystemIR,
build: IRNativeBuildIdentity,
) -> str:
"""Build cache key; target details never contaminate the program hash."""
if build.abi_version != NATIVE_NUMERIC_ABI_VERSION:
raise ValueError(
"Native build ABI does not match NATIVE_NUMERIC_ABI_VERSION."
)
for field_name, value in (
("target_triple", build.target_triple),
("compiler_id", build.compiler_id),
("compiler_version", build.compiler_version),
("floating_point_policy", build.floating_point_policy),
):
if not value:
raise ValueError(f"Native build {field_name} must not be empty.")
if any(not flag for flag in build.compile_flags):
raise ValueError("Native build flags must not contain empty entries.")
if (
len(build.kernel_library_signature) != 64
or any(
character not in "0123456789abcdef"
for character in build.kernel_library_signature
)
):
raise ValueError(
"Native kernel library signature must be lowercase SHA-256 hex."
)
payload = _IRNativeArtifactKeyInput(program.structural_signature, build)
return sha256(canonical_json_bytes(payload)).hexdigest()
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
# C 语言数值内核实施计划与可行性评估
> 文档状态:待评审实施方案
> 文档状态:历史阶段方案;当前默认 C 且已退役 Python 数值实现,见 [退役记录](Python数值实现退役记录.md)。
> 建立日期:2026-09-02
> 补充日期:2026-09-09;第 10 节结合最新实测,细化 XML → 系统专用 C → 原生 EXE 路线。前九节保留原阶段方案与历史评估;本次仅补充设计,未完成生产内核替换。
> 适用分支:`model-development`
@@ -140,6 +140,8 @@ SIMULATION_NUMERIC_ENGINE=python|native|shadow|auto
#### C-01 完整数值 IR schema v2
状态:**已完成 v2.0 schema、规范、系统编译器、fail-closed 语义校验和复杂模型结构回归。** 当前结果明确为 `reference_only`,C-02/C-03 尚未完成,默认 Python 求解路径没有切换。
完整 IR 必须描述共享数值阶段,以及 RHS、Event、Jacobian 和输出四类独立的按需入口。它们可以复用同一套槽位和依赖信息,但不能被误实现成“每次 RHS 都顺序计算 Event、Jacobian 和输出”。共享 primal 计划为:
```text
@@ -170,11 +172,12 @@ IR 至少描述:
- stream 图、SCC、物性状态包、外层固定点和事务恢复集合;
- event ID、左/右状态、reset、Jacobian 失效和模式计划;
- 固定的 CSR Jacobian 结构和 output projection;
- IR schema、native ABI、组件模型/实现、介质、编译器、dtype、平台和构建选项组成的结构签名。
- 完整 IR 内容(含组件模型/实现、介质、dtype、参数和执行计划)组成跨平台内容签名;
- native artifact key 再把内容签名与 native ABI、目标平台、编译器、构建选项、浮点策略和 kernel 库签名组合。
原生兼容 program 中不得存在 Python callback。schema v1 保留为参考适配器,不直接扩展成生产 ABI。
完成标准:同一模型重复编译得到字节级稳定的结构和签名;所有索引及读写集合可静态校验;缺少能力时明确拒绝编译。
完成标准:同一模型重复编译得到字节级稳定的结构和签名;所有索引及读写集合可静态校验;结构可完整描述但缺少原生能力时明确标为 `reference_only`,原生加载器必须拒绝接管。
#### C-02 纯数值组件合同
@@ -398,8 +401,8 @@ Numba 可以在完整数组 IR 后用于 1–2 周的架构验证,但不作为
第一批只做 P0,不直接开始大规模 C 编码:
1. **已完成:** 固定当前权威输入和回归证据的 `LF` 检出规则,重新规范化 Windows 工作树,并增加 Windows/Linux 字节合同测试;未重建语义未变的 golden。
2. 生成当前模型的组件类型、槽位、阶段、副作用和事件能力矩阵。
3. 将 schema v2 写成独立规范,先冻结 RHS 阶段、错误、事务、事件和结构签名。
2. **已完成(C-01 结构层):** 生成当前模型的组件类型、槽位、阶段、副作用和事件能力描述;纯数值 kernel 能力矩阵的实现细节继续归入 C-02。
3. **已完成:** 将 schema v2 写成独立规范,并冻结 RHS/Event/Jacobian/Outputs 入口、错误能力、事务、事件、Jacobian 和内容签名。
4. 建立对象引擎与扁平 IR 的逐阶段 Shadow runner。
5. 用一条完整机械—气动—管路—接触支路完成 Python 参考闭环。
6. 评审通过后,再建立最小 C ABI 和纵向切片。
@@ -24,3 +24,18 @@
当前版本仍仅提供 Windows x64 原生构建。此前已发现大型 `test-mql-8` 的 CVODE BDF 完整仿真过慢;本次删除旧实现不解决该问题,不能据组件覆盖或短时测试宣称其长时仿真已通过。详见 [组件覆盖记录](C内核组件库覆盖记录.md)。
本地生成的 EXE、DLL、计时日志在 `/test/`,不提交编译产物。源码和冻结基准进入 Git,可重新生成。
## 远端 IR 分支集成
合并保留 `system-optimization` 的 IR v2 schema、Python 数据结构、内容签名和静态校验器。旧 `compile_system_ir(GenericFluidSystem)` 导出器直接依赖已删除的压力流量、机械和闭合执行计划,因此停用并返回明确的迁移提示。当前 C 生成接口是 `native_codegen.compiler.compile_native_program(network)`,尚不输出独立的 System IR v2 执行计划。
从远端提交 `ce353d2` 固化机械、132 状态 MQL、74 状态历史模型的 reference-only IR,保存在 `tests/data/system-ir-v2-reference.json.gz`。保留并迁移了 20 项 IR 结构、签名、索引、阶段和错误校验测试,全部通过。用于捕获参考结果的临时旧代码副本已删除。
CI 同步移除旧 Python 基准命令:Linux 验证元数据和 IR 合同,Windows 配置 C 工具链后运行原生回归。CI 配置已更新,本地验证与远端 CI 的实际运行结果分别记录,不把本地通过当成远端 CI 已通过。
## 最终本地验证
- 后端完整发现 280 项测试,1 项 Linux 锁定环境版本校验按配置跳过。数值、XML、IR 等检查通过;心跳测试改为等待消费确认后,对应 11 项 API 回归通过,消除依赖几毫秒固定等待的时序假设。
- 前端 TypeScript 检查与 Vite 生产构建通过。
- Chrome 无界面网页测试 3 项通过:C 结果、缺少旧诊断字段的结果、历史 Python 结果均可显示并在刷新后恢复。
- `skill-test` 完整 10 s 原生求解通过,计时与设置见上文。
+264
View File
@@ -0,0 +1,264 @@
# 全系统数值中间表示(System Numeric IR)规范 v2.0
> 2026-09-10 集成说明:保留本规范、schema、签名与静态校验。依赖旧 `GenericFluidSystem` 的 Python 对象导出器已随旧数值内核退役,调用 `compile_system_ir()` 会明确报错。当前可执行 C 路径使用 `native_codegen.compiler.compile_native_program(network)`,尚未按本 v2 结构输出执行计划;不能把当前 C 支持等同于本规范的 native 能力声明。原导出结果固化在 `tests/data/system-ir-v2-reference.json.gz`,继续验证 schema 与复杂计划的错误检查。
状态:C-01 已实现并冻结 v2.0 数据合同;当前编译结果为 `reference_only`,原生 kernel 与执行器属于 C-02/C-03 及后续工作。
适用范围:后端 `GenericFluidSystem` 编译后的整个仿真系统。
机器可读定义:`schemas/system-numeric-ir-v2.schema.json`。
## 1. 定位与边界
System Numeric IR(以下简称 IR)描述“一个已经解析并编译好的系统,数值求解时需要哪些数据、按什么关系执行”。它是系统级合同,不是单个部件的文件。
```text
XML / 建模 JSON
→ 模型解析和连接检查
→ GenericFluidSystem 对象图
→ System IR v2.0
→ 参考执行器 / 未来原生执行器
```
XML 保存用户建立了哪些元件、参数和连线;IR 在此基础上补充求解器真正需要的槽位编号、状态降维、方程块、执行阶段、闭合范围、事务回滚、事件、Jacobian 和输出投影。因此二者看起来相似,但用途和层级不同。
本版本只完成“完整、确定、可校验的数据合同”和从现有系统生成该合同的编译器。它没有替换当前默认 Python 求解路径,不改变现有仿真结果。IR 中禁止保存 Python 函数、闭包、模型对象、对象地址和运行期临时状态。
## 2. 三个权威来源
三份实现共同定义 v2.0:
- `app/simulation/ir/schema.py`:Python 不可变数据类型、枚举、规范序列化和签名算法;
- `schemas/system-numeric-ir-v2.schema.json`:跨语言 JSON 结构合同;
- `app/simulation/ir/validation.py`:仅靠 JSON Schema 无法表达的引用、覆盖、拓扑和数值语义校验。
生产者必须同时满足机器 Schema 和语义校验。字段有增删时必须同步修改三处以及合同测试,不能只更新文档。
## 3. 版本、兼容性和严格读取
版本不是顶层整数,而是 `version` 对象:
```json
{
"$type": "schema_version",
"schema_id": "system-numeric-ir",
"major": 2,
"minor": 0
}
```
- `major` 改变表示不兼容的字段或执行语义变化;读取方必须拒绝未知主版本。
- `minor` 用于同一主版本内向前演进;当前读取方拒绝负数和高于自身能力的次版本。
- JSON Schema 对所有合同对象使用 `additionalProperties: false`,v2.0 读取方不会静默忽略未知字段或枚举值。
- 原生二进制 ABI 不写入 `SystemIR`,而由独立的 `IRNativeBuildIdentity.abi_version` 管理,当前值为 `1`。
- 压力流量中的 `causal_plans.source_schema_version == 1` 只表示其来源是既有 causal IR v1;它不是完整系统 IR 的版本,也不能携带 v1 的 Python 回调。
## 4. 线格式和顶层结构
每个 dataclass 序列化后都带有稳定的 `$type`;操作对象还带有 `opcode`。顶层 `$type` 为 `system_ir`,其字段完整集合如下:
| 字段 | 含义 |
| --- | --- |
| `version` | IR schema 身份与版本 |
| `model_id` / `model_version` | 输入系统的稳定身份和调用方提供的模型版本 |
| `compiler_id` / `compiler_version` | 产生 IR 的编译器身份,当前为 `generic-fluid-system` / `2.0.0` |
| `numeric_dtype` | 主数值类型,v2.0 只接受 `float64` |
| `buffers` / `values` | 连续缓冲区及每一个数值槽位的元数据 |
| `kernels` / `components` | kernel 声明与元件实例绑定 |
| `mediums` | 介质实现、介质常量和使用该介质的元件 |
| `ports` / `connections` | 端口变量和系统拓扑 |
| `state_reducer` | 求解器状态与元件局部状态/导数的线性映射 |
| `causal_plans` / `pressure_flow` | 因果子计划和完整压力流量方程计划 |
| `stream_plans` / `thermofluid` | stream SCC/DAG 与热流体外层闭合计划 |
| `stages` / `blocks` | 无回调操作阶段与复合/迭代执行块 |
| `entry_points` | `rhs`、`events`、`jacobian`、`outputs` 四个入口 |
| `transaction` | 试算快照、流量恢复和参考缓存诊断 |
| `modes` / `events` | 离散模式、根函数、reset 与缓存失效 |
| `jacobian` | 固定 CSR 结构、着色和局部有限差分计划 |
| `outputs` | 结果元数据和投影 |
| `capabilities` | 系统和元件的原生可执行能力及缺口 |
| `required_features` | 读取/执行该程序必须理解的功能 ID |
所有列表的顺序都是合同的一部分。引用统一采用数组索引或稳定 ID,不能依赖哈希表遍历顺序。
## 5. 规范序列化与内容签名
`canonical_json_bytes()` 是跨平台唯一线表示:
- UTF-8,ASCII 转义开启,JSON 键排序,无无意义空白;
- 字符串先做 Unicode NFC 规范化;
- tuple 写成 JSON array,不接受 list、dict、set 或任意对象;
- 浮点数写成 IEEE-754 binary64 大端十六进制对象,例如 `{"$float64":"3ff0000000000000"}`;
- `-0.0` 统一为 `+0.0`,NaN 和正负无穷直接拒绝;
- 枚举写成规范字符串,整数和布尔值保持其 JSON 类型。
`SystemIR.structural_signature` 是上述完整 `SystemIR` 内容字节的 SHA-256 小写十六进制值。它准确回答“这份 IR 内容是否完全相同”,包含参数值和所有计划,因此不声称不同表达形式的数学系统会得到同一签名。操作系统、机器路径、构建时间、编译器和 native flags 不进入这个签名。
## 6. 缓冲区、槽位和值
槽位引用的格式为 `{"$type":"slot_ref","buffer":"...","index":N}`。v2.0 恰好声明以下 16 类缓冲区,每类一次:
| dtype | 缓冲区 |
| --- | --- |
| `float64` | `time`、`state_input`、`derivative_output`、`local_state`、`local_derivative`、`algebraic`、`signal`、`parameter`、`constant`、`work_float`、`event_output`、`jacobian_value`、`result_output`、`runtime_input` |
| `int32` | `mode`、`work_int` |
`time` 的长度必须为 1;`int32` 初值必须在有符号 32 位范围内。每个缓冲区内索引为 `[0, size)`,而且每一个实际槽位必须恰好有一个 `IRValueSpec`。值描述包含稳定 ID、语义、角色、物理量、单位、缩放、可选上下界和可选所属元件。缩放必须为正有限数,边界必须有序且有限。
缓冲区是执行器唯一的数值寻址合同。名称用于诊断,不允许执行器重新用名称查找取代槽位访问。
## 7. Kernel 与元件绑定
`IRKernelSpec` 声明模型类型、模型版本、实现版本、能力、支持的 phase 以及参数/状态/mode/workspace 数量。phase 枚举为:
`primal`、`residual`、`derivative`、`property`、`event`、`reset`、`jacobian`、`output`。
phase 在 C-01 中只是稳定的功能标签。不同模型在同一 phase 下可能有不同输入输出数量,所以不能在 phase 上填写虚假的统一 arity。当前每个 `IRKernelCallOperation` 自身的有序 `read_slots`、`write_slots` 和 `equation_indices` 才是该次调用的权威依赖合同。C-02 将在此基础上冻结每个 `kernel_id + phase` 的纯数值调用签名并验证所有调用实例一致。
`IRComponentInstance` 把一个元件实例绑定到 kernel,并明确列出参数、局部状态、局部导数、mode、端口、输出和两类 workspace。绑定数量必须与 kernel 声明一致;端口和输出必须与其反向所属关系精确一致。
当前编译器把所有既有 Python kernel 标记为 `reference_only`。为便于结构审计,reference kernel 调用声明了保守读集合:可能多读,但不能漏掉模型对象当前可见的数值输入。Python 内部隐藏缓存仍不是原生槽位,因而任何含这类依赖的程序都不得宣称 `native`。
## 8. 介质、端口和连接
介质记录稳定 `medium_id`、名称、实现及其版本、常量槽位和使用它的元件索引。介质参数必须位于 `constant` 缓冲区。
端口分为 `physical` 与 `signal`:
- 物理端口必须声明正流方向,当前统一为 `intoComponent`;
- signal 端口不得声明物理流向;
- 变量角色与连接规则固定对应:`effort → equal`、`flow → sumToZero`、`stream → streamMix`、`signal → directed`。
连接必须引用两个已声明、不同、同 kind/同 domain 的端口,并精确覆盖两个端口的全部同名变量合同。物理端口和 signal 输入最多被一条连接占用;signal 输出允许扇出到多个输入。禁止重复端点对和悬空索引。
## 9. 状态降维与导数汇总
`IRStateReducer` 使求解器的 `state_input` 与各元件 `local_state` 分离。`state_reducer.initial_state` 是状态初值的语义描述,必须与 `state_input` 缓冲区的初值逐项完全相同,避免消费者面对两个不同初值:
- `state_scatter` 用 CSR 矩阵把求解器状态散射到有序局部状态槽位;
- `derivative_gather` 把有序局部导数汇总为 `derivative_output`;
- `initial_state` 与 `absolute_tolerances` 按求解器状态顺序定义。
这能显式表达共享机械坐标和气动储能状态的降维关系。例如同一气动储能状态可以按体积权重散射到多个局部状态,而不是由执行器临时按对象身份猜测。两个矩阵必须满足 CSR 不变量、维度和值数量合同,并覆盖全部组件状态/导数绑定。
## 10. 压力—流量计划与因果元数据
`IRPressureFlowPlan` 包含:
- `unknowns`:未知量的元件、端口、变量角色、槽位、缩放和边界;
- `equations`:元件或连接拥有的方程、关系、涉及槽位、残差槽位和缩放;
- `blocks`:未知量/方程的方块分解及每块 Jacobian CSR 结构;
- `scopes`:全网、敏感物理岛或方程块作用域及求解限制;
- `global_scope_index` 和 `secondary_scope_indices`:第一次全网求解与后续局部重算范围;
- `pressure_lower_bound`:全局压力下界。
全局 scope 必须覆盖完整网络;每个 scope 的未知量和方程必须等于它包含的 blocks 之并集;secondary scope 唯一且不能包含 global scope。每条方程必须拥有唯一的 residual 槽位,防止两个残差互相覆盖。`sparse_pattern_trusted=false` 时必须给出回退原因,可信结构则不得携带回退原因。
`IRCausalPlan` 保存当前 causal IR v1 编译得到的无回调元数据,包括作用域、规范/兼容/重置槽位、外部 effort、effort 阶段和 flow 阶段。它只作为 v2 压力流量计划的一部分,不代替完整系统计划。
## 11. 操作、阶段、执行块和四个入口
v2.0 的无回调 opcode 为:
`fill`、`copy`、`scatter`、`linear_combination`、`state_map`、`kernel_call`、`effort_broadcast`、`flow_assign`、`check_finite`。
`IRStage` 给出 stage kind、操作序列以及声明的读/写集合;声明集合必须与操作读写并集完全一致。`IRExecutionBlock` 可以按顺序引用 stage 或其他 block;引用图必须无环。`fixed_point` 和 `stream_scc` block 必须声明监控槽位、绝对/相对容差、最大迭代、松弛、回滚槽位和失败策略,其他 block 禁止携带收敛合同。
四个入口必须恰好各一个,且入口输入统一按 `time`、完整 `state_input`、完整 `runtime_input` 排列:
| 入口 | 必须到达的结果阶段 | 精确输出缓冲区 | 不允许夹带 |
| --- | --- | --- | --- |
| `rhs` | `derivative_reduce` | 全部 `derivative_output` | event、Jacobian、output、reset |
| `events` | `event` | 按事件顺序的全部 `event_output` 根槽位 | derivative reduce、Jacobian、output、reset |
| `jacobian` | `jacobian` | 按 CSR 顺序的全部 `jacobian_value` | event、output、reset |
| `outputs` | `output` | 按结果顺序的全部 `result_output` | event、Jacobian、reset |
入口可以复用前置 primal 阶段,但不能把四个入口合并成“每次 RHS 都把事件、Jacobian 和全部输出计算一遍”。入口执行不得写入 `state_input`、`parameter`、`constant` 或 `mode` 等持久输入。
## 12. Stream、热流体闭合和事务
`IRStreamPlan` 显式列出 stream 节点、强连通分量(SCC)、SCC 间缩点 DAG 和拓扑顺序。每个 SCC 对应一个 `stream_scc` block,循环只在 SCC 内迭代;监控槽位必须覆盖该 SCC 节点。
`IRThermofluidPlan` 覆盖全部物理端口,关联 stream plan、全局元件集合、敏感元件、secondary 压力 scope、最大迭代和流量相对容差。是否使用保守全网求解及原因必须成对出现,避免执行器静默扩大作用域。
`IRTransactionPlan` 冻结一次试探计算需要快照和恢复的端口变量,并单列实际活动气动端口上的 `m_flow`。当前目标系统中的这部分数量为 232;机械模型对象里没有作为活动端口变量出现的隐藏 `m_flow` 字段不会被误算进该集合。所有失败试算必须恢复快照;正常成功返回即为隐式提交,不另设可被误排序的 commit opcode。
`cache_component_indices`、`cache_attribute_ids` 和 `diagnostic_owner_ids` 只记录当前 Python 参考路径中仍需关注的隐藏副作用,供 C-02/C-03 清除和 Shadow 诊断;它们不是原生内存布局。存在 opaque Python cache 属性的系统不能标记为 `native`。
## 13. 模式、事件和 Reset
每个 `IRModeSpec` 记录 int32 mode 槽位、所属元件、合法值及初值。模式槽位必须全部且只被一个 mode 说明,组件 mode 绑定与 owner 关系必须双向覆盖。
每个 `IREventSpec` 记录稳定事件 ID、事件类型、owner、根槽位、触发方向、终止性、优先级、mode guard、reset steps、失效缓存种类以及是否重启积分器。reset 只能引用 `reset` stage,事件根槽位必须精确覆盖 `event_output` 缓冲区。
当前 IR 已能表达现有元件暴露的事件和模式结构;仍隐藏在 Python 信号求解或机械密集输出逻辑中的行为属于 `reference_only` 能力缺口,必须在 C-02/C-08 显式化后才能原生执行。
## 14. Jacobian 合同
`IRJacobianPlan` 包含固定 CSR pattern、与非零项一一对应的 `value_slots`、颜色组、填充值步骤、解析 value 索引和局部有限差分列。
CSR 必须满足:`row_pointers` 长度为行数加一、首项为 0、单调不减、末项等于非零项数量;每行列索引递增、唯一且在范围内。颜色组中的列不能共享同一潜在非零行,列不能重复着色。解析项和有限差分项不得重复或越界;每个有限差分列只能填写该列在 CSR 中确实存在的 value 索引,步长必须为正有限数。Jacobian 入口的执行步骤必须与 `fill_steps` 完全一致。
结构可以保守地多报潜在非零项,但不能漏报可能依赖。结构、模式布局或 kernel 实现改变会自然改变整份 IR 内容签名。
## 15. 输出合同
每个 `IROutputSpec` 包含稳定 output ID、所属元件、scope、可选端口名、内部名、显示标签、类别、物理量、单位、局部顺序、来源槽位、结果槽位以及线性 scale/offset。
`output_id` 和 `result_output` 槽位在全系统唯一。`order` 只在 `(component_index, scope, port_name)` 内排序,因此不同元件出现相同 `order` 是合法的;全局最终列顺序由 `outputs` 数组顺序确定。组件的 `output_indices` 必须精确反向覆盖其所有输出。
## 16. 能力报告与拒绝规则
能力级别只有:
- `native`:所有 kernel phase、状态、事件、事务和缓存都满足原生合同;
- `reference_only`:数学/结构已描述,但至少一个阶段仍依赖 Python 参考实现;
- `unsupported`:当前 IR 无法安全表达或执行,必须带 error 级能力问题。
每个元件必须恰好有一条 capability,列出支持 phase 与缺失 feature。系统为 `native` 时所有元件和 kernels 都必须是 native,且不能依赖 opaque Python cache;系统含任意 reference-only 元件时不能伪装为 native。能力问题具有 code、severity、scope ID 和消息,相同 code/scope 不得重复。
当前 `compile_system_ir()` 的输出明确为 `reference_only`,原因是 C-02 的纯数值 kernel 签名与 C-03 的扁平参考执行器尚未完成。这不是 IR 编译失败,也不允许 native loader 越过能力报告运行。标为 native 的系统还必须覆盖所有实际调用 phase,并且不得要求 `reference_kernel_dispatch`。
## 17. 原生构建产物键
二进制缓存身份与 IR 内容签名严格分离。`native_artifact_key(program, build)` 对以下信息再次做规范序列化和 SHA-256:
- `program.structural_signature`;
- native ABI 版本;
- target triple;
- 编译器 ID 与版本;
- 有序编译 flags;
- 浮点策略;
- kernel 库 SHA-256 签名。
ABI 必须等于当前支持值,字符串不能为空,flags 不能含空项,kernel 库签名必须为 64 位小写十六进制。这样相同 IR 在 Windows/Linux 上具有相同内容签名,但得到不同且安全的 native artifact key。
## 18. 编译、校验和消费流程
当前公开入口为:
```python
from app.simulation.ir import compile_system_ir, require_valid_system_ir
program = compile_system_ir(system, model_version="...")
require_valid_system_ir(program)
payload = program.canonical_json_bytes()
signature = program.structural_signature
```
`compile_system_ir()` 接收已完成解析和系统构建的 `GenericFluidSystem`,不直接解析 XML。消费者必须先验证,再根据 `capabilities.system_level` 选择参考路径或未来原生路径;不得把“JSON Schema 能读取”误当成“具备 native 执行能力”。
静态校验采用 fail-closed 策略,覆盖:版本与 required feature、全部槽位、数值范围、组件/kernel arity、端口/连接、介质、状态映射、压力流量方程与 scope、阶段读写、block 无环、四入口切片、stream/热流体、事务、mode/event/reset、Jacobian、输出及能力一致性。`require_valid_system_ir()` 聚合错误后拒绝程序。
## 19. C-01 验收边界与后续工作
C-01 的完成标准是:
- 能从当前目标复杂模型和历史 0.81 s 模型生成完整系统级结构;
- 同一系统重复编译得到字节完全相同的 canonical JSON 和签名;
- 换行方式、Python 哈希种子和目标平台不会污染 IR 内容身份;
- 故意破坏引用、覆盖、CSR、事务、入口或能力合同会被拒绝;
- IR 中没有 callback、对象地址或任意 Python 对象;
- 默认 Python 仿真路径保持不变。
C-01 不等于已经拥有可运行的 C 后端。下一步 C-02 要冻结每个 kernel 的纯数值签名、隐藏缓存和错误码;C-03 要用扁平 Python 执行器逐槽 Shadow 对照;完成这两项后,才可以建立 C ABI、原生执行器并逐步把 capability 从 `reference_only` 提升为 `native`。
@@ -13,3 +13,9 @@
- 固化 50 个网络、112 组 Python 参考状态,删除后用独立 EXE 回归;补充无需 NumPy/SciPy 导入的后台与代码生成检查。
- 原生 RK45 完成 `skill-test` 10 s,最大步长 0.001 s、rtol 1e-7,预热后纯求解 0.381517 s;前端生产构建通过。
- 当前生效范围与限制以 `docs/other/Python数值实现退役记录.md` 为准,前面的 Python 参考后端描述属于当日较早阶段。
## 01:19
- 合并 `system-optimization` 的 IR v2 规范、结构与校验器;退役依赖旧 Python 系统对象的 IR 导出器,保留 3 个冻结 IR 示例及 20 项校验回归。
- 后端回归覆盖 280 项(1 项环境锁版本校验按配置跳过);修正心跳测试的毫秒级定时假设后,对应 API 回归通过。前端构建与 3 项 Chrome 结果展示/恢复测试通过。
- 更新 CI 为 Linux 合同校验、Windows C 求解回归;历史基准保留原始输入与哈希,生成的 EXE/DLL 不进入提交。
+71
View File
@@ -0,0 +1,71 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://systemsimulationapp.local/schema/system-numeric-ir-v2.schema.json",
"title": "System Numeric IR v2",
"description": "Schema for the callback-free canonical JSON emitted by app.simulation.ir.schema.SystemIR.canonical_json_bytes().",
"$ref": "#/$defs/system_ir",
"$defs": {
"float64": {"type":"object","additionalProperties":false,"required":["$float64"],"properties":{"$float64":{"type":"string","pattern":"^(?!8000000000000000$)(?!(?:7ff|fff)[0-9a-f]{13}$)[0-9a-f]{16}$"}}},
"str": {"type":"string"},
"int": {"type":"integer"},
"int32": {"type":"integer","minimum":-2147483648,"maximum":2147483647},
"nonnegative_int": {"type":"integer","minimum":0},
"positive_int": {"type":"integer","minimum":1},
"bool": {"type":"boolean"},
"float_or_null": {"anyOf":[{"$ref":"#/$defs/float64"},{"type":"null"}]},
"str_or_null": {"anyOf":[{"type":"string"},{"type":"null"}]},
"slot_ref": {"type":"object","additionalProperties":false,"required":["$type","buffer","index"],"properties":{"$type":{"const":"slot_ref"},"buffer":{"type":"string","enum":["time","state_input","derivative_output","local_state","local_derivative","algebraic","signal","parameter","constant","mode","work_float","work_int","event_output","jacobian_value","result_output","runtime_input"]},"index":{"$ref":"#/$defs/nonnegative_int"}}},
"schema_version": {"type":"object","additionalProperties":false,"required":["$type","schema_id","major","minor"],"properties":{"$type":{"const":"schema_version"},"schema_id":{"const":"system-numeric-ir"},"major":{"const":2},"minor":{"const":0}}},
"buffer": {"type":"object","additionalProperties":false,"required":["$type","kind","dtype","size","initial_float_values","initial_int_values"],"properties":{"$type":{"const":"buffer"},"kind":{"type":"string","enum":["time","state_input","derivative_output","local_state","local_derivative","algebraic","signal","parameter","constant","mode","work_float","work_int","event_output","jacobian_value","result_output","runtime_input"]},"dtype":{"type":"string","enum":["float64","int32"]},"size":{"$ref":"#/$defs/nonnegative_int"},"initial_float_values":{"type":"array","items":{"$ref":"#/$defs/float64"}},"initial_int_values":{"type":"array","items":{"$ref":"#/$defs/int32"}}}},
"value": {"type":"object","additionalProperties":false,"required":["$type","value_id","slot","semantic","role","quantity","unit","scale","lower_bound","upper_bound","owner_component_index"],"properties":{"$type":{"const":"value"},"value_id":{"type":"string"},"slot":{"$ref":"#/$defs/slot_ref"},"semantic":{"type":"string"},"role":{"type":"string"},"quantity":{"type":"string"},"unit":{"type":"string"},"scale":{"$ref":"#/$defs/float64"},"lower_bound":{"$ref":"#/$defs/float_or_null"},"upper_bound":{"$ref":"#/$defs/float_or_null"},"owner_component_index":{"anyOf":[{"$ref":"#/$defs/nonnegative_int"},{"type":"null"}]} }},
"kernel_phase": {"type":"object","additionalProperties":false,"required":["$type","phase"],"properties":{"$type":{"const":"kernel_phase"},"phase":{"type":"string","enum":["primal","residual","derivative","property","event","reset","jacobian","output"]}}},
"kernel": {"type":"object","additionalProperties":false,"required":["$type","kernel_id","model_type","model_version","implementation_version","availability","unavailable_reason","phases","parameter_count","state_count","mode_count","workspace_float_count","workspace_int_count"],"properties":{"$type":{"const":"kernel"},"kernel_id":{"type":"string"},"model_type":{"type":"string"},"model_version":{"type":"string"},"implementation_version":{"type":"string"},"availability":{"type":"string","enum":["native","reference_only"]},"unavailable_reason":{"$ref":"#/$defs/str_or_null"},"phases":{"type":"array","items":{"$ref":"#/$defs/kernel_phase"}},"parameter_count":{"$ref":"#/$defs/nonnegative_int"},"state_count":{"$ref":"#/$defs/nonnegative_int"},"mode_count":{"$ref":"#/$defs/nonnegative_int"},"workspace_float_count":{"$ref":"#/$defs/nonnegative_int"},"workspace_int_count":{"$ref":"#/$defs/nonnegative_int"}}},
"component": {"type":"object","additionalProperties":false,"required":["$type","instance_id","kernel_index","parameter_slots","state_slots","derivative_slots","mode_slots","port_indices","port_slots","output_indices","workspace_float_slots","workspace_int_slots"],"properties":{"$type":{"const":"component"},"instance_id":{"type":"string"},"kernel_index":{"$ref":"#/$defs/nonnegative_int"},"parameter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"state_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"derivative_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"mode_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"port_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"port_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"output_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"workspace_float_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"workspace_int_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"port_variable": {"type":"object","additionalProperties":false,"required":["$type","variable_id","name","role","connection_rule","quantity","unit","result_visible","slot"],"properties":{"$type":{"const":"port_variable"},"variable_id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string","enum":["effort","flow","stream","signal"]},"connection_rule":{"type":"string","enum":["equal","sumToZero","streamMix","directed"]},"quantity":{"type":"string"},"unit":{"type":"string"},"result_visible":{"type":"boolean"},"slot":{"$ref":"#/$defs/slot_ref"}}},
"port": {"type":"object","additionalProperties":false,"required":["$type","port_id","component_index","name","kind","domain","nominal_role","positive_flow_direction","variables"],"properties":{"$type":{"const":"port"},"port_id":{"type":"string"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"name":{"type":"string"},"kind":{"type":"string","enum":["physical","signal"]},"domain":{"type":"string"},"nominal_role":{"type":"string","enum":["inlet","outlet","bidirectional","input","output"]},"positive_flow_direction":{"anyOf":[{"const":"intoComponent"},{"type":"null"}]},"variables":{"type":"array","items":{"$ref":"#/$defs/port_variable"}}}},
"connection_variable": {"type":"object","additionalProperties":false,"required":["$type","name","rule","endpoint_a_slot","endpoint_b_slot"],"properties":{"$type":{"const":"connection_variable"},"name":{"type":"string"},"rule":{"type":"string","enum":["equal","sumToZero","streamMix","directed"]},"endpoint_a_slot":{"$ref":"#/$defs/slot_ref"},"endpoint_b_slot":{"$ref":"#/$defs/slot_ref"}}},
"connection": {"type":"object","additionalProperties":false,"required":["$type","connection_id","kind","domain","endpoint_a_port_index","endpoint_b_port_index","variables"],"properties":{"$type":{"const":"connection"},"connection_id":{"type":"string"},"kind":{"type":"string","enum":["physical","signal"]},"domain":{"type":"string"},"endpoint_a_port_index":{"$ref":"#/$defs/nonnegative_int"},"endpoint_b_port_index":{"$ref":"#/$defs/nonnegative_int"},"variables":{"type":"array","items":{"$ref":"#/$defs/connection_variable"}}}},
"medium": {"type":"object","additionalProperties":false,"required":["$type","medium_id","name","implementation_id","implementation_version","parameter_slots","component_indices"],"properties":{"$type":{"const":"medium"},"medium_id":{"type":"string"},"name":{"type":"string"},"implementation_id":{"type":"string"},"implementation_version":{"type":"string"},"parameter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"csr_pattern": {"type":"object","additionalProperties":false,"required":["$type","row_count","column_count","row_pointers","column_indices"],"properties":{"$type":{"const":"csr_pattern"},"row_count":{"$ref":"#/$defs/nonnegative_int"},"column_count":{"$ref":"#/$defs/nonnegative_int"},"row_pointers":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"column_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"csr_matrix": {"type":"object","additionalProperties":false,"required":["$type","pattern","values"],"properties":{"$type":{"const":"csr_matrix"},"pattern":{"$ref":"#/$defs/csr_pattern"},"values":{"type":"array","items":{"$ref":"#/$defs/float64"}}}},
"state_reducer": {"type":"object","additionalProperties":false,"required":["$type","solver_state_count","local_state_slots","raw_derivative_slots","state_scatter","derivative_gather","initial_state","absolute_tolerances"],"properties":{"$type":{"const":"state_reducer"},"solver_state_count":{"$ref":"#/$defs/nonnegative_int"},"local_state_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"raw_derivative_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"state_scatter":{"$ref":"#/$defs/csr_matrix"},"derivative_gather":{"$ref":"#/$defs/csr_matrix"},"initial_state":{"type":"array","items":{"$ref":"#/$defs/float64"}},"absolute_tolerances":{"type":"array","items":{"$ref":"#/$defs/float64"}}}},
"operation": {"oneOf":[{"$ref":"#/$defs/fill"},{"$ref":"#/$defs/copy"},{"$ref":"#/$defs/scatter"},{"$ref":"#/$defs/linear_combination"},{"$ref":"#/$defs/state_map"},{"$ref":"#/$defs/kernel_call"},{"$ref":"#/$defs/effort_broadcast"},{"$ref":"#/$defs/flow_assign"},{"$ref":"#/$defs/check_finite"}]},
"fill": {"type":"object","additionalProperties":false,"required":["$type","opcode","target_slots","value"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"fill"},"target_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"value":{"$ref":"#/$defs/float64"}}},
"copy": {"type":"object","additionalProperties":false,"required":["$type","opcode","source_slot","target_slot"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"copy"},"source_slot":{"$ref":"#/$defs/slot_ref"},"target_slot":{"$ref":"#/$defs/slot_ref"}}},
"scatter": {"type":"object","additionalProperties":false,"required":["$type","opcode","source_slot","target_slots"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"scatter"},"source_slot":{"$ref":"#/$defs/slot_ref"},"target_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"linear_combination": {"type":"object","additionalProperties":false,"required":["$type","opcode","source_slots","weights","target_slot","bias"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"linear_combination"},"source_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"weights":{"type":"array","items":{"$ref":"#/$defs/float64"}},"target_slot":{"$ref":"#/$defs/slot_ref"},"bias":{"$ref":"#/$defs/float64"}}},
"state_map": {"type":"object","additionalProperties":false,"required":["$type","opcode","map_kind","source_slots","target_slots"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"state_map"},"map_kind":{"type":"string","enum":["scatter","derivative_gather"]},"source_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"target_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"kernel_call": {"type":"object","additionalProperties":false,"required":["$type","opcode","kernel_index","component_index","phase","read_slots","write_slots","equation_indices"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"kernel_call"},"kernel_index":{"$ref":"#/$defs/nonnegative_int"},"component_index":{"anyOf":[{"$ref":"#/$defs/nonnegative_int"},{"type":"null"}]},"phase":{"type":"string","enum":["primal","residual","derivative","property","event","reset","jacobian","output"]},"read_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"write_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"equation_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"effort_broadcast": {"type":"object","additionalProperties":false,"required":["$type","opcode","variable","anchor_slot","residual_slot","result_slot","scatter_slots","equation_id","lower_bound","upper_bound"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"effort_broadcast"},"variable":{"type":"string"},"anchor_slot":{"$ref":"#/$defs/slot_ref"},"residual_slot":{"$ref":"#/$defs/slot_ref"},"result_slot":{"$ref":"#/$defs/slot_ref"},"scatter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"equation_id":{"type":"string"},"lower_bound":{"$ref":"#/$defs/float_or_null"},"upper_bound":{"$ref":"#/$defs/float_or_null"}}},
"flow_assign": {"type":"object","additionalProperties":false,"required":["$type","opcode","value_slot","result_slot","scatter_slots","equation_id"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"flow_assign"},"value_slot":{"$ref":"#/$defs/slot_ref"},"result_slot":{"$ref":"#/$defs/slot_ref"},"scatter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"equation_id":{"type":"string"}}},
"check_finite": {"type":"object","additionalProperties":false,"required":["$type","opcode","slots","error_code"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"check_finite"},"slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"error_code":{"type":"string"}}},
"stage": {"type":"object","additionalProperties":false,"required":["$type","stage_id","kind","operations","declared_read_slots","declared_write_slots"],"properties":{"$type":{"const":"stage"},"stage_id":{"type":"string"},"kind":{"type":"string","enum":["state_reduce","signal","mechanical_equivalence","dynamic_volume","property","pressure_flow","stream","temperature_reference","thermofluid_fixed_point","mechanical_acceleration","derivative_reduce","event","jacobian","output","reset"]},"operations":{"type":"array","items":{"$ref":"#/$defs/operation"}},"declared_read_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"declared_write_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"step_ref": {"type":"object","additionalProperties":false,"required":["$type","kind","index"],"properties":{"$type":{"const":"step_ref"},"kind":{"type":"string","enum":["stage","block"]},"index":{"$ref":"#/$defs/nonnegative_int"}}},
"convergence": {"type":"object","additionalProperties":false,"required":["$type","monitor_slots","absolute_tolerance","relative_tolerance","max_iterations","relaxation","rollback_slots","failure_policy"],"properties":{"$type":{"const":"convergence"},"monitor_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"absolute_tolerance":{"$ref":"#/$defs/float64"},"relative_tolerance":{"$ref":"#/$defs/float64"},"max_iterations":{"$ref":"#/$defs/positive_int"},"relaxation":{"$ref":"#/$defs/float64"},"rollback_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"failure_policy":{"type":"string","enum":["fail","retry_smaller_step"]}}},
"block": {"type":"object","additionalProperties":false,"required":["$type","block_id","kind","steps","convergence"],"properties":{"$type":{"const":"block"},"block_id":{"type":"string"},"kind":{"type":"string","enum":["sequence","fixed_point","stream_scc"]},"steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"convergence":{"anyOf":[{"$ref":"#/$defs/convergence"},{"type":"null"}]} }},
"entry_point": {"type":"object","additionalProperties":false,"required":["$type","kind","steps","input_slots","output_slots"],"properties":{"$type":{"const":"entry_point"},"kind":{"type":"string","enum":["rhs","events","jacobian","outputs"]},"steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"input_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"output_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"causal_effort_stage": {"type":"object","additionalProperties":false,"required":["$type","variable","stage_index"],"properties":{"$type":{"const":"causal_effort_stage"},"variable":{"type":"string"},"stage_index":{"$ref":"#/$defs/nonnegative_int"}}},
"causal_plan": {"type":"object","additionalProperties":false,"required":["$type","plan_id","scope_component_indices","source_schema_version","source_structural_signature","fallback_reason","canonical_slots","compatibility_slots","reset_slots","external_effort_slots","effort_stages","flow_stage_indices"],"properties":{"$type":{"const":"causal_plan"},"plan_id":{"type":"string"},"scope_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"source_schema_version":{"$ref":"#/$defs/nonnegative_int"},"source_structural_signature":{"$ref":"#/$defs/str_or_null"},"fallback_reason":{"$ref":"#/$defs/str_or_null"},"canonical_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"compatibility_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"reset_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"external_effort_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"effort_stages":{"type":"array","items":{"$ref":"#/$defs/causal_effort_stage"}},"flow_stage_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"algebraic_unknown": {"type":"object","additionalProperties":false,"required":["$type","unknown_id","component_index","port_index","variable","role","slot","scale","lower_bound","upper_bound"],"properties":{"$type":{"const":"algebraic_unknown"},"unknown_id":{"type":"string"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"port_index":{"$ref":"#/$defs/nonnegative_int"},"variable":{"type":"string"},"role":{"type":"string","enum":["effort","flow","stream","signal"]},"slot":{"$ref":"#/$defs/slot_ref"},"scale":{"$ref":"#/$defs/float64"},"lower_bound":{"$ref":"#/$defs/float_or_null"},"upper_bound":{"$ref":"#/$defs/float_or_null"}}},
"pressure_flow_equation": {"type":"object","additionalProperties":false,"required":["$type","equation_id","owner","owner_index","relation","role","variable_slots","residual_slot","scale"],"properties":{"$type":{"const":"pressure_flow_equation"},"equation_id":{"type":"string"},"owner":{"type":"string","enum":["component","connection"]},"owner_index":{"$ref":"#/$defs/nonnegative_int"},"relation":{"type":"string","enum":["equal","sumToZero","constitutive","state"]},"role":{"anyOf":[{"type":"string","enum":["effort","flow","stream","signal"]},{"type":"null"}]},"variable_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"residual_slot":{"$ref":"#/$defs/slot_ref"},"scale":{"$ref":"#/$defs/float64"}}},
"pressure_flow_block": {"type":"object","additionalProperties":false,"required":["$type","block_id","unknown_indices","equation_indices","jacobian_pattern"],"properties":{"$type":{"const":"pressure_flow_block"},"block_id":{"type":"string"},"unknown_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"equation_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"jacobian_pattern":{"$ref":"#/$defs/csr_pattern"}}},
"pressure_flow_scope": {"type":"object","additionalProperties":false,"required":["$type","scope_id","kind","component_indices","unknown_indices","equation_indices","block_indices","causal_plan_index","residual_tolerance","max_evaluations","sparse_pattern_trusted","sparse_fallback_reason"],"properties":{"$type":{"const":"pressure_flow_scope"},"scope_id":{"type":"string"},"kind":{"type":"string","enum":["network","physical_island","equation_block"]},"component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"unknown_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"equation_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"block_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"causal_plan_index":{"anyOf":[{"$ref":"#/$defs/nonnegative_int"},{"type":"null"}]},"residual_tolerance":{"$ref":"#/$defs/float64"},"max_evaluations":{"$ref":"#/$defs/positive_int"},"sparse_pattern_trusted":{"type":"boolean"},"sparse_fallback_reason":{"$ref":"#/$defs/str_or_null"}}},
"pressure_flow_plan": {"type":"object","additionalProperties":false,"required":["$type","unknowns","equations","blocks","scopes","global_scope_index","secondary_scope_indices","pressure_lower_bound"],"properties":{"$type":{"const":"pressure_flow_plan"},"unknowns":{"type":"array","items":{"$ref":"#/$defs/algebraic_unknown"}},"equations":{"type":"array","items":{"$ref":"#/$defs/pressure_flow_equation"}},"blocks":{"type":"array","items":{"$ref":"#/$defs/pressure_flow_block"}},"scopes":{"type":"array","items":{"$ref":"#/$defs/pressure_flow_scope"}},"global_scope_index":{"$ref":"#/$defs/nonnegative_int"},"secondary_scope_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"pressure_lower_bound":{"$ref":"#/$defs/float64"}}},
"stream_scc": {"type":"object","additionalProperties":false,"required":["$type","scc_id","node_slots","block_index"],"properties":{"$type":{"const":"stream_scc"},"scc_id":{"type":"string"},"node_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"block_index":{"$ref":"#/$defs/nonnegative_int"}}},
"stream_edge": {"type":"object","additionalProperties":false,"required":["$type","source_scc_index","target_scc_index"],"properties":{"$type":{"const":"stream_edge"},"source_scc_index":{"$ref":"#/$defs/nonnegative_int"},"target_scc_index":{"$ref":"#/$defs/nonnegative_int"}}},
"stream_plan": {"type":"object","additionalProperties":false,"required":["$type","plan_id","node_slots","strongly_connected_components","condensed_edges","topological_scc_indices"],"properties":{"$type":{"const":"stream_plan"},"plan_id":{"type":"string"},"node_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"strongly_connected_components":{"type":"array","items":{"$ref":"#/$defs/stream_scc"}},"condensed_edges":{"type":"array","items":{"$ref":"#/$defs/stream_edge"}},"topological_scc_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"thermofluid_plan": {"type":"object","additionalProperties":false,"required":["$type","physical_port_indices","global_component_indices","stream_plan_index","secondary_pressure_scope_indices","sensitive_component_indices","maximum_iterations","flow_relative_tolerance","uses_conservative_global_solver","conservative_fallback_reason"],"properties":{"$type":{"const":"thermofluid_plan"},"physical_port_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"global_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"stream_plan_index":{"$ref":"#/$defs/nonnegative_int"},"secondary_pressure_scope_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"sensitive_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"maximum_iterations":{"$ref":"#/$defs/positive_int"},"flow_relative_tolerance":{"$ref":"#/$defs/float64"},"uses_conservative_global_solver":{"type":"boolean"},"conservative_fallback_reason":{"$ref":"#/$defs/str_or_null"}}},
"transaction_plan": {"type":"object","additionalProperties":false,"required":["$type","snapshot_slots","flow_slots","cache_component_indices","cache_attribute_ids","diagnostic_owner_ids","restores_on_recoverable_failure","restores_on_fatal_failure"],"properties":{"$type":{"const":"transaction_plan"},"snapshot_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"flow_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"cache_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"cache_attribute_ids":{"type":"array","items":{"type":"string"}},"diagnostic_owner_ids":{"type":"array","items":{"type":"string"}},"restores_on_recoverable_failure":{"type":"boolean"},"restores_on_fatal_failure":{"type":"boolean"}}},
"mode_value": {"type":"object","additionalProperties":false,"required":["$type","value","name"],"properties":{"$type":{"const":"mode_value"},"value":{"$ref":"#/$defs/int32"},"name":{"type":"string"}}},
"mode": {"type":"object","additionalProperties":false,"required":["$type","mode_id","slot","owner_component_indices","values","initial_value"],"properties":{"$type":{"const":"mode"},"mode_id":{"type":"string"},"slot":{"$ref":"#/$defs/slot_ref"},"owner_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"values":{"type":"array","items":{"$ref":"#/$defs/mode_value"}},"initial_value":{"$ref":"#/$defs/int32"}}},
"finite_difference_column": {"type":"object","additionalProperties":false,"required":["$type","column_index","value_indices","relative_step"],"properties":{"$type":{"const":"finite_difference_column"},"column_index":{"$ref":"#/$defs/nonnegative_int"},"value_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"relative_step":{"$ref":"#/$defs/float64"}}},
"jacobian_plan": {"type":"object","additionalProperties":false,"required":["$type","pattern","value_slots","color_groups","fill_steps","analytic_value_indices","local_finite_difference_columns"],"properties":{"$type":{"const":"jacobian_plan"},"pattern":{"$ref":"#/$defs/csr_pattern"},"value_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"color_groups":{"type":"array","items":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}},"fill_steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"analytic_value_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"local_finite_difference_columns":{"type":"array","items":{"$ref":"#/$defs/finite_difference_column"}}}},
"mode_guard": {"type":"object","additionalProperties":false,"required":["$type","mode_slot","allowed_values"],"properties":{"$type":{"const":"mode_guard"},"mode_slot":{"$ref":"#/$defs/slot_ref"},"allowed_values":{"type":"array","items":{"$ref":"#/$defs/int32"}}}},
"event": {"type":"object","additionalProperties":false,"required":["$type","event_id","event_kind","owner_component_indices","root_slot","direction","terminal","priority","mode_guards","reset_steps","invalidated_caches","restarts_integrator"],"properties":{"$type":{"const":"event"},"event_id":{"type":"string"},"event_kind":{"type":"string"},"owner_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"root_slot":{"$ref":"#/$defs/slot_ref"},"direction":{"type":"string","enum":["decreasing","any","increasing"]},"terminal":{"type":"boolean"},"priority":{"type":"integer"},"mode_guards":{"type":"array","items":{"$ref":"#/$defs/mode_guard"}},"reset_steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"invalidated_caches":{"type":"array","items":{"type":"string","enum":["property","pressure_flow","stream","jacobian","output"]}},"restarts_integrator":{"type":"boolean"}}},
"output": {"type":"object","additionalProperties":false,"required":["$type","output_id","component_index","scope","port_name","name","label","category","quantity","unit","order","source_slot","output_slot","scale","offset"],"properties":{"$type":{"const":"output"},"output_id":{"type":"string"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"scope":{"type":"string"},"port_name":{"$ref":"#/$defs/str_or_null"},"name":{"type":"string"},"label":{"type":"string"},"category":{"type":"string"},"quantity":{"type":"string"},"unit":{"type":"string"},"order":{"$ref":"#/$defs/nonnegative_int"},"source_slot":{"$ref":"#/$defs/slot_ref"},"output_slot":{"$ref":"#/$defs/slot_ref"},"scale":{"$ref":"#/$defs/float64"},"offset":{"$ref":"#/$defs/float64"}}},
"component_capability": {"type":"object","additionalProperties":false,"required":["$type","component_index","level","supported_phases","missing_features"],"properties":{"$type":{"const":"component_capability"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"level":{"type":"string","enum":["native","reference_only","unsupported"]},"supported_phases":{"type":"array","items":{"type":"string","enum":["primal","residual","derivative","property","event","reset","jacobian","output"]}},"missing_features":{"type":"array","items":{"type":"string"}}}},
"capability_issue": {"type":"object","additionalProperties":false,"required":["$type","code","severity","scope_id","message"],"properties":{"$type":{"const":"capability_issue"},"code":{"type":"string"},"severity":{"type":"string","enum":["info","warning","error"]},"scope_id":{"type":"string"},"message":{"type":"string"}}},
"capability_report": {"type":"object","additionalProperties":false,"required":["$type","system_level","components","issues"],"properties":{"$type":{"const":"capability_report"},"system_level":{"type":"string","enum":["native","reference_only","unsupported"]},"components":{"type":"array","items":{"$ref":"#/$defs/component_capability"}},"issues":{"type":"array","items":{"$ref":"#/$defs/capability_issue"}}}},
"system_ir": {"type":"object","additionalProperties":false,"required":["$type","version","model_id","model_version","compiler_id","compiler_version","numeric_dtype","buffers","values","kernels","components","mediums","ports","connections","state_reducer","causal_plans","pressure_flow","stream_plans","thermofluid","stages","blocks","entry_points","transaction","modes","jacobian","events","outputs","capabilities","required_features"],"properties":{"$type":{"const":"system_ir"},"version":{"$ref":"#/$defs/schema_version"},"model_id":{"type":"string"},"model_version":{"type":"string"},"compiler_id":{"type":"string"},"compiler_version":{"type":"string"},"numeric_dtype":{"const":"float64"},"buffers":{"type":"array","items":{"$ref":"#/$defs/buffer"}},"values":{"type":"array","items":{"$ref":"#/$defs/value"}},"kernels":{"type":"array","items":{"$ref":"#/$defs/kernel"}},"components":{"type":"array","items":{"$ref":"#/$defs/component"}},"mediums":{"type":"array","items":{"$ref":"#/$defs/medium"}},"ports":{"type":"array","items":{"$ref":"#/$defs/port"}},"connections":{"type":"array","items":{"$ref":"#/$defs/connection"}},"state_reducer":{"$ref":"#/$defs/state_reducer"},"causal_plans":{"type":"array","items":{"$ref":"#/$defs/causal_plan"}},"pressure_flow":{"$ref":"#/$defs/pressure_flow_plan"},"stream_plans":{"type":"array","items":{"$ref":"#/$defs/stream_plan"}},"thermofluid":{"$ref":"#/$defs/thermofluid_plan"},"stages":{"type":"array","items":{"$ref":"#/$defs/stage"}},"blocks":{"type":"array","items":{"$ref":"#/$defs/block"}},"entry_points":{"type":"array","items":{"$ref":"#/$defs/entry_point"}},"transaction":{"$ref":"#/$defs/transaction_plan"},"modes":{"type":"array","items":{"$ref":"#/$defs/mode"}},"jacobian":{"$ref":"#/$defs/jacobian_plan"},"events":{"type":"array","items":{"$ref":"#/$defs/event"}},"outputs":{"type":"array","items":{"$ref":"#/$defs/output"}},"capabilities":{"$ref":"#/$defs/capability_report"},"required_features":{"type":"array","items":{"type":"string"}}}}
}
}
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
"""Read the archived IR schema examples without a Python numerical engine."""
from dataclasses import is_dataclass
from enum import Enum
from functools import lru_cache
import gzip
import json
from pathlib import Path
from app.simulation.ir import schema
def _decode(value):
if isinstance(value, list):
return tuple(_decode(item) for item in value)
if isinstance(value, dict):
if 'enum' in value:
cls = getattr(schema, value['enum'])
if not isinstance(cls, type) or not issubclass(cls, Enum):
raise ValueError('Invalid IR enum in fixture')
return cls(value['value'])
cls = getattr(schema, value['type'])
if not isinstance(cls, type) or not is_dataclass(cls):
raise ValueError('Invalid IR record in fixture')
return cls(**{key: _decode(item) for key, item in value['fields'].items()})
return value
@lru_cache(maxsize=1)
def _references():
path = Path(__file__).parent / 'data/system-ir-v2-reference.json.gz'
return json.loads(gzip.decompress(path.read_bytes()))['programs']
def reference_ir(name):
return _decode(_references()[name])
+11 -5
View File
@@ -423,6 +423,8 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
self.assertEqual(snapshot["result"]["status"], "stopped")
def test_streaming_endpoint_keeps_quiet_solver_connection_alive(self) -> None:
heartbeat_observed = threading.Event()
def delayed_simulation(
_xml_bytes,
progress_callback,
@@ -433,8 +435,10 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
self.assertIsNotNone(activity_tracker)
activity_tracker.start_integration(0.0)
for trial_time in (0.0487, 0.0488, 0.0489, 0.0490):
heartbeat_observed.clear()
activity_tracker.record_rhs(trial_time)
time.sleep(0.008)
# Wait for the consumer, independent of machine scheduling.
self.assertTrue(heartbeat_observed.wait(timeout=2.0))
return {
"success": True,
"status": "completed",
@@ -449,10 +453,12 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
side_effect=delayed_simulation,
),
):
events = [
json.loads(line)
for line in simulation_event_stream(b"<System />")
]
events = []
for line in simulation_event_stream(b"<System />"):
event = json.loads(line)
events.append(event)
if event.get("heartbeat"):
heartbeat_observed.set()
heartbeats = [event for event in events if event.get("heartbeat") is True]
self.assertGreaterEqual(len(heartbeats), 1)
+748
View File
@@ -0,0 +1,748 @@
from __future__ import annotations
from dataclasses import fields, is_dataclass, replace
import json
from pathlib import Path
import unittest
from app.main import compile_reactflow_network, compile_system_xml_network
from app.simulation.ir import schema as ir_schema
from app.simulation.ir import (
IRBufferKind,
IRCapabilityLevel,
IRDType,
IREntryPointKind,
IRKernelAvailability,
IRKernelCallOperation,
IRNativeBuildIdentity,
IRStageKind,
IRStepKind,
canonical_json_bytes,
compile_system_ir,
native_artifact_key,
)
from app.simulation.ir.validation import (
SystemIRValidationError,
require_valid_system_ir,
validate_system_ir,
)
from tests.ir_reference import reference_ir
from app.system_xml import validate_system_xml_document
from tests.test_amesim_mechanical_xml import zero_force_mass_project
TARGET_XML = Path("tests/data/test-mql-8.xml")
HISTORICAL_XML = Path("tests/data/test_mql-full-branches-01-04.xml")
MACHINE_SCHEMA = Path("schemas/system-numeric-ir-v2.schema.json")
def _entry_stage_kinds(program, entry_kind: IREntryPointKind) -> set[IRStageKind]:
entry = next(item for item in program.entry_points if item.kind is entry_kind)
result: set[IRStageKind] = set()
visited_blocks: set[int] = set()
def visit(step) -> None:
if step.kind is IRStepKind.STAGE:
result.add(program.stages[step.index].kind)
return
if step.index in visited_blocks:
return
visited_blocks.add(step.index)
for nested in program.blocks[step.index].steps:
visit(nested)
for step in entry.steps:
visit(step)
return result
def _assert_callback_free(test: unittest.TestCase, value: object) -> None:
if is_dataclass(value) and not isinstance(value, type):
for item in fields(value):
_assert_callback_free(test, getattr(value, item.name))
return
if isinstance(value, tuple):
for item in value:
_assert_callback_free(test, item)
return
test.assertFalse(callable(value), type(value).__name__)
test.assertNotIsInstance(value, (dict, list, set))
class SystemNumericIRV2ContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.program = reference_ir('mechanical')
def test_reference_schema_is_statically_valid_and_callback_free(self) -> None:
report = validate_system_ir(self.program)
self.assertTrue(report.valid, report.issues)
self.assertIs(require_valid_system_ir(self.program), self.program)
_assert_callback_free(self, self.program)
with self.assertRaises(TypeError):
canonical_json_bytes(lambda: None)
def test_canonical_bytes_and_signature_are_deterministic(self) -> None:
second = reference_ir('mechanical')
self.assertEqual(
self.program.canonical_json_bytes(),
second.canonical_json_bytes(),
)
self.assertEqual(
self.program.structural_signature,
second.structural_signature,
)
self.assertEqual(len(self.program.structural_signature), 64)
self.assertEqual(
self.program.structural_signature,
self.program.calculate_structural_signature(),
)
decomposed = replace(self.program, model_id="e\u0301")
composed = replace(self.program, model_id="é")
self.assertEqual(
decomposed.canonical_json_bytes(),
composed.canonical_json_bytes(),
)
def test_native_artifact_key_is_separate_from_structural_signature(self) -> None:
windows = IRNativeBuildIdentity(
abi_version=1,
target_triple="x86_64-pc-windows-msvc",
compiler_id="msvc",
compiler_version="19.40",
compile_flags=("/O2", "/fp:precise"),
floating_point_policy="strict",
kernel_library_signature="a" * 64,
)
linux = replace(
windows,
target_triple="x86_64-unknown-linux-gnu",
compiler_id="gcc",
compiler_version="14.2",
compile_flags=("-O2", "-fno-fast-math"),
)
signature = self.program.structural_signature
self.assertNotEqual(
native_artifact_key(self.program, windows),
native_artifact_key(self.program, linux),
)
self.assertEqual(self.program.structural_signature, signature)
with self.assertRaises(ValueError):
native_artifact_key(
self.program,
replace(windows, abi_version=windows.abi_version + 1),
)
def test_four_entry_points_are_independent(self) -> None:
self.assertEqual(
{item.kind for item in self.program.entry_points},
set(IREntryPointKind),
)
rhs_kinds = _entry_stage_kinds(self.program, IREntryPointKind.RHS)
event_kinds = _entry_stage_kinds(
self.program, IREntryPointKind.EVENTS
)
self.assertIn(IRStageKind.DERIVATIVE_REDUCE, rhs_kinds)
self.assertNotIn(IRStageKind.EVENT, rhs_kinds)
self.assertNotIn(IRStageKind.JACOBIAN, rhs_kinds)
self.assertNotIn(IRStageKind.OUTPUT, rhs_kinds)
self.assertIn(IRStageKind.EVENT, event_kinds)
self.assertNotIn(IRStageKind.JACOBIAN, event_kinds)
self.assertNotIn(IRStageKind.OUTPUT, event_kinds)
def test_validator_rejects_a_missing_entry_point(self) -> None:
broken = replace(
self.program,
entry_points=self.program.entry_points[:-1],
)
report = validate_system_ir(broken)
self.assertFalse(report.valid)
self.assertIn(
"ENTRY_POINT_SET_INVALID",
{item.code for item in report.issues},
)
with self.assertRaises(SystemIRValidationError):
require_valid_system_ir(broken)
def test_validator_rejects_entry_contract_and_buffer_dtype_corruption(self) -> None:
rhs_index = next(
index
for index, entry in enumerate(self.program.entry_points)
if entry.kind is IREntryPointKind.RHS
)
events_entry = next(
entry
for entry in self.program.entry_points
if entry.kind is IREntryPointKind.EVENTS
)
corrupted_entries = list(self.program.entry_points)
corrupted_entries[rhs_index] = replace(
corrupted_entries[rhs_index],
steps=events_entry.steps,
output_slots=(),
)
entry_report = validate_system_ir(
replace(self.program, entry_points=tuple(corrupted_entries))
)
self.assertTrue(
{
"ENTRY_POINT_OUTPUT_COVERAGE",
"ENTRY_POINT_FINAL_STAGE_MISSING",
"ENTRY_POINT_STAGE_FORBIDDEN",
}.issubset({item.code for item in entry_report.issues})
)
state_buffer_index = next(
index
for index, buffer in enumerate(self.program.buffers)
if buffer.kind is IRBufferKind.STATE_INPUT
)
corrupted_buffers = list(self.program.buffers)
corrupted_buffers[state_buffer_index] = replace(
corrupted_buffers[state_buffer_index],
dtype=IRDType.INT32,
initial_float_values=(),
initial_int_values=tuple(
0 for _ in range(corrupted_buffers[state_buffer_index].size)
),
)
dtype_report = validate_system_ir(
replace(self.program, buffers=tuple(corrupted_buffers))
)
self.assertIn(
"BUFFER_DTYPE_INVALID",
{item.code for item in dtype_report.issues},
)
def test_validator_rejects_a_component_call_bound_to_another_kernel(self) -> None:
stage_index, operation_index, operation = next(
(stage_index, operation_index, operation)
for stage_index, stage in enumerate(self.program.stages)
for operation_index, operation in enumerate(stage.operations)
if isinstance(operation, IRKernelCallOperation)
and operation.component_index is not None
)
wrong_kernel_index = next(
index
for index in range(len(self.program.kernels))
if index != operation.kernel_index
)
broken_operations = list(self.program.stages[stage_index].operations)
broken_operations[operation_index] = replace(
operation,
kernel_index=wrong_kernel_index,
)
broken_stages = list(self.program.stages)
broken_stages[stage_index] = replace(
broken_stages[stage_index],
operations=tuple(broken_operations),
)
report = validate_system_ir(
replace(self.program, stages=tuple(broken_stages))
)
self.assertFalse(report.valid)
self.assertIn(
"KERNEL_COMPONENT_MISMATCH",
{item.code for item in report.issues},
)
def test_validator_rejects_native_component_with_missing_called_phases(self) -> None:
native_kernels = tuple(
replace(
kernel,
availability=IRKernelAvailability.NATIVE,
unavailable_reason=None,
)
for kernel in self.program.kernels
)
native_capabilities = tuple(
replace(
capability,
level=IRCapabilityLevel.NATIVE,
supported_phases=(),
missing_features=(),
)
for capability in self.program.capabilities.components
)
broken = replace(
self.program,
kernels=native_kernels,
required_features=tuple(
feature
for feature in self.program.required_features
if feature != "reference_kernel_dispatch"
),
transaction=replace(
self.program.transaction,
cache_attribute_ids=(),
),
capabilities=replace(
self.program.capabilities,
system_level=IRCapabilityLevel.NATIVE,
components=native_capabilities,
issues=(),
),
)
report = validate_system_ir(broken)
self.assertFalse(report.valid)
self.assertIn(
"CAPABILITY_NATIVE_PHASE_MISSING",
{item.code for item in report.issues},
)
def test_validator_rejects_runtime_types_that_break_the_wire_schema(self) -> None:
outputs = list(self.program.outputs)
outputs[0] = replace(outputs[0], scale=1)
report = validate_system_ir(
replace(self.program, outputs=tuple(outputs))
)
self.assertFalse(report.valid)
self.assertIn(
"RUNTIME_TYPE_MISMATCH",
{item.code for item in report.issues},
)
def test_validator_propagates_unsupported_component_to_system_level(self) -> None:
capabilities = list(self.program.capabilities.components)
capabilities[0] = replace(
capabilities[0],
level=IRCapabilityLevel.UNSUPPORTED,
)
report = validate_system_ir(
replace(
self.program,
capabilities=replace(
self.program.capabilities,
components=tuple(capabilities),
),
)
)
self.assertFalse(report.valid)
self.assertIn(
"CAPABILITY_LEVEL_CONFLICT",
{item.code for item in report.issues},
)
def test_machine_schema_has_no_dangling_local_references(self) -> None:
schema = json.loads(MACHINE_SCHEMA.read_text(encoding="utf-8"))
definitions = schema["$defs"]
references: list[str] = []
pending: list[object] = [schema]
while pending:
current = pending.pop()
if isinstance(current, dict):
references.extend(
value
for key, value in current.items()
if key == "$ref" and isinstance(value, str)
)
pending.extend(current.values())
elif isinstance(current, list):
pending.extend(current)
self.assertFalse(
{
reference
for reference in references
if reference.startswith("#/$defs/")
and reference.removeprefix("#/$defs/") not in definitions
}
)
payload = json.loads(self.program.canonical_json_bytes())
self.assertEqual(payload["$type"], "system_ir")
self.assertEqual(
set(payload),
set(definitions["system_ir"]["required"]),
)
self.assertEqual(
definitions["kernel_phase"]["required"],
["$type", "phase"],
)
def test_machine_schema_fields_match_every_serialized_dataclass(self) -> None:
definitions = json.loads(
MACHINE_SCHEMA.read_text(encoding="utf-8")
)["$defs"]
skipped_types = {"native_build", "native_artifact_key_input"}
for value_type, canonical_type in ir_schema._CANONICAL_TYPE_NAMES:
if canonical_type in skipped_types:
continue
definition_name = (
value_type.opcode.value
if canonical_type == "operation"
else canonical_type
)
definition = definitions[definition_name]
expected_fields = {"$type", *(item.name for item in fields(value_type))}
if canonical_type == "operation":
expected_fields.add("opcode")
self.assertEqual(
set(definition["required"]),
expected_fields,
definition_name,
)
self.assertEqual(
set(definition["properties"]),
expected_fields,
definition_name,
)
self.assertFalse(
definition["additionalProperties"],
definition_name,
)
class TargetSystemNumericIRV2Tests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.program = reference_ir('target')
def test_target_model_is_fully_described(self) -> None:
program = self.program
self.assertEqual(len(program.components), 156)
self.assertEqual(len(program.ports), 356)
self.assertEqual(len(program.connections), 178)
self.assertEqual(program.state_reducer.solver_state_count, 132)
self.assertEqual(len(program.pressure_flow.unknowns), 776)
self.assertEqual(len(program.pressure_flow.equations), 776)
self.assertEqual(len(program.outputs), 1784)
self.assertEqual(program.jacobian.pattern.row_count, 132)
self.assertEqual(program.jacobian.pattern.column_count, 132)
self.assertGreater(program.jacobian.pattern.nonzero_count, 132)
self.assertLessEqual(len(program.jacobian.color_groups), 132)
self.assertEqual(len(program.causal_plans), 1)
causal = program.causal_plans[0]
self.assertEqual(len(causal.canonical_slots), 452)
self.assertEqual(len(causal.compatibility_slots), 776)
self.assertIsNone(causal.fallback_reason)
self.assertEqual(len(program.modes), 2)
self.assertEqual(len(program.events), 14)
self.assertTrue(program.thermofluid.sensitive_component_indices)
self.assertEqual(
program.thermofluid.secondary_pressure_scope_indices,
program.pressure_flow.secondary_scope_indices,
)
def test_state_reducer_and_all_buffers_have_complete_index_contracts(self) -> None:
reducer = self.program.state_reducer
self.assertEqual(
(reducer.state_scatter.pattern.row_count,
reducer.state_scatter.pattern.column_count),
(len(reducer.local_state_slots), reducer.solver_state_count),
)
self.assertEqual(
(reducer.derivative_gather.pattern.row_count,
reducer.derivative_gather.pattern.column_count),
(reducer.solver_state_count, len(reducer.raw_derivative_slots)),
)
by_buffer: dict[IRBufferKind, list[int]] = {
buffer.kind: [] for buffer in self.program.buffers
}
for value in self.program.values:
by_buffer[value.slot.buffer].append(value.slot.index)
for buffer in self.program.buffers:
self.assertEqual(
sorted(by_buffer[buffer.kind]),
list(range(buffer.size)),
buffer.kind,
)
def test_transaction_tracks_exactly_the_active_pneumatic_flows(self) -> None:
pneumatic_flow_slots = {
variable.slot
for port in self.program.ports
if port.kind.value == "physical" and port.domain == "pneumatic"
for variable in port.variables
if variable.name == "m_flow"
}
self.assertEqual(
set(self.program.transaction.flow_slots),
pneumatic_flow_slots,
)
def test_reference_archive_does_not_claim_native_execution(self) -> None:
self.assertIs(
self.program.capabilities.system_level,
IRCapabilityLevel.REFERENCE_ONLY,
)
self.assertTrue(self.program.capabilities.components)
self.assertTrue(
all(
item.level is IRCapabilityLevel.REFERENCE_ONLY
for item in self.program.capabilities.components
)
)
self.assertIn(
"IR_NATIVE_KERNELS_NOT_DECLARED",
{item.code for item in self.program.capabilities.issues},
)
def test_validator_rejects_cross_plan_and_sparse_contract_corruption(self) -> None:
program = self.program
variants: list[tuple[str, object, str]] = []
components = list(program.components)
foreign_port = next(
index
for index, port in enumerate(program.ports)
if port.component_index != 0
)
components[0] = replace(
components[0],
port_indices=(*components[0].port_indices, foreign_port),
)
variants.append(
(
"component port back-reference",
replace(program, components=tuple(components)),
"COMPONENT_PORT_COVERAGE",
)
)
variants.append(
(
"thermofluid scope mismatch",
replace(
program,
thermofluid=replace(
program.thermofluid,
secondary_pressure_scope_indices=(),
),
),
"THERMOFLUID_SCOPE_MISMATCH",
)
)
equations = list(program.pressure_flow.equations)
equations[1] = replace(
equations[1],
residual_slot=equations[0].residual_slot,
)
variants.append(
(
"pressure-flow residual slot alias",
replace(
program,
pressure_flow=replace(
program.pressure_flow,
equations=tuple(equations),
),
),
"PRESSURE_FLOW_RESIDUAL_SLOT_DUPLICATE",
)
)
buffers = list(program.buffers)
state_buffer_index = next(
index
for index, buffer in enumerate(buffers)
if buffer.kind is IRBufferKind.STATE_INPUT
)
state_values = list(buffers[state_buffer_index].initial_float_values)
state_values[0] += 1.0
buffers[state_buffer_index] = replace(
buffers[state_buffer_index],
initial_float_values=tuple(state_values),
)
variants.append(
(
"state initial value disagreement",
replace(program, buffers=tuple(buffers)),
"STATE_INITIAL_VALUE_MISMATCH",
)
)
aliased_values = (
program.jacobian.value_slots[0],
program.jacobian.value_slots[0],
*program.jacobian.value_slots[2:],
)
variants.append(
(
"Jacobian value slot alias",
replace(
program,
jacobian=replace(
program.jacobian,
value_slots=aliased_values,
),
),
"JACOBIAN_VALUE_SLOT_COVERAGE",
)
)
variants.append(
(
"Jacobian color conflict",
replace(
program,
jacobian=replace(
program.jacobian,
color_groups=(
tuple(range(program.state_reducer.solver_state_count)),
),
),
),
"JACOBIAN_COLOR_CONFLICT",
)
)
fd_columns = list(program.jacobian.local_finite_difference_columns)
fd_column = fd_columns[0]
wrong_value_index = next(
index
for index, column in enumerate(
program.jacobian.pattern.column_indices
)
if column != fd_column.column_index
)
fd_columns[0] = replace(
fd_column,
value_indices=(wrong_value_index, *fd_column.value_indices[1:]),
)
variants.append(
(
"Jacobian finite-difference column mismatch",
replace(
program,
jacobian=replace(
program.jacobian,
local_finite_difference_columns=tuple(fd_columns),
),
),
"JACOBIAN_FD_COLUMN_MISMATCH",
)
)
capabilities = list(program.capabilities.components)
capabilities[0] = replace(
capabilities[0],
level=IRCapabilityLevel.NATIVE,
missing_features=(),
)
variants.append(
(
"native capability overclaim",
replace(
program,
capabilities=replace(
program.capabilities,
components=tuple(capabilities),
),
),
"CAPABILITY_KERNEL_MISMATCH",
)
)
variants.append(
(
"duplicate transaction flow",
replace(
program,
transaction=replace(
program.transaction,
flow_slots=(
*program.transaction.flow_slots,
program.transaction.flow_slots[0],
),
),
),
"TRANSACTION_DUPLICATE_SLOT",
)
)
modes = list(program.modes)
modes[1] = replace(modes[1], slot=modes[0].slot)
variants.append(
(
"duplicate mode slot",
replace(program, modes=tuple(modes)),
"MODE_SLOT_DUPLICATE",
)
)
gather_pattern = program.state_reducer.derivative_gather.pattern
gather_pointers = list(gather_pattern.row_pointers)
gather_pointers[1] = gather_pointers[0]
variants.append(
(
"empty derivative row",
replace(
program,
state_reducer=replace(
program.state_reducer,
derivative_gather=replace(
program.state_reducer.derivative_gather,
pattern=replace(
gather_pattern,
row_pointers=tuple(gather_pointers),
),
),
),
),
"DERIVATIVE_GATHER_EMPTY_ROW",
)
)
for label, corrupted, expected_code in variants:
with self.subTest(label=label):
report = validate_system_ir(corrupted)
self.assertFalse(report.valid)
self.assertIn(
expected_code,
{item.code for item in report.issues},
)
def test_target_reference_reload_is_byte_stable(self) -> None:
second = reference_ir('target')
self.assertEqual(
self.program.canonical_json_bytes(),
second.canonical_json_bytes(),
)
class HistoricalSystemNumericIRV2Tests(unittest.TestCase):
def test_historical_complex_model_schema_remains_valid(self) -> None:
program = reference_ir('historical')
self.assertTrue(validate_system_ir(program).valid)
self.assertEqual(len(program.components), 98)
self.assertEqual(len(program.connections), 106)
self.assertEqual(program.state_reducer.solver_state_count, 74)
self.assertEqual(len(program.pressure_flow.unknowns), 472)
self.assertEqual(len(program.pressure_flow.equations), 472)
self.assertEqual(len(program.outputs), 1021)
if __name__ == "__main__":
unittest.main()
class RetiredIRExporterTests(unittest.TestCase):
def test_legacy_exporter_fails_with_a_migration_message(self):
with self.assertRaisesRegex(NotImplementedError, 'retired'):
compile_system_ir(object())