Compare commits
2
Commits
main
..
ce353d2dd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce353d2dd2 | ||
|
|
03b86f52ba |
No files matched your search
@@ -1,3 +1,10 @@
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.sh text eol=lf
|
||||
|
||||
# Regression manifests hash these files as raw bytes. Keep their checkout
|
||||
# representation identical on Windows and Linux so hashes remain portable.
|
||||
tests/data/test-mql-8.xml text eol=lf
|
||||
tests/data/test-mql-8.json text eol=lf
|
||||
tests/data/test_mql-full-branches-01-04.xml text eol=lf
|
||||
tests/baselines/simulation/**/*.json text eol=lf
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- "requirements.txt"
|
||||
- "constraints/**"
|
||||
- ".python-version"
|
||||
- ".gitattributes"
|
||||
- "README.md"
|
||||
- ".github/workflows/solver-regression.yml"
|
||||
pull_request:
|
||||
@@ -17,6 +18,7 @@ on:
|
||||
- "requirements.txt"
|
||||
- "constraints/**"
|
||||
- ".python-version"
|
||||
- ".gitattributes"
|
||||
- "README.md"
|
||||
- ".github/workflows/solver-regression.yml"
|
||||
schedule:
|
||||
@@ -60,6 +62,27 @@ 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' ||
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -12,6 +12,10 @@
|
||||
|
||||
新增或移动文档时,应根据文档用途放入对应目录。目录链接可用于查看其中的全部文档,无需在本文件中逐项维护清单。
|
||||
|
||||
## 重点实施计划
|
||||
|
||||
- [C 语言数值内核实施计划与可行性评估](other/C语言数值内核实施计划与可行性评估.md):后端数值内核的分阶段迁移顺序、验收门、风险和可行性评估。
|
||||
|
||||
## `update-log` 书写规范
|
||||
|
||||
以下规范适用于新建和后续追加的日志。历史日志缺少准确完成时间时,不猜测或补写时间。
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
# C 语言数值内核实施计划与可行性评估
|
||||
|
||||
> 文档状态:待评审实施方案
|
||||
> 建立日期:2026-09-02
|
||||
> 适用分支:`model-development`
|
||||
> 关联文档:[求解器性能优化任务清单](求解器性能优化任务清单.md)、[后端求解逻辑与效率优化调研](后端求解逻辑与效率优化调研.md)
|
||||
> 范围:后端数值执行内核;不包含前端重写,也不主张把整个 Python 后端改写为 C
|
||||
|
||||
## 1. 结论
|
||||
|
||||
这条路线值得实施,但正确的目标不是“把现有 Python 代码逐行翻译成 C”,而是:
|
||||
|
||||
1. 保留 Python 作为模型解析、网络编译、任务管理、结果服务和正确性参考;
|
||||
2. 先把完整的一次 RHS 计算编译成无 Python 回调的扁平数值 IR;
|
||||
3. 再让 C 内核一次完成整次 RHS、stream、物性、Jacobian 和事件计算;
|
||||
4. Python 旧引擎长期保留为不支持模型的兼容路径和故障回退路径。
|
||||
|
||||
总体技术可行性为**中高**。仓库已经具备因果计划、参考 IR、稀疏 Jacobian 结构、性能埋点和 AMESim 回归基础,因此不是从零开始。主要难点不是 C 语法,而是完整冻结当前模型的计算顺序、副作用、事件和回滚语义。
|
||||
|
||||
“实现稳定的 C 数值后端”具有较高可能性;“仅靠换成 C 就达到接近 AMESim 的速度”目前不能承诺。高刚度 LSTP 接触会让积分器产生大量纳秒级小步,有限差分 Jacobian 又会重复调用 RHS。C 能降低每次计算的成本,但不会自动减少这些计算次数。真正的大幅收益需要同时完成:
|
||||
|
||||
- 完整扁平 IR;
|
||||
- 整体 C RHS;
|
||||
- 稀疏解析或半解析 Jacobian;
|
||||
- 正确的接触事件和模式切换;
|
||||
- 必要时再评估原生积分器。
|
||||
|
||||
## 2. 推荐的最终边界
|
||||
|
||||
```text
|
||||
XML / JSON
|
||||
↓
|
||||
Python:解析、校验、组件注册、网络编译
|
||||
↓
|
||||
完整扁平 IR:槽位、阶段、依赖、模式、事件、Jacobian 结构
|
||||
↓ 一次跨语言调用完成整次计算
|
||||
C 数值内核:RHS / stream / 物性 / Jacobian / Event
|
||||
↓
|
||||
SciPy BDF(首阶段保留)或后续通过验证的原生积分器
|
||||
↓
|
||||
Python:采样、结果编码、API、任务状态和诊断
|
||||
```
|
||||
|
||||
以下边界必须坚持:
|
||||
|
||||
- 不在每个组件上来回调用 Python 和 C;一次 RHS 最多进行一次主要跨语言调用。
|
||||
- C 兼容模型的热路径内不得调用 Python callback。
|
||||
- C 内核不直接解析 XML,不管理 HTTP,也不承担组件编辑和数据库职责。
|
||||
- 自定义 Python 组件不能静默降级为跨语言逐组件调用;只要整模型不满足原生能力合同,就明确走 Python 引擎。
|
||||
- 迁移期间始终保留 `python`、`native`、`shadow` 三种模式;通过长时发布门后才能增加 `auto` 默认选择。
|
||||
|
||||
建议统一配置为:
|
||||
|
||||
```text
|
||||
SIMULATION_NUMERIC_ENGINE=python|native|shadow|auto
|
||||
```
|
||||
|
||||
其中 `shadow` 由 Python 控制正式积分,C 只接收相同输入并逐阶段比较,不允许影响 Python 工作区。
|
||||
|
||||
## 3. 当前基础与首个阻断
|
||||
|
||||
### 3.1 已有基础
|
||||
|
||||
- `causal_ir.py` 已有 schema v1、结构签名、预分配工作区、阶段观察和事务回滚原型。
|
||||
- 当前因果执行器已经消去一部分重复逻辑坐标,并证明了数组化和预编译执行计划的方向有效。
|
||||
- 已有 Jacobian 稀疏图、局部切向原语和显式实验回退路径。
|
||||
- 已有 AMESim 结果读取、物理投影、递进时域 runner、性能统计、取消和故障注入测试。
|
||||
- 当前主模型约有 156 个运行组件、20 种组件类型和 132 个连续状态,适合用“能力矩阵 + 分阶段覆盖”推进。
|
||||
|
||||
### 3.2 当前 IR 的缺口
|
||||
|
||||
现有 schema v1 只覆盖全局因果代数计划,绑定仍依赖 Python 的 reader、writer 和 evaluator 回调。以下内容尚未进入同一个无回调执行计划:
|
||||
|
||||
- 状态写入和信号传播;
|
||||
- dynamic volume 和物性状态包;
|
||||
- secondary 压力/流量块;
|
||||
- stream SCC/DAG 和热流体外层闭合;
|
||||
- 状态导数;
|
||||
- 离散 mode、事件检测、reset 和重启;
|
||||
- Jacobian 数值填充;
|
||||
- 结果投影。
|
||||
|
||||
因此,当前 IR 不能直接包一层 C 接口后就获得预期收益。
|
||||
|
||||
### 3.3 P0 跨平台换行阻断(已解决)
|
||||
|
||||
当前 Windows 工作树中的权威输入按原始字节计算时与 manifest 不一致;进一步核对确认,差异全部来自 Git 检出后的 `CRLF` 换行,而不是模型内容变化:
|
||||
|
||||
| 文件 | Windows 工作树原始字节 | 将 `CRLF` 还原为 `LF` 后 | manifest 记录值 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tests/data/test-mql-8.xml` | 106,188 B;2,078 个 `CRLF`;SHA-256 `e7eef641...b801` | 104,110 B;`0a2d9331...7b0b` | 104,110 B;`0a2d9331...7b0b` |
|
||||
| `tests/data/test-mql-8.json` | 282,796 B;10,204 个 `CRLF`;SHA-256 `51e1acb3...11e2` | 272,592 B;`b44bf540...fbe0` | 272,592 B;`b44bf540...fbe0` |
|
||||
| `AmesimModels/test_mql.ame` | 21,708,800 B;`cbc3aadd...c20fbb` | 不适用 | 一致 |
|
||||
|
||||
因此没有证据表明 fixture 语义发生漂移,也不应仅因这一差异重建 golden。真正的问题是:manifest 按原始字节锁定输入,而 `.gitattributes` 没有固定这两个文本 fixture 的换行;当前加载器又直接校验工作树原始字节,所以同一提交在 Windows 上可能被拒绝、在 Linux 上通过。
|
||||
|
||||
该阻断已于 2026-09-02 完成本地修复:`.gitattributes` 已将两个主模型输入、历史 0.81 s 输入和 `tests/baselines/simulation` 下的 JSON 证据固定为 `LF`,当前 Windows 工作树也已从 Git 索引重新检出为 `LF`。自动测试会同时检查属性规则、禁止原始 `CR` 字节,并按 manifest 复核输入大小和 SHA-256;CI 已配置为在 Ubuntu 和 Windows 上分别执行这一合同,远端运行证据待提交后取得。
|
||||
|
||||
本地验收显示这些文件均为 `index=LF / worktree=LF / eol=lf`,两个 regression manifest 可在 Windows 正常完整加载,现有 manifest 和 golden 无需更新。未来只有在规范化后的内容确实变化时,才进入“模型变更、重建 manifest/golden”的流程。历史性能数字仍需按其代码版本看待,但不因换行差异失效。
|
||||
|
||||
## 4. 按重要性排序的实施计划
|
||||
|
||||
优先级定义:
|
||||
|
||||
- `P0`:正确性和架构前置,不完成就不能安全编写生产 C 内核;
|
||||
- `P1`:形成可用且有明显收益的 C 后端;
|
||||
- `P2`:P1 证明有效后再实施的增强项。
|
||||
|
||||
表中的工期是单名熟悉现有求解器的开发者的粗略有效工作量,不是交付承诺,也不包含长时仿真排队时间。
|
||||
|
||||
| 顺序 | ID | 优先级 | 工作项 | 可行性 | 粗略工作量 | 主要依赖 |
|
||||
| ---: | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | C-00 | P0 | 固定权威基准的跨平台合同,建立双引擎与回滚合同 | 高 | 1–2 人周 | 现有 OPT-00 |
|
||||
| 2 | C-01 | P0 | 定义完整数值 IR schema v2 | 高 | 3–5 人周 | C-00、OPT-01/02 |
|
||||
| 3 | C-02 | P0 | 建立纯数值组件 kernel 合同和能力矩阵 | 中高 | 3–6 人周 | C-01 |
|
||||
| 4 | C-03 | P0 | 完成 Python 扁平参考执行器与 Shadow 差分 | 中高 | 3–5 人周 | C-01、C-02 |
|
||||
| 5 | C-04 | P1 | 稳定 C ABI、构建链和隔离 worker | 高 | 3–5 人周 | C-01,可并行 |
|
||||
| 6 | C-05 | P1 | 完成一个闭环代表子系统的 C 纵向切片 | 高 | 2–4 人周 | C-03、C-04 |
|
||||
| 7 | C-06 | P1 | 扩展到完整内置 RHS、stream 和物性 | 中高 | 5–9 人周 | C-05、OPT-04 |
|
||||
| 8 | C-07 | P1 | 稀疏解析/半解析 Jacobian 与局部回退 | 中 | 6–12 人周 | C-02、C-06、OPT-03 |
|
||||
| 9 | C-08 | P1 | LSTP/MECMAS 事件、模式和默认灰度 | 中 | 4–8 人周 | C-06、C-07、OPT-05/06 |
|
||||
| 10 | C-09 | P2 | 模型专用 C 代码生成和编译缓存 | 中 | 4–8 人周 | C-06~C-08 |
|
||||
| 11 | C-10 | P2 | 评估 CVODE;有真实 DAE 需求时再评估 IDA | 中/当前低 | 4–10 人周 | C-06~C-08 |
|
||||
| 12 | C-11 | P2 | 输出、部署、并发和发布收口 | 中高 | 3–6 人周 | 原生默认候选 |
|
||||
|
||||
### 4.1 P0:先固定语义和参考答案
|
||||
|
||||
#### C-00 固定权威基准、双引擎和回滚合同
|
||||
|
||||
工作内容:
|
||||
|
||||
- [x] 固定 `test-mql-8.xml/json`、历史输入和回归证据的跨平台 `LF` 字节合同,实际重新规范化当前 Windows 工作树,并用 Git EOL 状态、原始 SHA-256 和双平台 CI 保护该合同;未重建语义未变的 golden。
|
||||
- 重新加载 manifest 并执行 Python `0.01 s` smoke;只有发现规范化后的内容或物理结果确实变化时,才重新生成结构清单和 golden。
|
||||
- 冻结当前 Python 引擎为参考实现,定义 `python/native/shadow/auto` 的启用条件。
|
||||
- 原生不支持的模型只允许在编译或加载阶段整模型回退;正式验收时禁止静默回退。
|
||||
- 每个阶段使用独立小提交,记录输入哈希、环境、二进制哈希和回滚开关。
|
||||
|
||||
完成标准:相同输入能够稳定强制走 Python;关闭原生后行为与当前参考提交一致;模型、环境和结果来源都能追溯。
|
||||
|
||||
#### 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
|
||||
state scatter
|
||||
→ signal
|
||||
→ mechanical equivalence / dynamic volume
|
||||
→ property bundle
|
||||
→ first pressure-flow closure
|
||||
→ stream SCC/DAG
|
||||
→ temperature reference update
|
||||
→ sensitive-island closure / thermofluid fixed point
|
||||
→ mechanical acceleration
|
||||
```
|
||||
|
||||
四类入口分别编译自己的依赖切片:
|
||||
|
||||
```text
|
||||
eval_rhs = required primal stages → derivative
|
||||
eval_events = required primal stages → event values
|
||||
eval_jacobian = RHS primal / local derivative or JVP → CSR values
|
||||
eval_outputs = required primal stages → output projection
|
||||
```
|
||||
|
||||
IR 至少描述:
|
||||
|
||||
- 连续状态、代数坐标、参数、常量、离散模式和工作区槽位;
|
||||
- 每个阶段和 opcode 的读写集合、单位、缩放、上下界和错误来源;
|
||||
- stream 图、SCC、物性状态包、外层固定点和事务恢复集合;
|
||||
- event ID、左/右状态、reset、Jacobian 失效和模式计划;
|
||||
- 固定的 CSR Jacobian 结构和 output projection;
|
||||
- 完整 IR 内容(含组件模型/实现、介质、dtype、参数和执行计划)组成跨平台内容签名;
|
||||
- native artifact key 再把内容签名与 native ABI、目标平台、编译器、构建选项、浮点策略和 kernel 库签名组合。
|
||||
|
||||
原生兼容 program 中不得存在 Python callback。schema v1 保留为参考适配器,不直接扩展成生产 ABI。
|
||||
|
||||
完成标准:同一模型重复编译得到字节级稳定的结构和签名;所有索引及读写集合可静态校验;结构可完整描述但缺少原生能力时明确标为 `reference_only`,原生加载器必须拒绝接管。
|
||||
|
||||
#### C-02 纯数值组件合同
|
||||
|
||||
每个内置组件需声明:
|
||||
|
||||
- `kernel_id` 和实现版本;
|
||||
- 输入、输出、参数、状态和 mode 槽位;
|
||||
- primal、局部导数/JVP、event 和 reset 能力;
|
||||
- 支持的正反流、接触和物性范围;
|
||||
- 类型化错误码和局部数值差分回退边界。
|
||||
|
||||
组件 kernel 必须满足“相同输入得到相同输出”,不能依赖隐藏的 Python 对象写入。旧组件类继续作为参考实现。
|
||||
|
||||
首批纵向切片建议选择一条完整的 `MECMAS21 → PNRP17 → PNCH012 → PNL0001/LSTP00A` 支路及其介质/stream 闭合,因为它同时覆盖机械、气动、物性、流量和高刚度接触。
|
||||
|
||||
#### C-03 Python 扁平参考执行器
|
||||
|
||||
先用 Python/NumPy 完整执行 schema v2,目标是验证语义,而不是追求最终速度。它应摆脱 `PortState` 作为主语义载体,能在每个阶段与当前对象引擎比较首个差异。
|
||||
|
||||
完成标准:代表模型在普通点、反向流点、事件左右和 Jacobian 扰动点逐槽一致;`0.01/0.2/0.81/2.10 s` 的状态、事件、守恒和物理投影满足现有合同。
|
||||
|
||||
### 4.2 P1:形成真正有价值的 C 后端
|
||||
|
||||
#### C-04 稳定 C ABI、构建链和隔离 worker
|
||||
|
||||
底层使用稳定纯 C ABI,Python 绑定保持很薄。最小接口应包含:
|
||||
|
||||
```text
|
||||
model_create
|
||||
eval_rhs
|
||||
eval_jacobian
|
||||
eval_events
|
||||
apply_event
|
||||
eval_outputs
|
||||
model_destroy
|
||||
```
|
||||
|
||||
接口使用不透明 `ModelContext`、固定宽度整数、连续 `float64` 数组和显式长度;CSR 结构在编译时固定,运行时只填 values。每个任务独占可变 context/workspace/cache/mode,不使用可变全局缓存,热路径预分配且不进行常规堆分配。
|
||||
|
||||
首阶段仍由 SciPy BDF 积分,只替换整个 RHS/Jacobian 计算。构建链需覆盖 Windows x64 和 Linux x86_64,固定构建依赖,并禁止 `/fp:fast`、`-ffast-math`、`-march=native` 等会改变事件边界或破坏可移植性的默认选项。
|
||||
|
||||
原生访问违规、崩溃或死循环不能在同一 Python 进程中安全恢复。因此原生默认启用前,仿真必须运行在可硬终止的隔离 worker 中;C 同时接收取消标志并在组件阶段、stream SCC、Jacobian 和迭代处有界检查。
|
||||
|
||||
#### C-05 闭环纵向切片和首次 Go/No-Go
|
||||
|
||||
一次跨语言调用应完成代表子系统的整次 RHS,禁止按组件往返。先为该闭环子系统建立独立代表 fixture,执行 Shadow 和短时积分,不立即扩大组件覆盖。由于此阶段尚未覆盖主模型的全部组件,`test-mql-8` 整模型按能力合同回退 Python 是预期行为,不能拿它衡量 C-05 的端到端收益。
|
||||
|
||||
继续扩面的最低门槛:
|
||||
|
||||
- RHS 微基准至少达到 Python 的 `2×`;目标值为 `3×` 以上;
|
||||
- 独立代表 fixture 的短时完整仿真中位时间至少降低 25%;
|
||||
- 代表 fixture 的状态、事件、模式和守恒门全部通过;存在对应 AMESim 投影时也必须通过;
|
||||
- `nfev/njev/nlu` 和接受步等工作量没有无法解释的变化;
|
||||
- 无 Python callback、无静默回退、无内存错误。
|
||||
|
||||
如果 RHS 已快很多而完整仿真几乎不变,应先检查 Jacobian、微步数和跨语言边界,而不是直接继续扩大 C 代码。
|
||||
|
||||
#### C-06 完整内置 RHS、stream 和物性
|
||||
|
||||
- 把所有受支持的内置组件纳入能力矩阵;一个组件不支持时整模型明确走 Python。
|
||||
- 将 stream 图编译成 SCC 和缩点 DAG:无环部分一次传播,只在循环 SCC 中迭代。
|
||||
- 将 `p/T/rho/u/h` 及其导数组成同一物性状态包,按精确输入和阶段统一复用。
|
||||
- 移除每轮临时字典/列表,使用预分配数组和原地误差统计。
|
||||
- 保留试探状态事务回滚、反向流、温度参考更新和类型化可恢复失败语义。
|
||||
|
||||
完成标准:主目标使用的全部内置组件可原生执行;stream 迭代、压力流量残差和物性结果不恶化;跨平台字节合同修复后的同一权威输入通过短程与中程回归,且 `0.2 s` 完整仿真中位时间至少降低 30%。
|
||||
|
||||
#### C-07 稀疏解析/半解析 Jacobian
|
||||
|
||||
这是获得大幅端到端收益的关键步骤。当前历史报告中,有限差分 Jacobian 会贡献大量额外 RHS 调用;仅把 primal RHS 改成 C 仍会重复执行它。
|
||||
|
||||
- 组件局部导数沿完整 IR 传播,直接填充固定 CSR values。
|
||||
- 不支持或非光滑点只对局部组件、状态列或 SCC 做数值差分,不能让一个局部问题恢复全网有限差分。
|
||||
- 每次构建记录解析列、局部差分列、回退位置、JVP 审计和装配时间。
|
||||
- LSTP 接触、流向切换、饱和及临界流动使用分段导数,并在边界上明确选择事件分段或局部回退。
|
||||
|
||||
完成标准:有限差分附加 RHS 至少减少 50%,相对完整 C RHS 阶段再降低至少 20% 总时间;事件顺序、模式和物理投影不变。
|
||||
|
||||
#### C-08 事件、模式和默认灰度
|
||||
|
||||
- 将 LSTP/MECMAS 的 event、mode、reset、Jacobian 失效和 solver restart 纳入正式 IR/C 合同。
|
||||
- 对接触进入、保持、释放,上下端挡、回弹、同时事件、擦边事件和防抖分别测试。
|
||||
- 按 `shadow → 显式 native → 模型签名白名单 → auto` 逐级启用。
|
||||
- 只有当前权威 `10 s` 连续三次通过后,才允许受支持模型默认走 C。
|
||||
|
||||
需要特别注意:C-08 不只是把同一接触公式写成 C,还要减少因接触表达方式造成的不必要纳秒级微步。任何软化、容差或事件改动都属于数值算法变化,必须与纯执行优化分开提交和验收。
|
||||
|
||||
### 4.3 P2:证明 P1 有效后再做
|
||||
|
||||
#### C-09 模型专用 C 代码生成
|
||||
|
||||
通用 C IR 解释器稳定后,若 opcode 分派仍是明确热点,再为固定模型生成专用 C。XML 文本不得直接拼入源码;生成器只消费校验后的数值 IR。编译缓存键必须包含完整结构签名、编译器和 flags。
|
||||
|
||||
只有相对通用 C 执行器额外获得至少约 `1.5×` 的稳定收益,并且编译成本能在重复运行中摊销时才继续。否则保留通用执行器,放弃这一层复杂度。
|
||||
|
||||
#### C-10 原生积分器
|
||||
|
||||
- 当前系统是“RHS 内完成代数闭合”的半显式 ODE,先评估 CVODE,不直接切换为 DASSL/IDA。
|
||||
- 只有 C RHS、Jacobian、事件、可恢复缩步、partial result、activity 和 cancel 合同稳定后,才对 CVODE 做独立 A/B。
|
||||
- CVODE 在相同正确性门下额外降低至少 25% 总时间才值得替换 SciPy。
|
||||
- IDA/DASSL 需要新的 `F(t,y,ydot)=0`、代数变量布局、一致初始化、指数和质量矩阵合同。只有真实模型证明无法稳健因果化或 ODE 化时再立项,当前不把它视为提速开关。
|
||||
|
||||
#### C-11 输出、部署和发布收口
|
||||
|
||||
吸收按需结果变量、列式输出、结果分块、编译缓存、并发限流、worker 硬终止和原生崩溃恢复。确保 native 崩溃只影响当前任务,服务仍能接收新任务。
|
||||
|
||||
## 5. 分阶段验证与 Go/No-Go 门
|
||||
|
||||
| 验证阶段 | 主要内容 | 通过条件 | 未通过时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| V0 | fixture、环境、Python 和 AMESim 基线 | 来源一致;Python 连续 3 次稳定;现有测试通过 | 基线仍漂移时停止 C 默认路径开发 |
|
||||
| V1 | 完整 Python IR | 槽位、阶段、读写集合、事件和回滚差分通过 | 先修隐藏副作用和 IR 合同 |
|
||||
| V2 | C 纵向切片 | 单组件/逐阶段通过;RHS 至少 `2×` | 未达门槛则检查 IR/FFI,必要时停止扩面 |
|
||||
| V3 | 完整 C RHS + SciPy | `0.01/0.2 s` 通过;总时间至少降低 30%;工作量变化不超过可解释范围 | 不作为纯执行优化合入默认路径 |
|
||||
| V4 | C Jacobian/Event | FD 附加 RHS 至少减少 50%;相对 V3 再降低 20%;事件/模式一致 | 保留局部差分,禁止整网静默回退 |
|
||||
| V5 | 长时与鲁棒性 | `1/5/10 s` 递进;10 s 连续 3 次;无泄漏、无静默回退 | 任一短时前驱失败即停止后续长跑 |
|
||||
| V6 | 默认启用 | Windows/Linux 发布通过;支持模型走 C,不支持模型明确走 Python;可一键关闭 | 保持显式 opt-in |
|
||||
|
||||
正式性能比较统一要求:预热 1 次、完整仿真至少测量 3 次并比较中位数、固定机器与电源模式,冷编译和热缓存分开报告。至少记录:
|
||||
|
||||
- 总时间、积分、RHS、闭合、stream、物性、Jacobian 和输出时间;
|
||||
- `nfev/njev/nlu`、接受/拒绝步、solver 启动和事件次数;
|
||||
- Jacobian 附加 RHS、局部数值差分和回退原因;
|
||||
- Python/C 边界调用次数和耗时;
|
||||
- 峰值 RSS、C 工作区、IR/ABI 版本、编译器、flags 和二进制哈希。
|
||||
|
||||
附加硬门:关闭 C 时 Python 路径额外开销不超过 2%;纯执行层替换若使 `nfev/njev/nlu` 或接受步数变化超过 2%,必须按数值语义变化单独调查,不能直接归为性能优化;峰值 RSS 不超过 Python 基线的 110%;10 s 中 C 固定工作区不随步数增长,只有结果数组可随采样数增长。
|
||||
|
||||
## 6. 正确性验证范围
|
||||
|
||||
### 6.1 Shadow 阶段观察点
|
||||
|
||||
两个引擎必须接收完全相同的 `time/state/mode/parameters`,按第 4.1 节的 RHS 阶段逐项比较。差异报告至少包含:阶段、槽位、组件、输入、Python 值、C 值、绝对/相对误差和当前模式。
|
||||
|
||||
- 槽位布局、模式码、事件 ID、执行顺序和迭代次数要求一致。
|
||||
- 连续量使用“每种物理量绝对容差 + 相对容差”,不能用一个绝对容差覆盖压力、流量、温度和位移。
|
||||
- NaN/Inf、压力越界、符号错误和模式不同立即失败。
|
||||
- 压力流量最大缩放残差继续使用现有 `1e-7` 门。
|
||||
- 事件点分别比较左极限、事件处理结果和右极限,不扩大容差掩盖不连续。
|
||||
|
||||
差分语料至少覆盖:初始状态、接受状态、Jacobian 扰动状态、固定随机种子扰动、压力近似相等、流量换向、接触启停、端挡释放、物性边界,以及历史 `0.69/0.81/2.05 s` 区域。
|
||||
|
||||
### 6.2 AMESim 物理门
|
||||
|
||||
Python golden 只用于发现实现漂移,AMESim 结果仍是外部物理基线。正式默认前,应在现有 physical-state-v2.1 基础上至少覆盖:
|
||||
|
||||
- 代表性气室压力、温度和质量;
|
||||
- PNL 管路压力、流量及换向;
|
||||
- PNRP 活塞力;
|
||||
- MECMAS 位移、速度和模式;
|
||||
- LSTP 间隙、接触力和接触切换;
|
||||
- 每个主要支路至少一个代表量。
|
||||
|
||||
检查点至少包含 `0`、`0.04 s` 左右、`0.2`、`0.8 s` 左右、`1`、`2`、`5` 和 `10 s`。AMESim 未保存的内部守恒量继续执行独立绝对残差门。
|
||||
|
||||
### 6.3 错误、取消和并发
|
||||
|
||||
必须覆盖 C 返回非有限值、物性域错误、闭合不收敛、未知 opcode、ABI 不匹配、损坏二进制、失败后事务回滚、编译失败、并发 context 隔离、单任务取消、重复创建/销毁无内存增长和 native 崩溃后的服务存活。
|
||||
|
||||
回退分为三层:
|
||||
|
||||
1. **启动前自动选择**:编译或加载时发现不支持能力,整次任务在开始积分前明确选择 Python;这是正式模式唯一允许的自动换引擎路径。
|
||||
2. **受控运行时错误**:native 已开始积分后返回错误时,当前 native 任务直接失败并保留诊断。灰度验证可以从不可变原始请求的 `tStart` 另起一个 Python 重放任务,但必须标为独立重放,不能算作 native 成功。没有完整保存 BDF 历史、事件上下文和已有输出前,禁止从某个 `(t,y,mode)` 假装无缝续跑。
|
||||
3. **进程级故障**:访问违规或无法返回时,由父进程硬终止 worker,禁止在受损进程中继续;如需 Python 对照,同样从原始请求重新执行。
|
||||
|
||||
正式 C 验收要求异常 `fallbackCount=0`。声明过的局部 Jacobian 数值差分不是异常回退,但必须单独计数。
|
||||
|
||||
## 7. 对现有优化清单的取舍
|
||||
|
||||
现有优化清单不能整体放弃,应按新路线重组:
|
||||
|
||||
| 现有任务 | 决策 | 在新路线中的位置 |
|
||||
| --- | --- | --- |
|
||||
| OPT-00 基线与回归 | 保留并加强 | C-00 和全部发布门 |
|
||||
| OPT-01 因果代数内核 | 冻结成果 | 作为 IR 编译输入和 Python 回退,不再继续零散微调 |
|
||||
| OPT-02 扁平 IR | 升为主线 | C-01、C-03、C-05 |
|
||||
| OPT-03 Jacobian | 升为主线 | C-07 |
|
||||
| OPT-04 stream/物性 | 升为主线 | C-06 |
|
||||
| OPT-05 步长、接触和鲁棒性 | 必须保留 | C-08;C 不能消除微步根因 |
|
||||
| OPT-06 dense output | 部分前置、其余后置 | 事件合同进入 C-01/C-08,插值微调后置 |
|
||||
| OPT-07 输出与内存 | 暂缓但不删除 | C-11 |
|
||||
| OPT-08 取消与并发 | 必须保留并提前 | C-04/C-11;原生代码更需要进程隔离 |
|
||||
| OPT-09 10 s 验收 | 必须保留 | V5/V6 发布门 |
|
||||
| OPT-10 高指数 DAE | 有条件暂缓 | 只有真实 DAE 需求时进入 C-10 |
|
||||
|
||||
可以停止继续投入的方向:
|
||||
|
||||
- 在现有对象热路径上继续做零散字典和属性访问微优化;
|
||||
- 继续叠加缺少统一失效合同的通用缓存;
|
||||
- 优化目标模型中未触发的 `least_squares` 回退;
|
||||
- 不断增加少量特例半解析列,而不先建立通用组件导数合同;
|
||||
- 直接对当前对象图使用 Numba;
|
||||
- 通过修改容差、软化物理或寻找“幸运 maxStep”伪装性能收益。
|
||||
|
||||
Numba 可以在完整数组 IR 后用于 1–2 周的架构验证,但不作为长期生产依赖。若完整数组 RHS 在 Numba 原型中仍没有明显改善,应先修正 IR 和算法,而不是立即开始大规模 C 重写。
|
||||
|
||||
## 8. 可行性评估
|
||||
|
||||
| 目标 | 当前判断 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 验证框架、双引擎和基准恢复 | 高 | 现有 runner、golden、AMESim 读取和埋点可复用 |
|
||||
| 完整 Python 扁平 IR | 中高 | 结构基础已有,主要工作是显式化隐藏副作用和事件语义 |
|
||||
| 代表子系统通用 C RHS | 高 | 数值边界清楚,一次调用可避免 FFI 碎片化 |
|
||||
| 当前全部内置组件的 C RHS | 中高 | 约 20 种类型可逐类迁移,但 stream/物性/模式较复杂 |
|
||||
| 可维护的稀疏解析/半解析 Jacobian | 中 | 收益大,但非光滑接触、流向切换和导数覆盖难度最高 |
|
||||
| 模型专用 C 代码生成 | 中 | 收益上限高,但构建、缓存、安全和诊断成本明显增加 |
|
||||
| CVODE 替换 SciPy | 中,且后置 | 可能减少调度开销,但不会自动解决错误方程或接触微步 |
|
||||
| IDA/DASSL 直接提速 | 当前低 | 目前缺少真正 DAE 的残差、一致初始化和质量矩阵合同 |
|
||||
| 接近 AMESim 的总速度 | 中低、待实测 | 缺少同机 AMESim 墙钟基线,且当前主要瓶颈同时包含 Jacobian 和接触微步数 |
|
||||
|
||||
从项目节奏看,建议把目标分成三档;这里与第 4 节一样使用“人周”,多人并行时日历时间可短于人周总量:
|
||||
|
||||
1. **约 15–27 人周:技术决策闭环。** 完成 C-00~C-05,回答完整 IR 是否正确、C RHS 是否有足够收益。
|
||||
2. **累计约 20–36 人周:可选原生后端。** 完成 C-06,使主要内置 RHS、stream/物性、双平台构建和受控回退可由用户显式启用。
|
||||
3. **累计约 30–56 人周:默认候选。** 完成 C-07/C-08、10 s 长时、并发隔离和发布门。
|
||||
|
||||
模型专用代码生成和原生积分器属于额外阶段,不应计入首个可用 C 后端的承诺。多人并行可以缩短日历时间,但 IR、组件合同、Jacobian 和事件语义存在强依赖,不能按人数等比例压缩。
|
||||
|
||||
## 9. 建议立即开展的第一批工作
|
||||
|
||||
第一批只做 P0,不直接开始大规模 C 编码:
|
||||
|
||||
1. **已完成:** 固定当前权威输入和回归证据的 `LF` 检出规则,重新规范化 Windows 工作树,并增加 Windows/Linux 字节合同测试;未重建语义未变的 golden。
|
||||
2. **已完成(C-01 结构层):** 生成当前模型的组件类型、槽位、阶段、副作用和事件能力描述;纯数值 kernel 能力矩阵的实现细节继续归入 C-02。
|
||||
3. **已完成:** 将 schema v2 写成独立规范,并冻结 RHS/Event/Jacobian/Outputs 入口、错误能力、事务、事件、Jacobian 和内容签名。
|
||||
4. 建立对象引擎与扁平 IR 的逐阶段 Shadow runner。
|
||||
5. 用一条完整机械—气动—管路—接触支路完成 Python 参考闭环。
|
||||
6. 评审通过后,再建立最小 C ABI 和纵向切片。
|
||||
|
||||
第一批的退出条件不是“已经写了多少 C”,而是:当前基准可信、同一计算能被无回调 IR 完整表达、差异能定位到具体阶段和槽位。只有达到这个条件,后续 C 工作才具有可预测的收益和可控的回滚成本。
|
||||
@@ -0,0 +1,262 @@
|
||||
# 全系统数值中间表示(System Numeric IR)规范 v2.0
|
||||
|
||||
状态: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`。
|
||||
@@ -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"}}}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
BASELINE_ROOT = REPOSITORY_ROOT / "tests" / "baselines" / "simulation"
|
||||
GIT_ATTRIBUTES_PATH = REPOSITORY_ROOT / ".gitattributes"
|
||||
|
||||
EXPECTED_ATTRIBUTE_RULES = {
|
||||
("tests/data/test-mql-8.xml", "text", "eol=lf"),
|
||||
("tests/data/test-mql-8.json", "text", "eol=lf"),
|
||||
("tests/data/test_mql-full-branches-01-04.xml", "text", "eol=lf"),
|
||||
("tests/baselines/simulation/**/*.json", "text", "eol=lf"),
|
||||
}
|
||||
|
||||
|
||||
def _manifest_paths() -> tuple[Path, ...]:
|
||||
return tuple(sorted(BASELINE_ROOT.glob("*/manifest.json")))
|
||||
|
||||
|
||||
def _manifest_source_descriptors() -> tuple[tuple[Path, dict[str, object]], ...]:
|
||||
descriptors: list[tuple[Path, dict[str, object]]] = []
|
||||
for manifest_path in _manifest_paths():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
source = manifest["source"]
|
||||
descriptors.append((manifest_path, source))
|
||||
companion = source.get("companionProject")
|
||||
if companion is not None:
|
||||
descriptors.append((manifest_path, companion))
|
||||
return tuple(descriptors)
|
||||
|
||||
|
||||
class RegressionFixtureLineEndingTests(unittest.TestCase):
|
||||
def test_gitattributes_pin_byte_locked_artifacts_to_lf(self) -> None:
|
||||
attribute_rules = {
|
||||
tuple(line.split())
|
||||
for raw_line in GIT_ATTRIBUTES_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if (line := raw_line.strip()) and not line.startswith("#")
|
||||
}
|
||||
|
||||
self.assertTrue(
|
||||
EXPECTED_ATTRIBUTE_RULES.issubset(attribute_rules),
|
||||
"Raw-byte regression artifacts must be explicitly pinned to LF.",
|
||||
)
|
||||
|
||||
def test_byte_locked_artifacts_contain_no_carriage_returns(self) -> None:
|
||||
paths = {
|
||||
path
|
||||
for path in BASELINE_ROOT.rglob("*.json")
|
||||
if path.is_file()
|
||||
}
|
||||
paths.update(
|
||||
REPOSITORY_ROOT / str(descriptor["path"])
|
||||
for _, descriptor in _manifest_source_descriptors()
|
||||
)
|
||||
|
||||
self.assertTrue(paths, "No byte-locked regression artifacts were found.")
|
||||
for path in sorted(paths):
|
||||
with self.subTest(path=path.relative_to(REPOSITORY_ROOT).as_posix()):
|
||||
self.assertNotIn(
|
||||
b"\r",
|
||||
path.read_bytes(),
|
||||
"Byte-locked regression artifacts must use LF line endings.",
|
||||
)
|
||||
|
||||
def test_manifest_source_identities_match_raw_worktree_bytes(self) -> None:
|
||||
self.assertTrue(_manifest_paths(), "No regression manifests were found.")
|
||||
for manifest_path, descriptor in _manifest_source_descriptors():
|
||||
relative_path = str(descriptor["path"])
|
||||
payload = (REPOSITORY_ROOT / relative_path).read_bytes()
|
||||
label = f"{manifest_path.parent.name}:{relative_path}"
|
||||
with self.subTest(source=label):
|
||||
self.assertEqual(len(payload), descriptor["bytes"])
|
||||
self.assertEqual(
|
||||
hashlib.sha256(payload).hexdigest(),
|
||||
descriptor["sha256"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,767 @@
|
||||
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 app.simulation.systems.generic import GenericFluidSystem
|
||||
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 _system_from_xml(path: Path) -> GenericFluidSystem:
|
||||
report = validate_system_xml_document(path.read_bytes())
|
||||
if not report.valid or report.document is None:
|
||||
raise AssertionError(report.as_dict())
|
||||
return GenericFluidSystem(compile_system_xml_network(report.document))
|
||||
|
||||
|
||||
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.system = GenericFluidSystem(
|
||||
compile_reactflow_network(zero_force_mass_project())
|
||||
)
|
||||
cls.program = compile_system_ir(cls.system)
|
||||
|
||||
def test_compiler_returns_a_statically_valid_callback_free_program(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 = compile_system_ir(
|
||||
GenericFluidSystem(
|
||||
compile_reactflow_network(zero_force_mass_project())
|
||||
)
|
||||
)
|
||||
|
||||
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_parameter_change_invalidates_the_program_signature(self) -> None:
|
||||
changed_project = zero_force_mass_project()
|
||||
changed_project.nodes[1].data.parameters["mass"] = 3.0
|
||||
changed = compile_system_ir(
|
||||
GenericFluidSystem(compile_reactflow_network(changed_project))
|
||||
)
|
||||
|
||||
self.assertNotEqual(
|
||||
self.program.structural_signature,
|
||||
changed.structural_signature,
|
||||
)
|
||||
|
||||
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.system = _system_from_xml(TARGET_XML)
|
||||
cls.program = compile_system_ir(cls.system)
|
||||
|
||||
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_native_support_is_not_claimed_before_c02(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_recompilation_is_byte_stable(self) -> None:
|
||||
second = compile_system_ir(_system_from_xml(TARGET_XML))
|
||||
|
||||
self.assertEqual(
|
||||
self.program.canonical_json_bytes(),
|
||||
second.canonical_json_bytes(),
|
||||
)
|
||||
|
||||
|
||||
class HistoricalSystemNumericIRV2Tests(unittest.TestCase):
|
||||
def test_historical_complex_model_also_compiles(self) -> None:
|
||||
program = compile_system_ir(_system_from_xml(HISTORICAL_XML))
|
||||
|
||||
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()
|
||||
Reference in new issue
Block a user