Compare commits

Author SHA1 Message Date
ljz ce353d2dd2 完成仿真系统IR-schema规定 2026-09-02 19:17:55 +08:00
ljz 03b86f52ba P0跨平台暂存问题解决 2026-09-02 19:14:40 +08:00
22 changed files with 5952 additions and 10686 deletions

No files matched your search

+3 -46
View File
@@ -22,11 +22,6 @@ from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
from pydantic import BaseModel, ConfigDict, Field, ValidationError from pydantic import BaseModel, ConfigDict, Field, ValidationError
from app.parameter_expression import (
ParameterExpressionError,
evaluate_parameter_expression,
expression_value_to_base_unit,
)
from app.simulation.performance import performance_span, profile_phase, profile_run from app.simulation.performance import performance_span, profile_phase, profile_run
from app.simulation.property_cache import property_cache_run from app.simulation.property_cache import property_cache_run
from app.simulation.solvers.solver import SolverActivityTracker from app.simulation.solvers.solver import SolverActivityTracker
@@ -1105,14 +1100,7 @@ def validate_reactflow_component_contract(
parameter_values: dict[str, float] = {} parameter_values: dict[str, float] = {}
for parameter in component_spec.parameters: for parameter in component_spec.parameters:
value = parameter_float( value = parameter_float(node, parameter.name, parameter.default)
node,
parameter.name,
parameter.default,
quantity=parameter.quantity,
base_unit=parameter.unit,
expressions_allowed=parameter.editor is None,
)
validation_message = parameter.validation_message(value) validation_message = parameter.validation_message(value)
if validation_message is not None: if validation_message is not None:
raise ValueError( raise ValueError(
@@ -1679,45 +1667,14 @@ def parameter_float(
node: ReactFlowNodePayload | None, node: ReactFlowNodePayload | None,
name: str, name: str,
default: float, default: float,
*,
quantity: str = "dimensionless",
base_unit: str = "",
expressions_allowed: bool = True,
) -> float: ) -> float:
if node is None: if node is None:
return default return default
value = node.data.parameters.get(name, default) value = node.data.parameters.get(name, default)
try: try:
numeric_value = float(value) return float(value)
except (TypeError, ValueError): except (TypeError, ValueError):
if not isinstance(value, str): raise ValueError(f"Parameter '{name}' on component '{node.id}' must be numeric.")
raise ValueError(
f"Parameter '{name}' on component '{node.id}' must be numeric."
)
if not expressions_allowed:
raise ValueError(
f"PARAMETER_EXPRESSION_FORBIDDEN: Parameter '{name}' on component "
f"'{node.id}' is a discrete selection and cannot use an expression."
)
try:
evaluated = evaluate_parameter_expression(value)
selected_unit = node.data.parameterUnits.get(name, base_unit)
return expression_value_to_base_unit(
evaluated,
quantity=quantity,
selected_unit=selected_unit,
)
except ParameterExpressionError as exc:
raise ValueError(
f"PARAMETER_EXPRESSION_INVALID: Parameter '{name}' on component "
f"'{node.id}' contains an invalid expression: {exc}."
) from exc
if not isfinite(numeric_value):
raise ValueError(
f"Parameter '{name}' on component '{node.id}' must be a finite "
"numeric value."
)
return numeric_value
def pipe_config_from_node(node: ReactFlowNodePayload | None, pipe_config_type): def pipe_config_from_node(node: ReactFlowNodePayload | None, pipe_config_type):
-408
View File
@@ -1,408 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import math
import re
from typing import Callable
MAX_INPUT_LENGTH = 512
MAX_TOKEN_COUNT = 256
MAX_OPERATION_COUNT = 256
MAX_NESTING_DEPTH = 32
MAX_FUNCTION_ARGUMENTS = 16
_UNSIGNED_NUMBER_PREFIX = re.compile(
r"(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?"
)
class ParameterExpressionError(ValueError):
"""Raised when an editor parameter expression cannot be resolved safely."""
@dataclass(frozen=True)
class _Token:
kind: str
text: str
position: int
value: float | None = None
def evaluate_parameter_expression(expression: str) -> float:
"""Evaluate the same bounded arithmetic subset accepted by the frontend.
The parser never executes Python code and cannot access names other than
the constants ``pi`` and ``e`` or the explicitly supported functions.
"""
source = expression.strip()
if source.startswith("="):
source = source[1:].strip()
if not source:
raise ParameterExpressionError("expression must not be empty")
if len(source) > MAX_INPUT_LENGTH:
raise ParameterExpressionError(
f"expression must not exceed {MAX_INPUT_LENGTH} characters"
)
return _ParameterExpressionParser(_tokenize(source)).parse()
def expression_value_to_base_unit(
value: float,
*,
quantity: str,
selected_unit: str,
) -> float:
"""Convert an expression result from its editor unit to the SI contract.
Plain numeric JSON values are already stored in SI and must not pass
through this function. Only expression results use the selected display
unit, matching the existing frontend behavior.
"""
conversions = _UNIT_CONVERSIONS.get(quantity)
if not conversions:
return _ensure_finite(value, "expression result")
conversion = conversions.get(selected_unit)
if conversion is None:
# The frontend falls back to the first (base) unit for an unknown or
# absent selection. Keep the execution boundary behavior identical.
conversion = next(iter(conversions.values()))
scale, offset = conversion
return _ensure_finite(value * scale + offset, "converted expression result")
def _tokenize(source: str) -> tuple[_Token, ...]:
tokens: list[_Token] = []
position = 0
def append(token: _Token) -> None:
tokens.append(token)
if len(tokens) > MAX_TOKEN_COUNT:
raise ParameterExpressionError(
f"expression must not exceed {MAX_TOKEN_COUNT} tokens"
)
while position < len(source):
character = source[position]
if character.isspace():
position += 1
continue
if character.isdigit() or (
character == "."
and position + 1 < len(source)
and source[position + 1].isdigit()
):
match = _UNSIGNED_NUMBER_PREFIX.match(source, position)
if match is None:
raise ParameterExpressionError(
f"invalid number near character {position + 1}"
)
text = match.group(0)
value = _ensure_finite(float(text), f"number {text!r}")
append(_Token("number", text, position, value))
position = match.end()
continue
if character.isascii() and (character.isalpha() or character == "_"):
end = position + 1
while end < len(source):
candidate = source[end]
if not candidate.isascii() or not (
candidate.isalnum() or candidate == "_"
):
break
end += 1
append(_Token("identifier", source[position:end], position))
position = end
continue
if character == "*" and source[position : position + 2] == "**":
append(_Token("operator", "**", position))
position += 2
continue
if character in "+-*/^":
append(_Token("operator", character, position))
position += 1
continue
if character == "(":
append(_Token("left_parenthesis", character, position))
position += 1
continue
if character == ")":
append(_Token("right_parenthesis", character, position))
position += 1
continue
if character == ",":
append(_Token("comma", character, position))
position += 1
continue
raise ParameterExpressionError(
f"unsupported symbol {character!r} at character {position + 1}"
)
tokens.append(_Token("end", "", len(source)))
return tuple(tokens)
class _ParameterExpressionParser:
def __init__(self, tokens: tuple[_Token, ...]) -> None:
self._tokens = tokens
self._index = 0
self._operation_count = 0
def parse(self) -> float:
value = self._parse_additive(0)
trailing = self._current()
if trailing.kind != "end":
raise ParameterExpressionError(
f"unexpected content {trailing.text!r} near character "
f"{trailing.position + 1}"
)
return _ensure_finite(value, "expression result")
def _parse_additive(self, depth: int) -> float:
value = self._parse_multiplicative(depth)
while self._is_operator("+") or self._is_operator("-"):
operator = self._advance().text
right = self._parse_multiplicative(depth)
self._count_operation()
value = _safe_operation(
lambda: value + right if operator == "+" else value - right,
f"operation {operator!r}",
)
return value
def _parse_multiplicative(self, depth: int) -> float:
value = self._parse_unary(depth)
while self._is_operator("*") or self._is_operator("/"):
operator = self._advance().text
right = self._parse_unary(depth)
self._count_operation()
if operator == "/" and right == 0:
raise ParameterExpressionError("division by zero is not allowed")
value = _safe_operation(
lambda: value * right if operator == "*" else value / right,
f"operation {operator!r}",
)
return value
def _parse_unary(self, depth: int) -> float:
self._assert_depth(depth)
if self._is_operator("+") or self._is_operator("-"):
operator = self._advance().text
self._count_operation()
operand = self._parse_unary(depth + 1)
return _ensure_finite(
operand if operator == "+" else -operand,
f"unary operation {operator!r}",
)
return self._parse_power(depth)
def _parse_power(self, depth: int) -> float:
self._assert_depth(depth)
base = self._parse_primary(depth)
if not self._is_operator("^") and not self._is_operator("**"):
return base
operator = self._advance().text
exponent = self._parse_unary(depth + 1)
self._count_operation()
return _safe_operation(
lambda: math.pow(base, exponent),
f"operation {operator!r}",
)
def _parse_primary(self, depth: int) -> float:
self._assert_depth(depth)
token = self._current()
if token.kind == "number":
self._advance()
return _ensure_finite(
token.value if token.value is not None else math.nan,
f"number {token.text!r}",
)
if token.kind == "identifier":
self._advance()
normalized_name = token.text.casefold()
if self._current().kind == "left_parenthesis":
return self._parse_function_call(
normalized_name,
token.text,
depth + 1,
)
if normalized_name == "pi":
return math.pi
if normalized_name == "e":
return math.e
raise ParameterExpressionError(f"unknown identifier {token.text!r}")
if token.kind == "left_parenthesis":
self._advance()
value = self._parse_additive(depth + 1)
self._expect("right_parenthesis", "missing closing parenthesis")
return value
if token.kind == "end":
raise ParameterExpressionError(
"expression ends before a number, constant, or function"
)
raise ParameterExpressionError(
f"expected a number, constant, or function near character "
f"{token.position + 1}"
)
def _parse_function_call(
self,
normalized_name: str,
source_name: str,
depth: int,
) -> float:
self._assert_depth(depth)
self._expect(
"left_parenthesis",
f"function {source_name} is missing an opening parenthesis",
)
arguments: list[float] = []
if self._current().kind != "right_parenthesis":
while True:
if len(arguments) >= MAX_FUNCTION_ARGUMENTS:
raise ParameterExpressionError(
f"function {source_name} accepts at most "
f"{MAX_FUNCTION_ARGUMENTS} arguments"
)
arguments.append(self._parse_additive(depth))
if self._current().kind != "comma":
break
self._advance()
if self._current().kind == "right_parenthesis":
raise ParameterExpressionError(
f"function {source_name} has no argument after its comma"
)
self._expect(
"right_parenthesis",
f"function {source_name} is missing a closing parenthesis",
)
self._count_operation()
return _evaluate_function(normalized_name, source_name, arguments)
def _current(self) -> _Token:
return self._tokens[min(self._index, len(self._tokens) - 1)]
def _advance(self) -> _Token:
token = self._current()
if token.kind != "end":
self._index += 1
return token
def _expect(self, kind: str, message: str) -> _Token:
if self._current().kind != kind:
raise ParameterExpressionError(message)
return self._advance()
def _is_operator(self, operator: str) -> bool:
token = self._current()
return token.kind == "operator" and token.text == operator
def _assert_depth(self, depth: int) -> None:
if depth > MAX_NESTING_DEPTH:
raise ParameterExpressionError(
f"expression nesting must not exceed {MAX_NESTING_DEPTH} levels"
)
def _count_operation(self) -> None:
self._operation_count += 1
if self._operation_count > MAX_OPERATION_COUNT:
raise ParameterExpressionError(
f"expression must not exceed {MAX_OPERATION_COUNT} operations"
)
def _evaluate_function(
normalized_name: str,
source_name: str,
arguments: list[float],
) -> float:
def require_count(expected: int) -> None:
if len(arguments) != expected:
raise ParameterExpressionError(
f"function {source_name} requires {expected} arguments, "
f"received {len(arguments)}"
)
if normalized_name == "sqrt":
require_count(1)
if arguments[0] < 0:
raise ParameterExpressionError("sqrt argument must not be negative")
operation = lambda: math.sqrt(arguments[0])
elif normalized_name == "abs":
require_count(1)
operation = lambda: abs(arguments[0])
elif normalized_name in {"sin", "cos", "tan", "asin", "acos", "atan"}:
require_count(1)
if normalized_name in {"asin", "acos"} and not -1 <= arguments[0] <= 1:
raise ParameterExpressionError(
f"{source_name} argument must be between -1 and 1"
)
function = getattr(math, normalized_name)
operation = lambda: function(arguments[0])
elif normalized_name == "exp":
require_count(1)
operation = lambda: math.exp(arguments[0])
elif normalized_name in {"ln", "log"}:
require_count(1)
if arguments[0] <= 0:
raise ParameterExpressionError(f"{source_name} argument must be positive")
operation = lambda: math.log(arguments[0])
elif normalized_name == "log10":
require_count(1)
if arguments[0] <= 0:
raise ParameterExpressionError("log10 argument must be positive")
operation = lambda: math.log10(arguments[0])
elif normalized_name in {"min", "max"}:
if not arguments:
raise ParameterExpressionError(
f"function {source_name} requires at least one argument"
)
function = min if normalized_name == "min" else max
operation = lambda: float(function(arguments))
elif normalized_name == "pow":
require_count(2)
operation = lambda: math.pow(arguments[0], arguments[1])
else:
raise ParameterExpressionError(f"unsupported function {source_name!r}")
return _safe_operation(operation, f"function {source_name}")
def _safe_operation(operation: Callable[[], float], context: str) -> float:
try:
value = operation()
except (ArithmeticError, ValueError) as exc:
raise ParameterExpressionError(f"{context} has no finite real result") from exc
return _ensure_finite(float(value), context)
def _ensure_finite(value: float, context: str) -> float:
if not math.isfinite(value):
raise ParameterExpressionError(f"{context} is not finite")
return value
# Ordered exactly like the editor's unit selector. The first entry is the
# fallback SI unit when a persisted selection is absent or unknown.
_UNIT_CONVERSIONS: dict[str, dict[str, tuple[float, float]]] = {
"area": {"m2": (1.0, 0.0), "cm2": (1.0e-4, 0.0), "mm2": (1.0e-6, 0.0)},
"heat_transfer_coefficient": {"W/(m2*K)": (1.0, 0.0)},
"pressure": {
"Pa": (1.0, 0.0),
"kPa": (1.0e3, 0.0),
"MPa": (1.0e6, 0.0),
"bar": (1.0e5, 0.0),
},
"volume": {"m3": (1.0, 0.0), "L": (1.0e-3, 0.0), "mL": (1.0e-6, 0.0)},
"temperature": {"K": (1.0, 0.0), "degC": (1.0, 273.15)},
"length": {"m": (1.0, 0.0), "cm": (1.0e-2, 0.0), "mm": (1.0e-3, 0.0)},
}
+40
View File
@@ -0,0 +1,40 @@
"""Whole-system numeric intermediate representation."""
from .compiler import compile_system_ir
from .schema import * # noqa: F403 - this package is the public schema facade.
from .schema import __dict__ as _schema_namespace
from .validation import (
IRValidationIssue,
IRValidationReport,
SystemIRValidationError,
require_valid_system_ir,
validate_system_ir,
)
__all__ = [
"compile_system_ir",
"IRValidationIssue",
"IRValidationReport",
"SystemIRValidationError",
"require_valid_system_ir",
"validate_system_ir",
*sorted(
name
for name in _schema_namespace
if name.startswith("IR")
or name.startswith("SystemIR")
or name.startswith("SYSTEM_NUMERIC_IR")
or name
in {
"CURRENT_SYSTEM_IR_VERSION",
"NATIVE_NUMERIC_ABI_VERSION",
"canonical_json_bytes",
"native_artifact_key",
"operation_read_slots",
"operation_write_slots",
}
),
]
del _schema_namespace
File diff suppressed because it is too large. Load diff
+973
View File
@@ -0,0 +1,973 @@
"""Callback-free data contract for the whole-system numeric IR.
System IR v2 describes a compiled simulation model. It is intentionally a
pure, immutable data graph: Python functions, model objects, object addresses,
and run-local diagnostic state are not part of this module's wire contract.
"""
from __future__ import annotations
from dataclasses import dataclass, fields, is_dataclass
from enum import StrEnum
from hashlib import sha256
import json
from math import isfinite
from struct import pack
from typing import ClassVar
from unicodedata import normalize
SYSTEM_NUMERIC_IR_SCHEMA_ID = "system-numeric-ir"
SYSTEM_NUMERIC_IR_SCHEMA_MAJOR = 2
SYSTEM_NUMERIC_IR_SCHEMA_MINOR = 0
SYSTEM_NUMERIC_IR_COMPILER_ID = "generic-fluid-system"
SYSTEM_NUMERIC_IR_COMPILER_VERSION = "2.0.0"
NATIVE_NUMERIC_ABI_VERSION = 1
class IRDType(StrEnum):
FLOAT64 = "float64"
INT32 = "int32"
class IRBufferKind(StrEnum):
TIME = "time"
STATE_INPUT = "state_input"
DERIVATIVE_OUTPUT = "derivative_output"
LOCAL_STATE = "local_state"
LOCAL_DERIVATIVE = "local_derivative"
ALGEBRAIC = "algebraic"
SIGNAL = "signal"
PARAMETER = "parameter"
CONSTANT = "constant"
MODE = "mode"
WORK_FLOAT = "work_float"
WORK_INT = "work_int"
EVENT_OUTPUT = "event_output"
JACOBIAN_VALUE = "jacobian_value"
RESULT_OUTPUT = "result_output"
RUNTIME_INPUT = "runtime_input"
class IRKernelPhase(StrEnum):
PRIMAL = "primal"
RESIDUAL = "residual"
DERIVATIVE = "derivative"
PROPERTY = "property"
EVENT = "event"
RESET = "reset"
JACOBIAN = "jacobian"
OUTPUT = "output"
class IRKernelAvailability(StrEnum):
NATIVE = "native"
REFERENCE_ONLY = "reference_only"
class IRPortKind(StrEnum):
PHYSICAL = "physical"
SIGNAL = "signal"
class IRPortNominalRole(StrEnum):
INLET = "inlet"
OUTLET = "outlet"
BIDIRECTIONAL = "bidirectional"
INPUT = "input"
OUTPUT = "output"
class IRPositiveFlowDirection(StrEnum):
INTO_COMPONENT = "intoComponent"
class IRVariableRole(StrEnum):
EFFORT = "effort"
FLOW = "flow"
STREAM = "stream"
SIGNAL = "signal"
class IRConnectionRule(StrEnum):
EQUAL = "equal"
SUM_TO_ZERO = "sumToZero"
STREAM_MIX = "streamMix"
DIRECTED = "directed"
class IREquationOwner(StrEnum):
COMPONENT = "component"
CONNECTION = "connection"
class IREquationRelation(StrEnum):
EQUAL = "equal"
SUM_TO_ZERO = "sumToZero"
CONSTITUTIVE = "constitutive"
STATE = "state"
class IRPressureFlowScopeKind(StrEnum):
NETWORK = "network"
PHYSICAL_ISLAND = "physical_island"
EQUATION_BLOCK = "equation_block"
class IRStageKind(StrEnum):
STATE_REDUCE = "state_reduce"
SIGNAL = "signal"
MECHANICAL_EQUIVALENCE = "mechanical_equivalence"
DYNAMIC_VOLUME = "dynamic_volume"
PROPERTY = "property"
PRESSURE_FLOW = "pressure_flow"
STREAM = "stream"
TEMPERATURE_REFERENCE = "temperature_reference"
THERMOFLUID_FIXED_POINT = "thermofluid_fixed_point"
MECHANICAL_ACCELERATION = "mechanical_acceleration"
DERIVATIVE_REDUCE = "derivative_reduce"
EVENT = "event"
JACOBIAN = "jacobian"
OUTPUT = "output"
RESET = "reset"
class IREntryPointKind(StrEnum):
RHS = "rhs"
EVENTS = "events"
JACOBIAN = "jacobian"
OUTPUTS = "outputs"
class IRBlockKind(StrEnum):
SEQUENCE = "sequence"
FIXED_POINT = "fixed_point"
STREAM_SCC = "stream_scc"
class IRStepKind(StrEnum):
STAGE = "stage"
BLOCK = "block"
class IRStateMapKind(StrEnum):
SCATTER = "scatter"
DERIVATIVE_GATHER = "derivative_gather"
class IREventDirection(StrEnum):
DECREASING = "decreasing"
ANY = "any"
INCREASING = "increasing"
class IRFailurePolicy(StrEnum):
FAIL = "fail"
RETRY_SMALLER_STEP = "retry_smaller_step"
class IRCacheKind(StrEnum):
PROPERTY = "property"
PRESSURE_FLOW = "pressure_flow"
STREAM = "stream"
JACOBIAN = "jacobian"
OUTPUT = "output"
class IRCapabilityLevel(StrEnum):
NATIVE = "native"
REFERENCE_ONLY = "reference_only"
UNSUPPORTED = "unsupported"
class IRDiagnosticSeverity(StrEnum):
INFO = "info"
WARNING = "warning"
ERROR = "error"
class IROpcode(StrEnum):
FILL = "fill"
COPY = "copy"
SCATTER = "scatter"
LINEAR_COMBINATION = "linear_combination"
STATE_MAP = "state_map"
KERNEL_CALL = "kernel_call"
EFFORT_BROADCAST = "effort_broadcast"
FLOW_ASSIGN = "flow_assign"
CHECK_FINITE = "check_finite"
@dataclass(frozen=True, slots=True)
class IRSchemaVersion:
schema_id: str = SYSTEM_NUMERIC_IR_SCHEMA_ID
major: int = SYSTEM_NUMERIC_IR_SCHEMA_MAJOR
minor: int = SYSTEM_NUMERIC_IR_SCHEMA_MINOR
CURRENT_SYSTEM_IR_VERSION = IRSchemaVersion()
@dataclass(frozen=True, slots=True)
class IRSlotRef:
buffer: IRBufferKind
index: int
@dataclass(frozen=True, slots=True)
class IRBufferSpec:
kind: IRBufferKind
dtype: IRDType
size: int
initial_float_values: tuple[float, ...] = ()
initial_int_values: tuple[int, ...] = ()
@dataclass(frozen=True, slots=True)
class IRValueSpec:
value_id: str
slot: IRSlotRef
semantic: str
role: str
quantity: str
unit: str
scale: float
lower_bound: float | None = None
upper_bound: float | None = None
owner_component_index: int | None = None
@dataclass(frozen=True, slots=True)
class IRKernelPhaseSpec:
"""Phase capability tag; each call's ordered slot lists define its C-01 ABI.
Fixed component-kernel signatures deliberately belong to the C-02 contract.
Recording placeholder arities here would make the current reference-only
kernels look more strictly specified than they are.
"""
phase: IRKernelPhase
@dataclass(frozen=True, slots=True)
class IRKernelSpec:
kernel_id: str
model_type: str
model_version: str
implementation_version: str
availability: IRKernelAvailability
unavailable_reason: str | None
phases: tuple[IRKernelPhaseSpec, ...]
parameter_count: int
state_count: int
mode_count: int
workspace_float_count: int
workspace_int_count: int
@dataclass(frozen=True, slots=True)
class IRComponentInstance:
instance_id: str
kernel_index: int
parameter_slots: tuple[IRSlotRef, ...]
state_slots: tuple[IRSlotRef, ...]
derivative_slots: tuple[IRSlotRef, ...]
mode_slots: tuple[IRSlotRef, ...]
port_indices: tuple[int, ...]
port_slots: tuple[IRSlotRef, ...]
output_indices: tuple[int, ...]
workspace_float_slots: tuple[IRSlotRef, ...]
workspace_int_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRPortVariable:
variable_id: str
name: str
role: IRVariableRole
connection_rule: IRConnectionRule
quantity: str
unit: str
result_visible: bool
slot: IRSlotRef
@dataclass(frozen=True, slots=True)
class IRPortSpec:
port_id: str
component_index: int
name: str
kind: IRPortKind
domain: str
nominal_role: IRPortNominalRole
positive_flow_direction: IRPositiveFlowDirection | None
variables: tuple[IRPortVariable, ...]
@dataclass(frozen=True, slots=True)
class IRConnectionVariable:
name: str
rule: IRConnectionRule
endpoint_a_slot: IRSlotRef
endpoint_b_slot: IRSlotRef
@dataclass(frozen=True, slots=True)
class IRConnectionSpec:
connection_id: str
kind: IRPortKind
domain: str
endpoint_a_port_index: int
endpoint_b_port_index: int
variables: tuple[IRConnectionVariable, ...]
@dataclass(frozen=True, slots=True)
class IRMediumSpec:
medium_id: str
name: str
implementation_id: str
implementation_version: str
parameter_slots: tuple[IRSlotRef, ...]
component_indices: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IRCSRPattern:
row_count: int
column_count: int
row_pointers: tuple[int, ...]
column_indices: tuple[int, ...]
@property
def nonzero_count(self) -> int:
return len(self.column_indices)
@dataclass(frozen=True, slots=True)
class IRCSRMatrix:
pattern: IRCSRPattern
values: tuple[float, ...]
@dataclass(frozen=True, slots=True)
class IRStateReducer:
solver_state_count: int
local_state_slots: tuple[IRSlotRef, ...]
raw_derivative_slots: tuple[IRSlotRef, ...]
state_scatter: IRCSRMatrix
derivative_gather: IRCSRMatrix
initial_state: tuple[float, ...]
absolute_tolerances: tuple[float, ...]
@dataclass(frozen=True, slots=True)
class IRFillOperation:
opcode: ClassVar[IROpcode] = IROpcode.FILL
target_slots: tuple[IRSlotRef, ...]
value: float
@dataclass(frozen=True, slots=True)
class IRCopyOperation:
opcode: ClassVar[IROpcode] = IROpcode.COPY
source_slot: IRSlotRef
target_slot: IRSlotRef
@dataclass(frozen=True, slots=True)
class IRScatterOperation:
opcode: ClassVar[IROpcode] = IROpcode.SCATTER
source_slot: IRSlotRef
target_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRLinearCombinationOperation:
opcode: ClassVar[IROpcode] = IROpcode.LINEAR_COMBINATION
source_slots: tuple[IRSlotRef, ...]
weights: tuple[float, ...]
target_slot: IRSlotRef
bias: float = 0.0
@dataclass(frozen=True, slots=True)
class IRStateMapOperation:
opcode: ClassVar[IROpcode] = IROpcode.STATE_MAP
map_kind: IRStateMapKind
source_slots: tuple[IRSlotRef, ...]
target_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRKernelCallOperation:
opcode: ClassVar[IROpcode] = IROpcode.KERNEL_CALL
kernel_index: int
component_index: int | None
phase: IRKernelPhase
read_slots: tuple[IRSlotRef, ...]
write_slots: tuple[IRSlotRef, ...]
equation_indices: tuple[int, ...] = ()
@dataclass(frozen=True, slots=True)
class IREffortBroadcastOperation:
opcode: ClassVar[IROpcode] = IROpcode.EFFORT_BROADCAST
variable: str
anchor_slot: IRSlotRef
residual_slot: IRSlotRef
result_slot: IRSlotRef
scatter_slots: tuple[IRSlotRef, ...]
equation_id: str
lower_bound: float | None = None
upper_bound: float | None = None
@dataclass(frozen=True, slots=True)
class IRFlowAssignmentOperation:
opcode: ClassVar[IROpcode] = IROpcode.FLOW_ASSIGN
value_slot: IRSlotRef
result_slot: IRSlotRef
scatter_slots: tuple[IRSlotRef, ...]
equation_id: str
@dataclass(frozen=True, slots=True)
class IRCheckFiniteOperation:
opcode: ClassVar[IROpcode] = IROpcode.CHECK_FINITE
slots: tuple[IRSlotRef, ...]
error_code: str
IROperation = (
IRFillOperation
| IRCopyOperation
| IRScatterOperation
| IRLinearCombinationOperation
| IRStateMapOperation
| IRKernelCallOperation
| IREffortBroadcastOperation
| IRFlowAssignmentOperation
| IRCheckFiniteOperation
)
def operation_read_slots(operation: IROperation) -> tuple[IRSlotRef, ...]:
if isinstance(operation, IRFillOperation):
return ()
if isinstance(operation, (IRCopyOperation, IRScatterOperation)):
return (operation.source_slot,)
if isinstance(operation, (IRLinearCombinationOperation, IRStateMapOperation)):
return operation.source_slots
if isinstance(operation, IRKernelCallOperation):
return operation.read_slots
if isinstance(operation, IREffortBroadcastOperation):
return (operation.anchor_slot, operation.residual_slot)
if isinstance(operation, IRFlowAssignmentOperation):
return (operation.value_slot,)
if isinstance(operation, IRCheckFiniteOperation):
return operation.slots
raise TypeError(f"Unsupported IR operation: {type(operation).__name__}.")
def operation_write_slots(operation: IROperation) -> tuple[IRSlotRef, ...]:
if isinstance(operation, IRFillOperation):
return operation.target_slots
if isinstance(operation, IRCopyOperation):
return (operation.target_slot,)
if isinstance(operation, IRScatterOperation):
return operation.target_slots
if isinstance(operation, IRLinearCombinationOperation):
return (operation.target_slot,)
if isinstance(operation, IRStateMapOperation):
return operation.target_slots
if isinstance(operation, IRKernelCallOperation):
return operation.write_slots
if isinstance(operation, IREffortBroadcastOperation):
return (operation.result_slot, *operation.scatter_slots)
if isinstance(operation, IRFlowAssignmentOperation):
return (operation.result_slot, *operation.scatter_slots)
if isinstance(operation, IRCheckFiniteOperation):
return ()
raise TypeError(f"Unsupported IR operation: {type(operation).__name__}.")
@dataclass(frozen=True, slots=True)
class IRStage:
stage_id: str
kind: IRStageKind
operations: tuple[IROperation, ...]
declared_read_slots: tuple[IRSlotRef, ...]
declared_write_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRStepRef:
kind: IRStepKind
index: int
@dataclass(frozen=True, slots=True)
class IRConvergenceSpec:
monitor_slots: tuple[IRSlotRef, ...]
absolute_tolerance: float
relative_tolerance: float
max_iterations: int
relaxation: float
rollback_slots: tuple[IRSlotRef, ...]
failure_policy: IRFailurePolicy
@dataclass(frozen=True, slots=True)
class IRExecutionBlock:
block_id: str
kind: IRBlockKind
steps: tuple[IRStepRef, ...]
convergence: IRConvergenceSpec | None = None
@dataclass(frozen=True, slots=True)
class IREntryPoint:
kind: IREntryPointKind
steps: tuple[IRStepRef, ...]
input_slots: tuple[IRSlotRef, ...]
output_slots: tuple[IRSlotRef, ...]
@dataclass(frozen=True, slots=True)
class IRCausalEffortStageRef:
variable: str
stage_index: int
@dataclass(frozen=True, slots=True)
class IRCausalPlan:
plan_id: str
scope_component_indices: tuple[int, ...]
source_schema_version: int
source_structural_signature: str | None
fallback_reason: str | None
canonical_slots: tuple[IRSlotRef, ...]
compatibility_slots: tuple[IRSlotRef, ...]
reset_slots: tuple[IRSlotRef, ...]
external_effort_slots: tuple[IRSlotRef, ...]
effort_stages: tuple[IRCausalEffortStageRef, ...]
flow_stage_indices: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IRAlgebraicUnknown:
unknown_id: str
component_index: int
port_index: int
variable: str
role: IRVariableRole
slot: IRSlotRef
scale: float
lower_bound: float | None = None
upper_bound: float | None = None
@dataclass(frozen=True, slots=True)
class IRPressureFlowEquation:
equation_id: str
owner: IREquationOwner
owner_index: int
relation: IREquationRelation
role: IRVariableRole | None
variable_slots: tuple[IRSlotRef, ...]
residual_slot: IRSlotRef
scale: float
@dataclass(frozen=True, slots=True)
class IRPressureFlowBlock:
block_id: str
unknown_indices: tuple[int, ...]
equation_indices: tuple[int, ...]
jacobian_pattern: IRCSRPattern
@dataclass(frozen=True, slots=True)
class IRPressureFlowScope:
scope_id: str
kind: IRPressureFlowScopeKind
component_indices: tuple[int, ...]
unknown_indices: tuple[int, ...]
equation_indices: tuple[int, ...]
block_indices: tuple[int, ...]
causal_plan_index: int | None
residual_tolerance: float
max_evaluations: int
sparse_pattern_trusted: bool
sparse_fallback_reason: str | None
@dataclass(frozen=True, slots=True)
class IRPressureFlowPlan:
unknowns: tuple[IRAlgebraicUnknown, ...]
equations: tuple[IRPressureFlowEquation, ...]
blocks: tuple[IRPressureFlowBlock, ...]
scopes: tuple[IRPressureFlowScope, ...]
global_scope_index: int
secondary_scope_indices: tuple[int, ...]
pressure_lower_bound: float
@dataclass(frozen=True, slots=True)
class IRStreamSCC:
scc_id: str
node_slots: tuple[IRSlotRef, ...]
block_index: int
@dataclass(frozen=True, slots=True)
class IRStreamEdge:
source_scc_index: int
target_scc_index: int
@dataclass(frozen=True, slots=True)
class IRStreamPlan:
plan_id: str
node_slots: tuple[IRSlotRef, ...]
strongly_connected_components: tuple[IRStreamSCC, ...]
condensed_edges: tuple[IRStreamEdge, ...]
topological_scc_indices: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IRThermofluidPlan:
physical_port_indices: tuple[int, ...]
global_component_indices: tuple[int, ...]
stream_plan_index: int
secondary_pressure_scope_indices: tuple[int, ...]
sensitive_component_indices: tuple[int, ...]
maximum_iterations: int
flow_relative_tolerance: float
uses_conservative_global_solver: bool
conservative_fallback_reason: str | None
@dataclass(frozen=True, slots=True)
class IRTransactionPlan:
snapshot_slots: tuple[IRSlotRef, ...]
flow_slots: tuple[IRSlotRef, ...]
cache_component_indices: tuple[int, ...]
cache_attribute_ids: tuple[str, ...]
diagnostic_owner_ids: tuple[str, ...]
restores_on_recoverable_failure: bool
restores_on_fatal_failure: bool
@dataclass(frozen=True, slots=True)
class IRModeValueSpec:
value: int
name: str
@dataclass(frozen=True, slots=True)
class IRModeSpec:
mode_id: str
slot: IRSlotRef
owner_component_indices: tuple[int, ...]
values: tuple[IRModeValueSpec, ...]
initial_value: int
@dataclass(frozen=True, slots=True)
class IRFiniteDifferenceColumn:
column_index: int
value_indices: tuple[int, ...]
relative_step: float
@dataclass(frozen=True, slots=True)
class IRJacobianPlan:
pattern: IRCSRPattern
value_slots: tuple[IRSlotRef, ...]
color_groups: tuple[tuple[int, ...], ...]
fill_steps: tuple[IRStepRef, ...]
analytic_value_indices: tuple[int, ...]
local_finite_difference_columns: tuple[IRFiniteDifferenceColumn, ...]
@dataclass(frozen=True, slots=True)
class IRModeGuard:
mode_slot: IRSlotRef
allowed_values: tuple[int, ...]
@dataclass(frozen=True, slots=True)
class IREventSpec:
event_id: str
event_kind: str
owner_component_indices: tuple[int, ...]
root_slot: IRSlotRef
direction: IREventDirection
terminal: bool
priority: int
mode_guards: tuple[IRModeGuard, ...]
reset_steps: tuple[IRStepRef, ...]
invalidated_caches: tuple[IRCacheKind, ...]
restarts_integrator: bool
@dataclass(frozen=True, slots=True)
class IROutputSpec:
output_id: str
component_index: int
scope: str
port_name: str | None
name: str
label: str
category: str
quantity: str
unit: str
order: int
source_slot: IRSlotRef
output_slot: IRSlotRef
scale: float = 1.0
offset: float = 0.0
@dataclass(frozen=True, slots=True)
class IRComponentCapability:
component_index: int
level: IRCapabilityLevel
supported_phases: tuple[IRKernelPhase, ...]
missing_features: tuple[str, ...]
@dataclass(frozen=True, slots=True)
class IRCapabilityIssue:
code: str
severity: IRDiagnosticSeverity
scope_id: str
message: str
@dataclass(frozen=True, slots=True)
class IRCapabilityReport:
system_level: IRCapabilityLevel
components: tuple[IRComponentCapability, ...]
issues: tuple[IRCapabilityIssue, ...]
@dataclass(frozen=True, slots=True)
class SystemIR:
version: IRSchemaVersion
model_id: str
model_version: str
compiler_id: str
compiler_version: str
numeric_dtype: IRDType
buffers: tuple[IRBufferSpec, ...]
values: tuple[IRValueSpec, ...]
kernels: tuple[IRKernelSpec, ...]
components: tuple[IRComponentInstance, ...]
mediums: tuple[IRMediumSpec, ...]
ports: tuple[IRPortSpec, ...]
connections: tuple[IRConnectionSpec, ...]
state_reducer: IRStateReducer
causal_plans: tuple[IRCausalPlan, ...]
pressure_flow: IRPressureFlowPlan
stream_plans: tuple[IRStreamPlan, ...]
thermofluid: IRThermofluidPlan
stages: tuple[IRStage, ...]
blocks: tuple[IRExecutionBlock, ...]
entry_points: tuple[IREntryPoint, ...]
transaction: IRTransactionPlan
modes: tuple[IRModeSpec, ...]
jacobian: IRJacobianPlan
events: tuple[IREventSpec, ...]
outputs: tuple[IROutputSpec, ...]
capabilities: IRCapabilityReport
required_features: tuple[str, ...] = ()
def canonical_json_bytes(self) -> bytes:
return canonical_json_bytes(self)
def calculate_structural_signature(self) -> str:
return sha256(self.canonical_json_bytes()).hexdigest()
@property
def structural_signature(self) -> str:
return self.calculate_structural_signature()
@dataclass(frozen=True, slots=True)
class IRNativeBuildIdentity:
abi_version: int
target_triple: str
compiler_id: str
compiler_version: str
compile_flags: tuple[str, ...]
floating_point_policy: str
kernel_library_signature: str
@dataclass(frozen=True, slots=True)
class _IRNativeArtifactKeyInput:
program_signature: str
build: IRNativeBuildIdentity
_CANONICAL_TYPE_NAMES: tuple[tuple[type[object], str], ...] = (
(IRSchemaVersion, "schema_version"),
(IRSlotRef, "slot_ref"),
(IRBufferSpec, "buffer"),
(IRValueSpec, "value"),
(IRKernelPhaseSpec, "kernel_phase"),
(IRKernelSpec, "kernel"),
(IRComponentInstance, "component"),
(IRPortVariable, "port_variable"),
(IRPortSpec, "port"),
(IRConnectionVariable, "connection_variable"),
(IRConnectionSpec, "connection"),
(IRMediumSpec, "medium"),
(IRCSRPattern, "csr_pattern"),
(IRCSRMatrix, "csr_matrix"),
(IRStateReducer, "state_reducer"),
(IRFillOperation, "operation"),
(IRCopyOperation, "operation"),
(IRScatterOperation, "operation"),
(IRLinearCombinationOperation, "operation"),
(IRStateMapOperation, "operation"),
(IRKernelCallOperation, "operation"),
(IREffortBroadcastOperation, "operation"),
(IRFlowAssignmentOperation, "operation"),
(IRCheckFiniteOperation, "operation"),
(IRStage, "stage"),
(IRStepRef, "step_ref"),
(IRConvergenceSpec, "convergence"),
(IRExecutionBlock, "block"),
(IREntryPoint, "entry_point"),
(IRCausalEffortStageRef, "causal_effort_stage"),
(IRCausalPlan, "causal_plan"),
(IRAlgebraicUnknown, "algebraic_unknown"),
(IRPressureFlowEquation, "pressure_flow_equation"),
(IRPressureFlowBlock, "pressure_flow_block"),
(IRPressureFlowScope, "pressure_flow_scope"),
(IRPressureFlowPlan, "pressure_flow_plan"),
(IRStreamSCC, "stream_scc"),
(IRStreamEdge, "stream_edge"),
(IRStreamPlan, "stream_plan"),
(IRThermofluidPlan, "thermofluid_plan"),
(IRTransactionPlan, "transaction_plan"),
(IRModeValueSpec, "mode_value"),
(IRModeSpec, "mode"),
(IRFiniteDifferenceColumn, "finite_difference_column"),
(IRJacobianPlan, "jacobian_plan"),
(IRModeGuard, "mode_guard"),
(IREventSpec, "event"),
(IROutputSpec, "output"),
(IRComponentCapability, "component_capability"),
(IRCapabilityIssue, "capability_issue"),
(IRCapabilityReport, "capability_report"),
(SystemIR, "system_ir"),
(IRNativeBuildIdentity, "native_build"),
(_IRNativeArtifactKeyInput, "native_artifact_key_input"),
)
_OPERATION_TYPES = (
IRFillOperation,
IRCopyOperation,
IRScatterOperation,
IRLinearCombinationOperation,
IRStateMapOperation,
IRKernelCallOperation,
IREffortBroadcastOperation,
IRFlowAssignmentOperation,
IRCheckFiniteOperation,
)
def _canonical_type_name(value: object) -> str:
value_type = type(value)
for candidate, name in _CANONICAL_TYPE_NAMES:
if value_type is candidate:
return name
raise TypeError(f"Unsupported IR schema object: {value_type.__name__}.")
def _canonical_float(value: float) -> object:
numeric = float(value)
if not isfinite(numeric):
raise ValueError("Canonical IR JSON does not permit NaN or infinity.")
if numeric == 0.0:
numeric = 0.0
return {"$float64": pack(">d", numeric).hex()}
def _canonical_value(value: object) -> object:
if value is None or isinstance(value, bool):
return value
if isinstance(value, StrEnum):
return value.value
if isinstance(value, int):
return value
if isinstance(value, float):
return _canonical_float(value)
if isinstance(value, str):
return normalize("NFC", value)
if isinstance(value, tuple):
return [_canonical_value(item) for item in value]
if is_dataclass(value) and not isinstance(value, type):
payload: dict[str, object] = {"$type": _canonical_type_name(value)}
if isinstance(value, _OPERATION_TYPES):
payload["opcode"] = value.opcode.value
for item in fields(value):
payload[item.name] = _canonical_value(getattr(value, item.name))
return payload
raise TypeError(
"Canonical IR JSON accepts only schema dataclasses, tuples, enums, and "
f"scalar values; received {type(value).__name__}."
)
def canonical_json_bytes(value: object) -> bytes:
"""Return the exact platform-independent canonical JSON byte sequence."""
return json.dumps(
_canonical_value(value),
ensure_ascii=True,
allow_nan=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def native_artifact_key(
program: SystemIR,
build: IRNativeBuildIdentity,
) -> str:
"""Build cache key; target details never contaminate the program hash."""
if build.abi_version != NATIVE_NUMERIC_ABI_VERSION:
raise ValueError(
"Native build ABI does not match NATIVE_NUMERIC_ABI_VERSION."
)
for field_name, value in (
("target_triple", build.target_triple),
("compiler_id", build.compiler_id),
("compiler_version", build.compiler_version),
("floating_point_policy", build.floating_point_policy),
):
if not value:
raise ValueError(f"Native build {field_name} must not be empty.")
if any(not flag for flag in build.compile_flags):
raise ValueError("Native build flags must not contain empty entries.")
if (
len(build.kernel_library_signature) != 64
or any(
character not in "0123456789abcdef"
for character in build.kernel_library_signature
)
):
raise ValueError(
"Native kernel library signature must be lowercase SHA-256 hex."
)
payload = _IRNativeArtifactKeyInput(program.structural_signature, build)
return sha256(canonical_json_bytes(payload)).hexdigest()
File diff suppressed because it is too large. Load diff
@@ -139,6 +139,8 @@ SIMULATION_NUMERIC_ENGINE=python|native|shadow|auto
#### C-01 完整数值 IR schema v2 #### 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 计划为: 完整 IR 必须描述共享数值阶段,以及 RHS、Event、Jacobian 和输出四类独立的按需入口。它们可以复用同一套槽位和依赖信息,但不能被误实现成“每次 RHS 都顺序计算 Event、Jacobian 和输出”。共享 primal 计划为:
```text ```text
@@ -169,11 +171,12 @@ IR 至少描述:
- stream 图、SCC、物性状态包、外层固定点和事务恢复集合; - stream 图、SCC、物性状态包、外层固定点和事务恢复集合;
- event ID、左/右状态、reset、Jacobian 失效和模式计划; - event ID、左/右状态、reset、Jacobian 失效和模式计划;
- 固定的 CSR Jacobian 结构和 output projection; - 固定的 CSR Jacobian 结构和 output projection;
- IR schema、native ABI、组件模型/实现、介质、编译器、dtype、平台和构建选项组成的结构签名。 - 完整 IR 内容(含组件模型/实现、介质、dtype、参数和执行计划)组成跨平台内容签名;
- native artifact key 再把内容签名与 native ABI、目标平台、编译器、构建选项、浮点策略和 kernel 库签名组合。
原生兼容 program 中不得存在 Python callback。schema v1 保留为参考适配器,不直接扩展成生产 ABI。 原生兼容 program 中不得存在 Python callback。schema v1 保留为参考适配器,不直接扩展成生产 ABI。
完成标准:同一模型重复编译得到字节级稳定的结构和签名;所有索引及读写集合可静态校验;缺少能力时明确拒绝编译。 完成标准:同一模型重复编译得到字节级稳定的结构和签名;所有索引及读写集合可静态校验;结构可完整描述但缺少原生能力时明确标为 `reference_only`,原生加载器必须拒绝接管。
#### C-02 纯数值组件合同 #### C-02 纯数值组件合同
@@ -397,8 +400,8 @@ Numba 可以在完整数组 IR 后用于 1–2 周的架构验证,但不作为
第一批只做 P0,不直接开始大规模 C 编码: 第一批只做 P0,不直接开始大规模 C 编码:
1. **已完成:** 固定当前权威输入和回归证据的 `LF` 检出规则,重新规范化 Windows 工作树,并增加 Windows/Linux 字节合同测试;未重建语义未变的 golden。 1. **已完成:** 固定当前权威输入和回归证据的 `LF` 检出规则,重新规范化 Windows 工作树,并增加 Windows/Linux 字节合同测试;未重建语义未变的 golden。
2. 生成当前模型的组件类型、槽位、阶段、副作用和事件能力矩阵。 2. **已完成(C-01 结构层):** 生成当前模型的组件类型、槽位、阶段、副作用和事件能力描述;纯数值 kernel 能力矩阵的实现细节继续归入 C-02。
3. 将 schema v2 写成独立规范,先冻结 RHS 阶段、错误、事务、事件和结构签名。 3. **已完成:** 将 schema v2 写成独立规范,并冻结 RHS/Event/Jacobian/Outputs 入口、错误能力、事务、事件、Jacobian 和内容签名。
4. 建立对象引擎与扁平 IR 的逐阶段 Shadow runner。 4. 建立对象引擎与扁平 IR 的逐阶段 Shadow runner。
5. 用一条完整机械—气动—管路—接触支路完成 Python 参考闭环。 5. 用一条完整机械—气动—管路—接触支路完成 Python 参考闭环。
6. 评审通过后,再建立最小 C ABI 和纵向切片。 6. 评审通过后,再建立最小 C ABI 和纵向切片。
@@ -45,7 +45,6 @@ FastAPI 自动生成的 OpenAPI 当前可能显示默认 `info.version=0.1.0`;
| 组件库及分类 | 各库 `library.py` | | 组件库及分类 | 各库 `library.py` |
| 组件目录 JSON | `build_component_catalog()` 与目录 JSON Schema | | 组件目录 JSON | `build_component_catalog()` 与目录 JSON Schema |
| System XML | v3 XSD、`app/system_xml.py` | | System XML | v3 XSD、`app/system_xml.py` |
| ReactFlow 参数表达式 | `app/parameter_expression.py`、`frontend/src/parameterExpression.ts` 及相应合同测试 |
| 网络最终连接检查 | `SimulationNetwork.connect()` | | 网络最终连接检查 | `SimulationNetwork.connect()` |
| HTTP 路由和请求模型 | `app/main.py` | | HTTP 路由和请求模型 | `app/main.py` |
@@ -152,13 +151,6 @@ OpenAPI,但当前多数 JSON 响应仍以 `dict[str, object]` 构造,XML、C
- XML 和求解参数统一使用 SI 基准值; - XML 和求解参数统一使用 SI 基准值;
- 实例 ID 和机器标识必须稳定,显示名称不能代替机器标识。 - 实例 ID 和机器标识必须稳定,显示名称不能代替机器标识。
ReactFlow 工程 JSON 的连续数值参数可保存前端既有的受限算术表达式。
编译或 JSON→XML 时,后端在内存中安全求值,再按 `parameterUnits` 从
显示单位换算为 SI。普通数值及数值字符串仍按已存储的 SI 值解释,避免
二次换算;原表达式不回写工程 JSON。离散选项参数和任意代码不属于该合同。
这是补齐已有工程 JSON v1 前端语义的兼容性修复,不改变 System XML v3:
XML 仍只保存最终 SI 数值。
System XML 校验问题统一包含: System XML 校验问题统一包含:
```json ```json
+262
View File
@@ -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`。
+71
View File
@@ -0,0 +1,71 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://systemsimulationapp.local/schema/system-numeric-ir-v2.schema.json",
"title": "System Numeric IR v2",
"description": "Schema for the callback-free canonical JSON emitted by app.simulation.ir.schema.SystemIR.canonical_json_bytes().",
"$ref": "#/$defs/system_ir",
"$defs": {
"float64": {"type":"object","additionalProperties":false,"required":["$float64"],"properties":{"$float64":{"type":"string","pattern":"^(?!8000000000000000$)(?!(?:7ff|fff)[0-9a-f]{13}$)[0-9a-f]{16}$"}}},
"str": {"type":"string"},
"int": {"type":"integer"},
"int32": {"type":"integer","minimum":-2147483648,"maximum":2147483647},
"nonnegative_int": {"type":"integer","minimum":0},
"positive_int": {"type":"integer","minimum":1},
"bool": {"type":"boolean"},
"float_or_null": {"anyOf":[{"$ref":"#/$defs/float64"},{"type":"null"}]},
"str_or_null": {"anyOf":[{"type":"string"},{"type":"null"}]},
"slot_ref": {"type":"object","additionalProperties":false,"required":["$type","buffer","index"],"properties":{"$type":{"const":"slot_ref"},"buffer":{"type":"string","enum":["time","state_input","derivative_output","local_state","local_derivative","algebraic","signal","parameter","constant","mode","work_float","work_int","event_output","jacobian_value","result_output","runtime_input"]},"index":{"$ref":"#/$defs/nonnegative_int"}}},
"schema_version": {"type":"object","additionalProperties":false,"required":["$type","schema_id","major","minor"],"properties":{"$type":{"const":"schema_version"},"schema_id":{"const":"system-numeric-ir"},"major":{"const":2},"minor":{"const":0}}},
"buffer": {"type":"object","additionalProperties":false,"required":["$type","kind","dtype","size","initial_float_values","initial_int_values"],"properties":{"$type":{"const":"buffer"},"kind":{"type":"string","enum":["time","state_input","derivative_output","local_state","local_derivative","algebraic","signal","parameter","constant","mode","work_float","work_int","event_output","jacobian_value","result_output","runtime_input"]},"dtype":{"type":"string","enum":["float64","int32"]},"size":{"$ref":"#/$defs/nonnegative_int"},"initial_float_values":{"type":"array","items":{"$ref":"#/$defs/float64"}},"initial_int_values":{"type":"array","items":{"$ref":"#/$defs/int32"}}}},
"value": {"type":"object","additionalProperties":false,"required":["$type","value_id","slot","semantic","role","quantity","unit","scale","lower_bound","upper_bound","owner_component_index"],"properties":{"$type":{"const":"value"},"value_id":{"type":"string"},"slot":{"$ref":"#/$defs/slot_ref"},"semantic":{"type":"string"},"role":{"type":"string"},"quantity":{"type":"string"},"unit":{"type":"string"},"scale":{"$ref":"#/$defs/float64"},"lower_bound":{"$ref":"#/$defs/float_or_null"},"upper_bound":{"$ref":"#/$defs/float_or_null"},"owner_component_index":{"anyOf":[{"$ref":"#/$defs/nonnegative_int"},{"type":"null"}]} }},
"kernel_phase": {"type":"object","additionalProperties":false,"required":["$type","phase"],"properties":{"$type":{"const":"kernel_phase"},"phase":{"type":"string","enum":["primal","residual","derivative","property","event","reset","jacobian","output"]}}},
"kernel": {"type":"object","additionalProperties":false,"required":["$type","kernel_id","model_type","model_version","implementation_version","availability","unavailable_reason","phases","parameter_count","state_count","mode_count","workspace_float_count","workspace_int_count"],"properties":{"$type":{"const":"kernel"},"kernel_id":{"type":"string"},"model_type":{"type":"string"},"model_version":{"type":"string"},"implementation_version":{"type":"string"},"availability":{"type":"string","enum":["native","reference_only"]},"unavailable_reason":{"$ref":"#/$defs/str_or_null"},"phases":{"type":"array","items":{"$ref":"#/$defs/kernel_phase"}},"parameter_count":{"$ref":"#/$defs/nonnegative_int"},"state_count":{"$ref":"#/$defs/nonnegative_int"},"mode_count":{"$ref":"#/$defs/nonnegative_int"},"workspace_float_count":{"$ref":"#/$defs/nonnegative_int"},"workspace_int_count":{"$ref":"#/$defs/nonnegative_int"}}},
"component": {"type":"object","additionalProperties":false,"required":["$type","instance_id","kernel_index","parameter_slots","state_slots","derivative_slots","mode_slots","port_indices","port_slots","output_indices","workspace_float_slots","workspace_int_slots"],"properties":{"$type":{"const":"component"},"instance_id":{"type":"string"},"kernel_index":{"$ref":"#/$defs/nonnegative_int"},"parameter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"state_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"derivative_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"mode_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"port_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"port_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"output_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"workspace_float_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"workspace_int_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"port_variable": {"type":"object","additionalProperties":false,"required":["$type","variable_id","name","role","connection_rule","quantity","unit","result_visible","slot"],"properties":{"$type":{"const":"port_variable"},"variable_id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string","enum":["effort","flow","stream","signal"]},"connection_rule":{"type":"string","enum":["equal","sumToZero","streamMix","directed"]},"quantity":{"type":"string"},"unit":{"type":"string"},"result_visible":{"type":"boolean"},"slot":{"$ref":"#/$defs/slot_ref"}}},
"port": {"type":"object","additionalProperties":false,"required":["$type","port_id","component_index","name","kind","domain","nominal_role","positive_flow_direction","variables"],"properties":{"$type":{"const":"port"},"port_id":{"type":"string"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"name":{"type":"string"},"kind":{"type":"string","enum":["physical","signal"]},"domain":{"type":"string"},"nominal_role":{"type":"string","enum":["inlet","outlet","bidirectional","input","output"]},"positive_flow_direction":{"anyOf":[{"const":"intoComponent"},{"type":"null"}]},"variables":{"type":"array","items":{"$ref":"#/$defs/port_variable"}}}},
"connection_variable": {"type":"object","additionalProperties":false,"required":["$type","name","rule","endpoint_a_slot","endpoint_b_slot"],"properties":{"$type":{"const":"connection_variable"},"name":{"type":"string"},"rule":{"type":"string","enum":["equal","sumToZero","streamMix","directed"]},"endpoint_a_slot":{"$ref":"#/$defs/slot_ref"},"endpoint_b_slot":{"$ref":"#/$defs/slot_ref"}}},
"connection": {"type":"object","additionalProperties":false,"required":["$type","connection_id","kind","domain","endpoint_a_port_index","endpoint_b_port_index","variables"],"properties":{"$type":{"const":"connection"},"connection_id":{"type":"string"},"kind":{"type":"string","enum":["physical","signal"]},"domain":{"type":"string"},"endpoint_a_port_index":{"$ref":"#/$defs/nonnegative_int"},"endpoint_b_port_index":{"$ref":"#/$defs/nonnegative_int"},"variables":{"type":"array","items":{"$ref":"#/$defs/connection_variable"}}}},
"medium": {"type":"object","additionalProperties":false,"required":["$type","medium_id","name","implementation_id","implementation_version","parameter_slots","component_indices"],"properties":{"$type":{"const":"medium"},"medium_id":{"type":"string"},"name":{"type":"string"},"implementation_id":{"type":"string"},"implementation_version":{"type":"string"},"parameter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"csr_pattern": {"type":"object","additionalProperties":false,"required":["$type","row_count","column_count","row_pointers","column_indices"],"properties":{"$type":{"const":"csr_pattern"},"row_count":{"$ref":"#/$defs/nonnegative_int"},"column_count":{"$ref":"#/$defs/nonnegative_int"},"row_pointers":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"column_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"csr_matrix": {"type":"object","additionalProperties":false,"required":["$type","pattern","values"],"properties":{"$type":{"const":"csr_matrix"},"pattern":{"$ref":"#/$defs/csr_pattern"},"values":{"type":"array","items":{"$ref":"#/$defs/float64"}}}},
"state_reducer": {"type":"object","additionalProperties":false,"required":["$type","solver_state_count","local_state_slots","raw_derivative_slots","state_scatter","derivative_gather","initial_state","absolute_tolerances"],"properties":{"$type":{"const":"state_reducer"},"solver_state_count":{"$ref":"#/$defs/nonnegative_int"},"local_state_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"raw_derivative_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"state_scatter":{"$ref":"#/$defs/csr_matrix"},"derivative_gather":{"$ref":"#/$defs/csr_matrix"},"initial_state":{"type":"array","items":{"$ref":"#/$defs/float64"}},"absolute_tolerances":{"type":"array","items":{"$ref":"#/$defs/float64"}}}},
"operation": {"oneOf":[{"$ref":"#/$defs/fill"},{"$ref":"#/$defs/copy"},{"$ref":"#/$defs/scatter"},{"$ref":"#/$defs/linear_combination"},{"$ref":"#/$defs/state_map"},{"$ref":"#/$defs/kernel_call"},{"$ref":"#/$defs/effort_broadcast"},{"$ref":"#/$defs/flow_assign"},{"$ref":"#/$defs/check_finite"}]},
"fill": {"type":"object","additionalProperties":false,"required":["$type","opcode","target_slots","value"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"fill"},"target_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"value":{"$ref":"#/$defs/float64"}}},
"copy": {"type":"object","additionalProperties":false,"required":["$type","opcode","source_slot","target_slot"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"copy"},"source_slot":{"$ref":"#/$defs/slot_ref"},"target_slot":{"$ref":"#/$defs/slot_ref"}}},
"scatter": {"type":"object","additionalProperties":false,"required":["$type","opcode","source_slot","target_slots"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"scatter"},"source_slot":{"$ref":"#/$defs/slot_ref"},"target_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"linear_combination": {"type":"object","additionalProperties":false,"required":["$type","opcode","source_slots","weights","target_slot","bias"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"linear_combination"},"source_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"weights":{"type":"array","items":{"$ref":"#/$defs/float64"}},"target_slot":{"$ref":"#/$defs/slot_ref"},"bias":{"$ref":"#/$defs/float64"}}},
"state_map": {"type":"object","additionalProperties":false,"required":["$type","opcode","map_kind","source_slots","target_slots"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"state_map"},"map_kind":{"type":"string","enum":["scatter","derivative_gather"]},"source_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"target_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"kernel_call": {"type":"object","additionalProperties":false,"required":["$type","opcode","kernel_index","component_index","phase","read_slots","write_slots","equation_indices"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"kernel_call"},"kernel_index":{"$ref":"#/$defs/nonnegative_int"},"component_index":{"anyOf":[{"$ref":"#/$defs/nonnegative_int"},{"type":"null"}]},"phase":{"type":"string","enum":["primal","residual","derivative","property","event","reset","jacobian","output"]},"read_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"write_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"equation_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"effort_broadcast": {"type":"object","additionalProperties":false,"required":["$type","opcode","variable","anchor_slot","residual_slot","result_slot","scatter_slots","equation_id","lower_bound","upper_bound"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"effort_broadcast"},"variable":{"type":"string"},"anchor_slot":{"$ref":"#/$defs/slot_ref"},"residual_slot":{"$ref":"#/$defs/slot_ref"},"result_slot":{"$ref":"#/$defs/slot_ref"},"scatter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"equation_id":{"type":"string"},"lower_bound":{"$ref":"#/$defs/float_or_null"},"upper_bound":{"$ref":"#/$defs/float_or_null"}}},
"flow_assign": {"type":"object","additionalProperties":false,"required":["$type","opcode","value_slot","result_slot","scatter_slots","equation_id"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"flow_assign"},"value_slot":{"$ref":"#/$defs/slot_ref"},"result_slot":{"$ref":"#/$defs/slot_ref"},"scatter_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"equation_id":{"type":"string"}}},
"check_finite": {"type":"object","additionalProperties":false,"required":["$type","opcode","slots","error_code"],"properties":{"$type":{"const":"operation"},"opcode":{"const":"check_finite"},"slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"error_code":{"type":"string"}}},
"stage": {"type":"object","additionalProperties":false,"required":["$type","stage_id","kind","operations","declared_read_slots","declared_write_slots"],"properties":{"$type":{"const":"stage"},"stage_id":{"type":"string"},"kind":{"type":"string","enum":["state_reduce","signal","mechanical_equivalence","dynamic_volume","property","pressure_flow","stream","temperature_reference","thermofluid_fixed_point","mechanical_acceleration","derivative_reduce","event","jacobian","output","reset"]},"operations":{"type":"array","items":{"$ref":"#/$defs/operation"}},"declared_read_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"declared_write_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"step_ref": {"type":"object","additionalProperties":false,"required":["$type","kind","index"],"properties":{"$type":{"const":"step_ref"},"kind":{"type":"string","enum":["stage","block"]},"index":{"$ref":"#/$defs/nonnegative_int"}}},
"convergence": {"type":"object","additionalProperties":false,"required":["$type","monitor_slots","absolute_tolerance","relative_tolerance","max_iterations","relaxation","rollback_slots","failure_policy"],"properties":{"$type":{"const":"convergence"},"monitor_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"absolute_tolerance":{"$ref":"#/$defs/float64"},"relative_tolerance":{"$ref":"#/$defs/float64"},"max_iterations":{"$ref":"#/$defs/positive_int"},"relaxation":{"$ref":"#/$defs/float64"},"rollback_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"failure_policy":{"type":"string","enum":["fail","retry_smaller_step"]}}},
"block": {"type":"object","additionalProperties":false,"required":["$type","block_id","kind","steps","convergence"],"properties":{"$type":{"const":"block"},"block_id":{"type":"string"},"kind":{"type":"string","enum":["sequence","fixed_point","stream_scc"]},"steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"convergence":{"anyOf":[{"$ref":"#/$defs/convergence"},{"type":"null"}]} }},
"entry_point": {"type":"object","additionalProperties":false,"required":["$type","kind","steps","input_slots","output_slots"],"properties":{"$type":{"const":"entry_point"},"kind":{"type":"string","enum":["rhs","events","jacobian","outputs"]},"steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"input_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"output_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}}}},
"causal_effort_stage": {"type":"object","additionalProperties":false,"required":["$type","variable","stage_index"],"properties":{"$type":{"const":"causal_effort_stage"},"variable":{"type":"string"},"stage_index":{"$ref":"#/$defs/nonnegative_int"}}},
"causal_plan": {"type":"object","additionalProperties":false,"required":["$type","plan_id","scope_component_indices","source_schema_version","source_structural_signature","fallback_reason","canonical_slots","compatibility_slots","reset_slots","external_effort_slots","effort_stages","flow_stage_indices"],"properties":{"$type":{"const":"causal_plan"},"plan_id":{"type":"string"},"scope_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"source_schema_version":{"$ref":"#/$defs/nonnegative_int"},"source_structural_signature":{"$ref":"#/$defs/str_or_null"},"fallback_reason":{"$ref":"#/$defs/str_or_null"},"canonical_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"compatibility_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"reset_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"external_effort_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"effort_stages":{"type":"array","items":{"$ref":"#/$defs/causal_effort_stage"}},"flow_stage_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"algebraic_unknown": {"type":"object","additionalProperties":false,"required":["$type","unknown_id","component_index","port_index","variable","role","slot","scale","lower_bound","upper_bound"],"properties":{"$type":{"const":"algebraic_unknown"},"unknown_id":{"type":"string"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"port_index":{"$ref":"#/$defs/nonnegative_int"},"variable":{"type":"string"},"role":{"type":"string","enum":["effort","flow","stream","signal"]},"slot":{"$ref":"#/$defs/slot_ref"},"scale":{"$ref":"#/$defs/float64"},"lower_bound":{"$ref":"#/$defs/float_or_null"},"upper_bound":{"$ref":"#/$defs/float_or_null"}}},
"pressure_flow_equation": {"type":"object","additionalProperties":false,"required":["$type","equation_id","owner","owner_index","relation","role","variable_slots","residual_slot","scale"],"properties":{"$type":{"const":"pressure_flow_equation"},"equation_id":{"type":"string"},"owner":{"type":"string","enum":["component","connection"]},"owner_index":{"$ref":"#/$defs/nonnegative_int"},"relation":{"type":"string","enum":["equal","sumToZero","constitutive","state"]},"role":{"anyOf":[{"type":"string","enum":["effort","flow","stream","signal"]},{"type":"null"}]},"variable_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"residual_slot":{"$ref":"#/$defs/slot_ref"},"scale":{"$ref":"#/$defs/float64"}}},
"pressure_flow_block": {"type":"object","additionalProperties":false,"required":["$type","block_id","unknown_indices","equation_indices","jacobian_pattern"],"properties":{"$type":{"const":"pressure_flow_block"},"block_id":{"type":"string"},"unknown_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"equation_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"jacobian_pattern":{"$ref":"#/$defs/csr_pattern"}}},
"pressure_flow_scope": {"type":"object","additionalProperties":false,"required":["$type","scope_id","kind","component_indices","unknown_indices","equation_indices","block_indices","causal_plan_index","residual_tolerance","max_evaluations","sparse_pattern_trusted","sparse_fallback_reason"],"properties":{"$type":{"const":"pressure_flow_scope"},"scope_id":{"type":"string"},"kind":{"type":"string","enum":["network","physical_island","equation_block"]},"component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"unknown_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"equation_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"block_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"causal_plan_index":{"anyOf":[{"$ref":"#/$defs/nonnegative_int"},{"type":"null"}]},"residual_tolerance":{"$ref":"#/$defs/float64"},"max_evaluations":{"$ref":"#/$defs/positive_int"},"sparse_pattern_trusted":{"type":"boolean"},"sparse_fallback_reason":{"$ref":"#/$defs/str_or_null"}}},
"pressure_flow_plan": {"type":"object","additionalProperties":false,"required":["$type","unknowns","equations","blocks","scopes","global_scope_index","secondary_scope_indices","pressure_lower_bound"],"properties":{"$type":{"const":"pressure_flow_plan"},"unknowns":{"type":"array","items":{"$ref":"#/$defs/algebraic_unknown"}},"equations":{"type":"array","items":{"$ref":"#/$defs/pressure_flow_equation"}},"blocks":{"type":"array","items":{"$ref":"#/$defs/pressure_flow_block"}},"scopes":{"type":"array","items":{"$ref":"#/$defs/pressure_flow_scope"}},"global_scope_index":{"$ref":"#/$defs/nonnegative_int"},"secondary_scope_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"pressure_lower_bound":{"$ref":"#/$defs/float64"}}},
"stream_scc": {"type":"object","additionalProperties":false,"required":["$type","scc_id","node_slots","block_index"],"properties":{"$type":{"const":"stream_scc"},"scc_id":{"type":"string"},"node_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"block_index":{"$ref":"#/$defs/nonnegative_int"}}},
"stream_edge": {"type":"object","additionalProperties":false,"required":["$type","source_scc_index","target_scc_index"],"properties":{"$type":{"const":"stream_edge"},"source_scc_index":{"$ref":"#/$defs/nonnegative_int"},"target_scc_index":{"$ref":"#/$defs/nonnegative_int"}}},
"stream_plan": {"type":"object","additionalProperties":false,"required":["$type","plan_id","node_slots","strongly_connected_components","condensed_edges","topological_scc_indices"],"properties":{"$type":{"const":"stream_plan"},"plan_id":{"type":"string"},"node_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"strongly_connected_components":{"type":"array","items":{"$ref":"#/$defs/stream_scc"}},"condensed_edges":{"type":"array","items":{"$ref":"#/$defs/stream_edge"}},"topological_scc_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}}},
"thermofluid_plan": {"type":"object","additionalProperties":false,"required":["$type","physical_port_indices","global_component_indices","stream_plan_index","secondary_pressure_scope_indices","sensitive_component_indices","maximum_iterations","flow_relative_tolerance","uses_conservative_global_solver","conservative_fallback_reason"],"properties":{"$type":{"const":"thermofluid_plan"},"physical_port_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"global_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"stream_plan_index":{"$ref":"#/$defs/nonnegative_int"},"secondary_pressure_scope_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"sensitive_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"maximum_iterations":{"$ref":"#/$defs/positive_int"},"flow_relative_tolerance":{"$ref":"#/$defs/float64"},"uses_conservative_global_solver":{"type":"boolean"},"conservative_fallback_reason":{"$ref":"#/$defs/str_or_null"}}},
"transaction_plan": {"type":"object","additionalProperties":false,"required":["$type","snapshot_slots","flow_slots","cache_component_indices","cache_attribute_ids","diagnostic_owner_ids","restores_on_recoverable_failure","restores_on_fatal_failure"],"properties":{"$type":{"const":"transaction_plan"},"snapshot_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"flow_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"cache_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"cache_attribute_ids":{"type":"array","items":{"type":"string"}},"diagnostic_owner_ids":{"type":"array","items":{"type":"string"}},"restores_on_recoverable_failure":{"type":"boolean"},"restores_on_fatal_failure":{"type":"boolean"}}},
"mode_value": {"type":"object","additionalProperties":false,"required":["$type","value","name"],"properties":{"$type":{"const":"mode_value"},"value":{"$ref":"#/$defs/int32"},"name":{"type":"string"}}},
"mode": {"type":"object","additionalProperties":false,"required":["$type","mode_id","slot","owner_component_indices","values","initial_value"],"properties":{"$type":{"const":"mode"},"mode_id":{"type":"string"},"slot":{"$ref":"#/$defs/slot_ref"},"owner_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"values":{"type":"array","items":{"$ref":"#/$defs/mode_value"}},"initial_value":{"$ref":"#/$defs/int32"}}},
"finite_difference_column": {"type":"object","additionalProperties":false,"required":["$type","column_index","value_indices","relative_step"],"properties":{"$type":{"const":"finite_difference_column"},"column_index":{"$ref":"#/$defs/nonnegative_int"},"value_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"relative_step":{"$ref":"#/$defs/float64"}}},
"jacobian_plan": {"type":"object","additionalProperties":false,"required":["$type","pattern","value_slots","color_groups","fill_steps","analytic_value_indices","local_finite_difference_columns"],"properties":{"$type":{"const":"jacobian_plan"},"pattern":{"$ref":"#/$defs/csr_pattern"},"value_slots":{"type":"array","items":{"$ref":"#/$defs/slot_ref"}},"color_groups":{"type":"array","items":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}}},"fill_steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"analytic_value_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"local_finite_difference_columns":{"type":"array","items":{"$ref":"#/$defs/finite_difference_column"}}}},
"mode_guard": {"type":"object","additionalProperties":false,"required":["$type","mode_slot","allowed_values"],"properties":{"$type":{"const":"mode_guard"},"mode_slot":{"$ref":"#/$defs/slot_ref"},"allowed_values":{"type":"array","items":{"$ref":"#/$defs/int32"}}}},
"event": {"type":"object","additionalProperties":false,"required":["$type","event_id","event_kind","owner_component_indices","root_slot","direction","terminal","priority","mode_guards","reset_steps","invalidated_caches","restarts_integrator"],"properties":{"$type":{"const":"event"},"event_id":{"type":"string"},"event_kind":{"type":"string"},"owner_component_indices":{"type":"array","items":{"$ref":"#/$defs/nonnegative_int"}},"root_slot":{"$ref":"#/$defs/slot_ref"},"direction":{"type":"string","enum":["decreasing","any","increasing"]},"terminal":{"type":"boolean"},"priority":{"type":"integer"},"mode_guards":{"type":"array","items":{"$ref":"#/$defs/mode_guard"}},"reset_steps":{"type":"array","items":{"$ref":"#/$defs/step_ref"}},"invalidated_caches":{"type":"array","items":{"type":"string","enum":["property","pressure_flow","stream","jacobian","output"]}},"restarts_integrator":{"type":"boolean"}}},
"output": {"type":"object","additionalProperties":false,"required":["$type","output_id","component_index","scope","port_name","name","label","category","quantity","unit","order","source_slot","output_slot","scale","offset"],"properties":{"$type":{"const":"output"},"output_id":{"type":"string"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"scope":{"type":"string"},"port_name":{"$ref":"#/$defs/str_or_null"},"name":{"type":"string"},"label":{"type":"string"},"category":{"type":"string"},"quantity":{"type":"string"},"unit":{"type":"string"},"order":{"$ref":"#/$defs/nonnegative_int"},"source_slot":{"$ref":"#/$defs/slot_ref"},"output_slot":{"$ref":"#/$defs/slot_ref"},"scale":{"$ref":"#/$defs/float64"},"offset":{"$ref":"#/$defs/float64"}}},
"component_capability": {"type":"object","additionalProperties":false,"required":["$type","component_index","level","supported_phases","missing_features"],"properties":{"$type":{"const":"component_capability"},"component_index":{"$ref":"#/$defs/nonnegative_int"},"level":{"type":"string","enum":["native","reference_only","unsupported"]},"supported_phases":{"type":"array","items":{"type":"string","enum":["primal","residual","derivative","property","event","reset","jacobian","output"]}},"missing_features":{"type":"array","items":{"type":"string"}}}},
"capability_issue": {"type":"object","additionalProperties":false,"required":["$type","code","severity","scope_id","message"],"properties":{"$type":{"const":"capability_issue"},"code":{"type":"string"},"severity":{"type":"string","enum":["info","warning","error"]},"scope_id":{"type":"string"},"message":{"type":"string"}}},
"capability_report": {"type":"object","additionalProperties":false,"required":["$type","system_level","components","issues"],"properties":{"$type":{"const":"capability_report"},"system_level":{"type":"string","enum":["native","reference_only","unsupported"]},"components":{"type":"array","items":{"$ref":"#/$defs/component_capability"}},"issues":{"type":"array","items":{"$ref":"#/$defs/capability_issue"}}}},
"system_ir": {"type":"object","additionalProperties":false,"required":["$type","version","model_id","model_version","compiler_id","compiler_version","numeric_dtype","buffers","values","kernels","components","mediums","ports","connections","state_reducer","causal_plans","pressure_flow","stream_plans","thermofluid","stages","blocks","entry_points","transaction","modes","jacobian","events","outputs","capabilities","required_features"],"properties":{"$type":{"const":"system_ir"},"version":{"$ref":"#/$defs/schema_version"},"model_id":{"type":"string"},"model_version":{"type":"string"},"compiler_id":{"type":"string"},"compiler_version":{"type":"string"},"numeric_dtype":{"const":"float64"},"buffers":{"type":"array","items":{"$ref":"#/$defs/buffer"}},"values":{"type":"array","items":{"$ref":"#/$defs/value"}},"kernels":{"type":"array","items":{"$ref":"#/$defs/kernel"}},"components":{"type":"array","items":{"$ref":"#/$defs/component"}},"mediums":{"type":"array","items":{"$ref":"#/$defs/medium"}},"ports":{"type":"array","items":{"$ref":"#/$defs/port"}},"connections":{"type":"array","items":{"$ref":"#/$defs/connection"}},"state_reducer":{"$ref":"#/$defs/state_reducer"},"causal_plans":{"type":"array","items":{"$ref":"#/$defs/causal_plan"}},"pressure_flow":{"$ref":"#/$defs/pressure_flow_plan"},"stream_plans":{"type":"array","items":{"$ref":"#/$defs/stream_plan"}},"thermofluid":{"$ref":"#/$defs/thermofluid_plan"},"stages":{"type":"array","items":{"$ref":"#/$defs/stage"}},"blocks":{"type":"array","items":{"$ref":"#/$defs/block"}},"entry_points":{"type":"array","items":{"$ref":"#/$defs/entry_point"}},"transaction":{"$ref":"#/$defs/transaction_plan"},"modes":{"type":"array","items":{"$ref":"#/$defs/mode"}},"jacobian":{"$ref":"#/$defs/jacobian_plan"},"events":{"type":"array","items":{"$ref":"#/$defs/event"}},"outputs":{"type":"array","items":{"$ref":"#/$defs/output"}},"capabilities":{"$ref":"#/$defs/capability_report"},"required_features":{"type":"array","items":{"type":"string"}}}}
}
}
-55
View File
@@ -1,55 +0,0 @@
---
name: system-simulation
description: 读取、校验并简要解释 SystemSimulationApp 工程 JSON v1 或 System XML v3,安全规范化文件文本,运行并监视仿真、导出结果,以及在用户确认计划后对 JSON v1 执行单目标、有界连续 SI 参数优化。适用于检查模型、修复编码或换行、运行仿真、获取结果和优化结果统计量;不用于旧格式迁移、任意语义修复、离散或拓扑优化、多目标优化或网页自动预装。
metadata:
openclaw:
requires:
bins: [python3.12]
---
# 系统仿真
使用本 Skill 随附的确定性脚本检查模型、调用现有后端并保存结果;不要让语言模型自行重写模型或猜测求解数据。`simulation_skill.py` 处理文件和单次仿真,同一 Skill 内的独立入口 `optimization_skill.py` 处理优化计划与执行。
## 基本边界
- 仅处理 ReactFlow 工程 JSON v1 和 System XML v3。版本缺失、不受支持或模型版本不匹配时,说明问题并停止,不进行迁移猜测。
- 组件参数是仿真前设定的固定输入;结果变量才是可随时间绘制的量。不要把“参数”当成结果曲线。
- 工程 JSON 可在连续数值参数中保存受限算术表达式。检查、编译或生成 XML 时由后端安全求值并换算为 SI;不得把计算结果回写到源 JSON。
- 文件通过格式校验不等于物理系统一定可求解。不要隐瞒编译或运行阶段的诊断。
- 不直接覆盖源文件。普通检查或仿真不自行修改参数、连接、组件类型、模型版本或求解设置;优化也只能在用户确认的派生副本中改变明确选定的参数。
- 优化仅面向 ReactFlow 工程 JSON v1,设计变量必须由用户指定,或由用户明确授权 Skill 提议后再纳入计划;它们必须是连续、线性 SI 参数。现有后端不负责证明参数连续性,不能只因字段是数字就自动选作设计变量。带编辑器、离散选项或后端显式否决的参数必须拒绝,整数、条件显示控制量及会改变活动端口、模式或拓扑的参数不得进入连续优化。
- 本版不支持把模型自动注入网页、生成可直接打开的预装页面、任意损坏文件修复、模型迁移、离散或拓扑优化以及多目标优化。不要用手工网页操作冒充支持。
处理文件、解释格式或选择结果变量时,读取 [references/file-contracts.md](references/file-contracts.md)。请求文件修复时,再读取 [references/repair-policy.md](references/repair-policy.md)。需要运行、监视、取消仿真或交付结果时,读取 [references/workflows.md](references/workflows.md)。用户请求按仿真结果优化参数时,必须读取 [references/optimization-workflow.md](references/optimization-workflow.md)。
## 工作原则
1. 先用 `inspect` 确认输入格式、版本、结构和诊断,再基于检查结果简要解释组件、连接与仿真设置。
2. 如果用户要求修复,只能执行文本规范化。先展示预览和源文件 SHA-256,获得针对该预览的明确确认后,才可写入另一个输出路径;随后重新 `inspect`。
3. 单次仿真前必须让用户选择直接曲线查看方式,并解析具体结果变量:
- 分别查看所选变量;
- 将多个同单位、可比较的变量叠加;
- 将不同物理量或单位的变量上下排列。
4. 用户用显示名称描述组件或变量时,利用检查结果中的稳定 ID、结果 `key`、物理量和单位消歧。存在重名、多个候选或“参数/结果变量”含义不清时,先询问,不能替用户猜。
5. 使用 `simulate` 的事件流持续判断 queued、validating、compiling、integrating 和结束状态。仿真时间暂时不变但内部活动仍增长时,只说明正在处理慢步,不能宣称卡死。
6. 成功运行后交付用户选择的 SVG 曲线和完整 `results.csv`,并简要说明完成状态、实际仿真终点和重要诊断。失败或取消时交付能够安全生成的部分结果;若运行前即失败而没有 CSV,要明确说明原因。
7. 优化需求优先按自然语言理解:从检查结果补齐稳定结果 `key`、单位和当前参数值,未指定的算法、预算、容差和输出目录采用参考文档中的推荐默认值。不要要求用户填写规格 JSON,也不要追问随机种子、变异因子等已有默认值。`plan --present` 成功后,面向用户展示的设计变量当前值和单位必须直接采用 `presentation.designVariables[].current` 与 `unit`;完整审计计划中的对应字段是 `designVariables[].initial` 与 `unit`。不得根据源 JSON 的 `parameterUnits` 再换算或另行推断。
8. 信息足以形成规格后,直接在内部写入规格并执行无候选仿真的 `plan`,无需先征求生成计划的许可。普通流程必须把完整计划以仅当前用户可读的权限保存到输出目录之外的新内部文件,并让 stdout 只返回展示白名单。计划阶段从回复第一个字起使用用户当前语言并直接展示计划,只列目标、可调参数及范围、约束、仿真预算槽位、搜索启动时限、完整输出位置和重要假设。输出位置必须是 `plan` 返回的完整绝对路径,不用 `...` 缩写。采用默认搜索设置且没有需要用户决策的警告时,只说“采用默认搜索设置”及其执行上限,不显示“无警告”或原始 warnings、算法名称或变体、随机种子、种群、变异/交叉参数、搜索/复验预算拆分、边界处理、端点播种或理论完整代数;若展示视图返回 `nonDefaultSettings`,则必须把其中将被确认的非默认值简明列出。参数明显是连续物理标量时,把连续性作为计划假设,一次整体执行确认即可覆盖,不展示用于作出判断的内部合同字段清单;只有语义确有歧义时才自然地追问。内部声明代码、SHA、`planHash` 和 `confirmationToken` 默认不展示。
9. 生成计划不等于获准执行。只有用户看过计划摘要并明确表示开始后才能传入 `--confirmed`;用户说只要计划、先看计划且暂时不要运行或其他同等表述时,展示计划后直接停住,不在本轮追问是否开始。计划任一实质内容变化都要重新确认。
10. 优化中的失败、取消、停滞或不完整仿真不计算目标分数。最终只对预算内找到的最佳可行候选做一次绕过缓存的完整复验;复验通过前不把候选称为已验证方案。
11. 严格区分搜索停止与候选复验:`verified` 只说明最佳搜索候选的新鲜复验通过,不等于搜索收敛、系统达到稳态或全局最优。优化执行完成后的结果报告分别说明搜索候选评估、按阶段拆分的仿真预算槽位占用及完成/失败记录、未形成试验记录的槽位、缓存命中、独立复验、未用预算和真实停止原因;这些审计明细不属于计划摘要,槽位占用也不能说成后端已接收或已完成。只要 `searchConvergenceEstablished` 为 `false`,计划、进度和结果中都不得说搜索“已收敛”“将收敛”或“大概率收敛”;若只想表达重复运行可能得到相同结果,改说“可能再次找到同一候选”。内部防死循环上限及重复停滞后的种群塌缩都不得解释成收敛。
12. 目标使用 `final` 时必须报告脚本给出的末段趋势诊断状态;只有完整且覆盖计划终点的新鲜序列才能分析趋势,诊断不可用或样本不足时明确说明且不自行推断。检测到明显变化时,说明终点值只是快照。有限样本只能表述为“在这些已评估点中,参数增大时结果均增大或均减小”,不得称整个范围单调,也不得外推样本之间或未采样位置。新鲜复验已经通过后不再建议重复同一复验;最佳点落在边界时,未经物理、安全和组件合同方面的工程可行性确认,不得建议放宽边界或把它列作默认下一步。
脚本命令统一从仓库根目录运行:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py --help
py -3.12 skills/system-simulation/scripts/optimization_skill.py --help
```
在 OpenClaw 中不要假设当前目录是仓库根目录。使用 `"{baseDir}/scripts/simulation_skill.py"` 或 `"{baseDir}/scripts/optimization_skill.py"`,也可以先进入本 Skill 目录再从 `scripts/` 运行;不要把 `{baseDir}` 当成需要手工猜测的仓库路径。
Windows 优先使用仓库 `.venv-win\Scripts\python.exe`(若存在),否则使用 `py -3.12`;Linux 优先使用 `.venv/bin/python`,否则使用 `python3.12`。不要调用未经版本确认的 `python`,本项目要求 Python 3.12。
优先依赖脚本返回的结构化 JSON/JSONL、稳定错误码和退出码做判断,不解析中文提示文本来驱动下一步。
@@ -1,7 +0,0 @@
interface:
display_name: "系统仿真与参数优化助手"
short_description: "用自然语言检查、仿真模型,并按安全默认值规划连续参数优化"
default_prompt: "使用 $system-simulation 按我描述的目标优化模型参数;请自动补齐安全默认值,先给出易读计划,等我一次确认后执行。"
policy:
allow_implicit_invocation: true
@@ -1,99 +0,0 @@
# 文件合同与解释规则
## 支持范围
本 Skill 只接受以下两种当前格式:
| 格式 | 版本标志 | 用途 |
| --- | --- | --- |
| ReactFlow 工程 JSON | 顶层 `projectSchemaVersion: 1` | 保存组件、画布、端口显示快照、连线和仿真设置,适合继续编辑 |
| System XML | 根元素 `System/@schemaVersion="3"` 且 `unitSystem="SI"` | 保存可执行模型,适合校验、编译和求解 |
默认让 `inspect --format auto` 根据内容和扩展名识别格式。若内容与扩展名不一致、无法唯一识别或用户明确指定格式,则报告实际证据,不悄悄按另一种格式解释。
System XML v1/v2、缺少 `projectSchemaVersion` 的旧工程、字符串端口和不匹配的组件 `modelVersion` 均不属于本 Skill 的迁移范围。不能只改版本号使其看似当前格式。
## 工程 JSON v1
顶层合同为:
```text
projectSchemaVersion = 1
name
nodes[]
edges[]
simulation { t_start, t_stop, step, max_step, method }
```
重要规则:
- 节点的 `id` 是实例稳定标识;显示标签不能替代它。
- `data.modelType` 标识注册模型,`data.modelVersion` 必须与当前组件目录精确匹配,执行前不得自动补成当前版本。
- `data.parameters` 保存输入值;`parameterUnits`、科学计数法偏好、坐标、旋转和镜像属于编辑显示信息。
- 连续数值参数可保存受限算术表达式字符串。支持可选前导 `=`、`+ - * / ^ **`、括号、科学计数法、`pi/e` 和白名单函数 `sqrt/abs/sin/cos/tan/asin/acos/atan/exp/ln/log/log10/min/max/pow`。不支持变量引用、组件间引用、属性访问或任意代码。
- 普通数值及可直接解析的数值字符串按已存储的 SI 值处理;只有表达式的计算结果才按 `parameterUnits` 中的显示单位换算为 SI。例如 `area0 = "3.14*10**2/4"` 且单位为 `mm2` 时,XML 值为 `7.85e-05` m²,JSON 仍保留原表达式。
- 下拉选项、介质引用等离散参数不允许使用表达式。表达式语法、值域或复杂度不合法时,必须在编译/仿真前明确报错,不得猜测或改写。
- 连接必须保留两端组件及 Handle。不能根据节点位置猜测缺失端口。
- `simulation.step` 是结果采样间隔;`max_step` 是求解器内部步长上限,两者不能混用。
工程 JSON 可以导出为 System XML v3,但转换后不会保留全部画布显示信息的对等逆转换合同。
导出时表达式仅在内存中求值,System XML 只写入换算后的 SI 数值,不改动输入工程对象或源 JSON 文件。
## System XML v3
XML v3 只描述“求解什么”:
- 每个 `Component` 必须有唯一 `id`、注册 `type`、精确 `modelVersion` 和完整 SI 参数;
- 每条连接由两个 `Endpoint(component, port)` 组成;端口类型、方向和物理合同由后端注册表恢复;
- `Simulation/@sampleStep` 对应工程 JSON 的 `simulation.step`;
- 不保存组件位置、旋转、镜像、显示单位或端口显示快照;
- 当前后端固定按 v3 校验,不会根据文件内容选择旧解析器。
XML 校验依次覆盖安全/语法、XSD 和语义层。通过这些检查后,编译和求解仍可能发现未连接端口、缺少储能锚点、方程结构或数值问题。
## 简要解释模型
解释必须依据 `inspect` 的结构化输出以及组件目录,而不是仅凭组件名称推测。优先说明:
1. 文件格式、版本和项目名;
2. 仿真起止时间、采样间隔、最大内部步长和算法;
3. 组件数量、稳定 ID、模型类型和主要输入参数;
4. 连接数量、连接端点及能够确定的物理域;
5. 错误、警告,以及它们属于格式、语义、编译还是运行阶段。
保持“文件合同正确”和“物理模型合理”两个结论分开。没有组件文档或注册元数据支持时,不声称某个参数具有推测出的物理效果。
## 参数与结果变量
必须明确区分:
- **参数**:仿真开始前设定的固定输入,例如质量、初始压力、摩擦选项;通常没有时间序列。
- **结果变量**:仿真返回的时间序列,例如位移、速度、压力或流量;只有这类量可以选作曲线。
选择曲线时以结果元数据为准,至少核对:
```text
key + componentId + componentType + label/quantity + unit
```
稳定 `key` 是传给 `simulate --variables` 的最终标识。用户只说“质量块的速度”而存在多个质量块,或一个组件存在多个符合描述的速度结果时,列出候选的组件 ID、结果名称和单位,请用户消歧。
`inspect` 默认对组件摘要、连接和结果变量分页。先读取 `componentTypes` 了解完整模型的组件类型分布,再根据 `componentPage`、`connectionPage` 或 `resultVariablePage` 的 `nextOffset` 翻页。优先使用 `--variable-query` 按组件 ID、标签、物理量或单位缩小范围;只有用户点名组件时才使用 `--component` 读取该组件的完整源数据和可用的编译合同。
组件摘要中的 `compiledForSimulation` 表示该节点是否进入动态求解网络。介质/物性配置节点仍属于工程,因此会保留在组件总数和列表中,但通常标记为 `false`;这不表示组件丢失或编译失败。
曲线模式约束:
- `separate`:每个所选结果变量分别成图;
- `overlay`:只叠加单位相同且含义可比较的结果变量;
- `stacked`:不同物理量或不同单位上下排列,避免共用一个纵轴造成误读。
本版运行 `simulate` 时必须指定至少一个 `--variables` 稳定键,避免在大型模型上无意生成成百上千张曲线。完整 CSV 仍包含全部可用结果变量。
## 权威来源
- 工程 JSON 请求合同:`app/main.py` 中的 `ReactFlowProjectPayload`
- 组件目录:`GET /api/components/catalog`
- XML v3:`schemas/system-simulation-v3.xsd`、`docs/standard/system-xml-v3.md`
- 接口边界:`docs/standard/backend-interface-version-spec-v1.md`
- 结果变量:组件注册合同中的 `RESULT_VARIABLES` 及仿真结果元数据
@@ -1,335 +0,0 @@
# 单目标参数优化工作流
## 执行入口与范围
本 Skill 内的 `scripts/optimization_skill.py` 是独立外层优化入口,它复用同目录的 `simulation_skill.py` 访问现有 FastAPI 后端。优化算法、候选生成、目标统计和报告都在 Skill 进程中完成,不要求后端提供原生优化端点或优化参数准入字段。优化入口只接受已通过检查和编译的 ReactFlow 工程 JSON v1,不接受 System XML 作为优化源文件。
支持范围固定为:
- 一个结果变量统计量构成的单目标;
- 1–16 个有限上下界内的线性尺度连续 SI 参数;
- 0–16 个结果响应约束;
- 最多占用 200 个仿真预算槽位;
- 顺序执行的有界 DE/rand/1/bin 差分进化。
不支持离散、整数、介质引用、条件显示控制量、活动端口数、组件类型、连接、拓扑、无界、对数尺度或多目标优化。不执行用户文本中的任意 Python 回调或表达式,也不在优化中改变仿真时段、采样间隔、最大内部步长、求解方法或物理拓扑。用户给出多个愿望时,必须选定一个目标,再将可用上下限表达的其余要求定义为响应约束;不自行设计权重合成多目标。
## 自然语言交互与推荐默认值
用户不需要了解优化规格 JSON。通常只需说明:想改善哪个结果、让它变大/变小或接近什么值,以及允许调整哪个参数和范围。若用户明确说“你先假定一个”或同等授权,可以提出一个设计变量和工程范围,但必须标成待确认的假设,不能伪装成后端验证结论。
先运行 `inspect` 和必要的编译检查,再把用户说法解析为严格规格:
- “结束时”映射为 `final`;“整个仿真中的最大/最小值”映射为 `maximum` / `minimum`;未指定窗口时使用完整结果时间范围。
- 从结果和参数合同补齐稳定 ID、`resultKey`、SI 单位和当前值;能唯一解析时不要反问用户这些机器字段。
- 用户未提响应约束时使用空列表,不逐项询问“是否需要约束”。
- 目标、参数、范围或方向无法唯一确定时必须询问;不要为了可默认的技术字段打断用户。
用户未指定高级设置时,把下列推荐配置显式写入规格文件,保证计划可复现:
```json
{
"algorithm": {
"name": "differentialEvolution",
"seed": 0,
"populationSize": 8,
"mutationFactor": 0.8,
"crossoverProbability": 0.7
},
"budget": {
"maxSimulationRuns": 25,
"maxWallSeconds": 3600
},
"validation": {
"relativeTolerance": 1e-08,
"absoluteTolerance": 0.0
}
}
```
用户描述了响应约束但未指定容差时,默认 `tolerance = 0`;未指定 `scale` 时,取该约束所有非空边界绝对值的最大值,若结果为零则取 `1`。目标或约束接近零、后端存在可观察的不确定性,或用户给出安全裕量时,应提出有物理意义的容差建议,不能用一个跨量纲的非零绝对容差替代判断。
默认输出目录使用项目文件所在目录下尚不存在的 `optimization-runs/<项目名>-<UTC时间戳>`。源目录不可写时,改用当前可写工作区中的同名新目录,并在计划摘要中说明实际位置。不要让用户命名目录,也不要覆盖既有目录。默认计划可对用户概括为“使用默认搜索设置,最多占用 25 个仿真预算槽位;搜索启动时限为 1 小时,并为找到的最佳可行候选预留一次独立复验”。搜索启动时限到达后不会取消正在运行的健康仿真,预留复验也可能在其后执行,所以不能把它称为总耗时硬上限。除非用户询问或计划产生需要用户决策的覆盖不足警告,不主动讲解算法名称或变体、种群、变异因子、交叉概率、随机种子、搜索/复验预算拆分、边界处理、端点播种、理论完整代数或公式。
生成新计划不以历史运行作为前置检查;除非用户要求复用、比较或解释旧结果,不主动扫描、校验或汇总旧优化目录。若当前对话已经明确存在源文件与实质规格相同的历史运行,为避免混淆最多用一句话注明它只是历史参考、不属于本次计划;用户未追问时不展开旧候选、停止细节、趋势或复验数据,也不用历史样本预测新搜索会收敛或断言连续区间性质。
## 优化规格 JSON
规格是独立 JSON 文件,顶层必须且只能包含 `optimizationSchemaVersion`、`objective`、`designVariables`、`constraints`、`algorithm`、`budget` 和 `validation`。完整示例如下;其中组件 ID、参数名、结果 `key` 和单位必须换成实际 `plan` 所检查的合同:
```json
{
"optimizationSchemaVersion": 1,
"objective": {
"resultKey": "pressure.chamber_1.absolute",
"expectedUnit": "Pa",
"statistic": {
"kind": "maximum",
"window": {"start": 0.0, "end": 1.0}
},
"goal": {"kind": "minimize"}
},
"designVariables": [
{
"id": "orifice_area",
"componentId": "valve_1",
"parameter": "area0",
"unit": "m2",
"lower": 1e-06,
"upper": 0.0001
}
],
"constraints": [
{
"id": "mass_flow_limit",
"resultKey": "massFlow.valve_1.port_2.intoComponent",
"expectedUnit": "kg/s",
"statistic": {"kind": "peakAbsolute", "window": null},
"lower": null,
"upper": 0.25,
"tolerance": 0.0,
"scale": 0.25
}
],
"algorithm": {
"name": "differentialEvolution",
"seed": 0,
"populationSize": 8,
"mutationFactor": 0.8,
"crossoverProbability": 0.7
},
"budget": {
"maxSimulationRuns": 25,
"maxWallSeconds": 3600
},
"validation": {
"relativeTolerance": 1e-08,
"absoluteTolerance": 0.0
}
}
```
规格使用严格字段集。未知字段、缺失字段、重复键、布尔型伪装的数字、`NaN` 和无穷值均不能进入优化;`optimizationSchemaVersion` 固定为 `1`。
`plan` 的 `spec.resolved` 会额外显示只读派生字段,例如统计量 `metricUnit`、算法 `strategy`/`workers`/`updating`、搜索策略、边界处理、一维端点播种策略、停滞代数、预算预留量和固定复验次数;这些字段不是输入规格字段,不能复制回 schema v1 规格文件。输入优化规格仍是 schema v1;最终结果的 `optimizationResultSchemaVersion` 为 `2`,两者不要混淆。
### 目标
`objective.resultKey` 必须是编译模型声明的稳定结果键,`expectedUnit` 必须与结果元数据单位完全一致,无量纲时使用空字符串。`goal.kind` 可为:
- `minimize`:最小化统计值;
- `maximize`:最大化统计值;
- `target`:最小化 `abs(statistic - value)`,此时必须增加有限数字 `goal.value`。
`minimize` 和 `maximize` 不接受 `goal.value`。`target` 只定义目标损失,不是提前停止阈值。
`expectedUnit` 是原始时间序列单位。`final`、`minimum`、`maximum`、`timeMean`、`rms` 和 `peakAbsolute` 的统计量单位与它相同;`integral` 与 `absoluteIntegral` 的统计量单位为原单位乘秒(结构化输出的 `metricUnit` 使用 `(<seriesUnit>)*s`,无量纲序列积分为 `s`)。`target` 的 `value` 使用统计量单位。
### 设计变量
`designVariables` 必须包含 1–16 项,ID 和 `(componentId, parameter)` 均不能重复。每项必须:
- 由用户明确指定,或在用户授权 Skill 提议后纳入计划;执行时它必须是连续、线性 SI 参数,且改变它不会切换模式、改变活动端口或拓扑;
- 不带组件合同中的 `editor` 或离散 `options`;若未来合同显式提供 `optimizationEligible: false`,该否决不可由用户确认覆盖;
- `unit` 与参数合同的 SI 单位完全一致;
- 使用有限数字 `lower < upper`,同时满足目录最小值、最大值和排他下界;
- 使源模型编译后的当前 SI 值位于闭区间 `[lower, upper]` 内。
当前后端目录没有能够单独证明“连续量、整数、模式控制量、活动端口数”的机器字段;字段缺失本身不是连续性证据。不得仅凭“值是数字”自动挑选设计变量。若用户已选定参数,且参数合同、物理量与单位、组件语义和用户给出的连续区间一致表明它是普通物理标量,同时不存在 `editor`、`options` 或显式否决,则可把“按连续线性 SI 参数处理且不改变结构”写成计划假设,不必在生成计划前要求用户复述声明。用户对该计划的一次整体执行确认同时接受这项假设。
若参数像整数、计数、无量纲模式量、条件控制量,元数据彼此矛盾,或无法判断改变它是否影响端口和拓扑,必须先用自然语言询问,例如:“这个参数可以取任意小数,并且调整时不会切换组件模式或端口吗?”不要向用户显示内部声明代码。`editor`、`options` 和未来可能出现的显式否决只用于拒绝明显不适用的参数,不能证明其余参数连续。
参数值、初值和边界一律使用线性 SI 合同。ReactFlow JSON 中普通数值参数已经是 SI 值,`parameterUnits` 只是编辑器显示信息,不能据此把普通数值再次换算;表达式所需的显示单位换算由后端完成。如果用户用显示单位给出边界,才把用户输入换算为 SI。普通 `plan --present` 成功后,`presentation.designVariables[].current` 和 `unit` 是计划摘要中当前值与单位的唯一依据;完整审计计划中的对应字段为 `designVariables[].initial` 和 `unit`。不要从源 JSON 重新计算显示值,若其他信息与它矛盾则先排查而不是向用户展示两套数值。用户给出的边界已经使用该 SI 单位且没有矛盾时,不主动解释 `parameterUnits` 或添加显示单位换算旁注。后端在计划阶段将源 JSON 转换为基准 System XML v3;每个候选都从该 XML 重新生成,只替换选中 `Parameter/@value` 的 SI 数字,不在前一个候选上累积修改,源 JSON 永不被覆盖。
### 统计量与响应约束
`statistic` 必须同时包含 `kind` 和 `window`。支持的 `kind` 为 `final`、`minimum`、`maximum`、`timeMean`、`rms`、`integral`、`absoluteIntegral` 和 `peakAbsolute`。`window` 可为 `null`,表示使用完整返回时间序列;也可为有限数字的 `{start, end}`,且 `start < end`。
窗口边界不在采样点时使用线性插值,不为超出结果范围的窗口外推。时间必须严格递增,时间和数值必须有限且等长。`timeMean`、`rms`、`integral` 和 `absoluteIntegral` 使用梯形时间积分。`minimum`、`maximum` 和 `peakAbsolute` 只是样本及插值边界上的统计,不证明连续时间真实峰值;安全关键峰值可能比 `sampleStep` 更窄时,必须报告采样风险。
`constraints` 只支持响应约束。每项必须包含唯一 `id`、`resultKey`、`expectedUnit`、`statistic`、`lower`、`upper`、`tolerance` 和 `scale`。`lower` 或 `upper` 可为 `null`,但至少一个必须是有限数字;两者都存在时必须 `lower <= upper`。`tolerance >= 0`,`scale > 0`。下界在 `value >= lower - tolerance` 时满足,上界在 `value <= upper + tolerance` 时满足。
`scale` 只用于将违反量归一化为 `rawViolation / scale`,以便对完整但不可行的候选排序;它不改变可行边界,也不是软约束权重。需要等式时用显式容差带,不使用浮点精确相等。
约束的 `lower`、`upper`、`tolerance` 和 `scale` 都使用该约束统计量的单位,而不是一律使用原始序列单位。复验中的单个 `absoluteTolerance` 数值分别按每项统计量自己的单位解释;目标与约束量纲差异很大时优先把它设为 `0` 并使用相对容差,或明确接受这一 schema v1 限制。
## 计划确认
信息足以形成规格后直接执行 `plan`,不需要用户先批准计划生成。它校验源 JSON、规格、结果键/单位、设计变量合同和边界,并请求后端生成基准 XML,但不开始优化候选仿真。完整审计计划文件包含源 JSON 与规格 JSON 的绝对路径/SHA-256、解析后规格、基准 XML SHA-256、目标/约束元数据、解析后设计变量、执行上限、绝对输出目录、`parameterContinuity`、`requiredAssertions`,以及值相同的 `planHash` 与 `confirmationToken`;普通流程的 stdout 只返回严格白名单的展示视图,不包含这些执行凭据和搜索内部字段。
计划中的 `OPTIMIZATION_CONTINUITY_USER_ASSERTION` 是给脚本和审计使用的内部标识,不是要求用户照抄的口令。后端不会验证参数的物理/语义连续性,因此面向用户的计划摘要必须用普通语言列出相关假设。若参数语义清楚,用户在看到摘要后明确同意开始运行,即视为同时接受完整计划和这些假设;未得到这次整体确认时不得传入 `--confirmed`。若参数语义不清,则应在执行确认之前先完成自然语言消歧。
`confirmationToken` 绑定源 JSON SHA-256、规格文件 SHA-256、基准 XML SHA-256、解析后的输出目录、后端 base URL、流读取超时、连续性确认策略和搜索策略。向用户展示计划摘要并获得明确确认后,才能传入 `--confirmed`。`optimize` 还必须提供 `plan` 返回的源 SHA-256、规格 SHA-256 和 token;脚本会重建当前计划并拒绝旧策略或其他过期确认。任一绑定项改变,包括搜索策略升级,都必须重新 `plan` 和确认。输出目录必须是新路径或空目录。
`requiredAssertions[].code`、各类 SHA、`planHash` 和 `confirmationToken` 是代理执行命令时保存和回传的机器字段。普通对话中应由 Skill 内部保管,不向用户倾倒;只有用户主动要求审计细节,或排查计划过期/文件变化时才展示。
普通计划摘要只需要回答:
- 要改善哪个结果,用什么统计口径;
- 调整哪些参数,各自在什么范围;
- 有哪些响应约束;
- 采用默认还是用户指定的搜索配置、最多占用多少仿真预算槽位、停止启动新搜索候选的时限,以及可使总耗时超过该时限的在途仿真和预留复验;
- 哪些参数连续性或工程边界属于假设;
- 输出写到哪里,并明确源模型不变。
计划正文只描述即将执行的运行,不把历史结果回顾、搜索审计明细或对本次结果的预测混入计划。默认设置不存在警告时,“采用默认搜索设置”已足够,不再把内部配置、端点播种或合同判定字段展开成技术清单。若安全展示视图包含 `nonDefaultSettings`,只列出其中实际偏离推荐默认值、并会随本计划一起确认的设置;不要反过来读取完整审计计划扩展技术细节。
用户尚未限制本轮只做计划、且接下来是否执行需要确认时,摘要后只问一次中性的自然问题,例如:“就按这个方案开始吗?”不要主动把换目标、放宽参数边界或其他扩展范围列成备选项。用户明确同意后直接执行,不再追加连续性声明、算法参数或 token 确认。若用户说“只要计划”“暂时不要运行”或同等意思,则交付摘要后直接陈述会停在计划阶段,不在本轮询问是否开始,等用户之后主动要求。用户主动提出调整时再讨论;涉及放宽工程边界时,必须先确认新的范围符合物理、安全和组件合同。
无警告且使用默认设置时,按下列内容边界组织计划回复;可以顺应用户语言调整措辞,但不要增加其他技术段落:
```text
优化目标:让哪个结果按什么统计口径变大、变小或接近目标值。
调整参数:参数名称、脚本 plan 返回的当前 SI 值、用户确认的 SI 范围。
响应约束:列出约束;没有就说无。
运行上限:采用默认搜索设置,最多占用多少仿真预算槽位;搜索到时后不再启动新候选,但会等在途仿真结束,并为找到的最佳可行候选预留一次独立复验。
重要假设:用一句普通语言说明参数按连续物理量处理且不改变模式或结构。
输出:plan 返回的新目录完整绝对路径,源模型不变。
结束语:若本轮可以询问执行,则问“就按这个方案开始吗?”;若用户说暂时不要运行,则说“计划已准备好,我会停在这里,等你之后明确说开始。”
```
正式计划回复从第一个字起使用用户当前语言并直接进入计划内容;不加过程旁白,不显示 `warnings: []` 等内部状态,不复述内部枚举名,也不在计划后追加单位科普、算法原理、历史回顾、结果预测或调整建议。输出目录照抄 `plan` 返回的完整绝对路径,不用省略号或相对路径。只有真实警告、无法消除的单位歧义或其他需要用户决策的问题,才在相应条目中简短说明。
## DE/rand/1/bin 搜索
外层优化由 `optimization_skill.py` 使用 Python 标准库自行实现,不调用 SciPy 优化器,也不安装额外优化依赖。`algorithm` 严格包含:
```text
name = differentialEvolution
seed = 0 .. 2^32-1 的整数
populationSize = 4 .. 50
mutationFactor = (0, 2]
crossoverProbability = [0, 1]
```
`populationSize` 是实际种群个体数,不是乘以设计变量数的倍数。初始候选先在每个线性归一化坐标上做拉丁超立方分层,再用工程当前参数替换第一行,所以替换后的最终种群不承诺保持严格拉丁超立方的每层唯一性。只有一个设计变量时,初始种群还会强制包含归一化坐标 `0` 和 `1`,也就是精确测试用户确认的 SI 下界和上界;若基准点已经等于某个端点,不再为该端点制造重复行。第一次提交仍是基准仿真。基准仿真必须完整成功;基准可以不满足响应约束,此时仍可继续搜索。
每个完整代开始时冻结当前种群和排名;代内所有目标个体都只从这份冻结种群中选三个不同且不是自己的个体,按 `a + mutationFactor * (b - c)` 生成变异向量,接受结果在完整代结束后统一成为下一代。这是 `updating = deferred`,不会让同一代后面的候选使用刚被接受的新个体。越出归一化区间的坐标通过周期为 `2` 的镜像反射折回 `[0, 1]`,不再硬裁剪到端点;随后做 binomial crossover,并强制至少一个坐标来自变异向量。候选排名顺序是:完整可行点按目标损失;其次是完整但不可行点,按约束归一化违反量总和再按目标损失;失败点最后。平局用最早评估 ID 确定性打破。
`seed` 由标准库 `random.Random` 使用。固定种子可使同一 Python 实现和同一后端环境中的候选顺序可重放,但不证明跨 Python 版本、后端代码、操作系统或硬件位级一致。
当前搜索策略标识为 `de-rand-1-bin-deferred-reflection-1d-endpoints-stagnation-v3`。该标识随解析后算法配置进入计划,并绑定到确认 token;不能拿旧搜索策略产生的确认 token 启动新版搜索。
## 预算、停止、缓存与失败
`maxSimulationRuns` 必须至少是 `populationSize + 1`,且不得超过 200。它是仿真预算槽位的严格总上限。每次准备进度记录和打开后端流之前先保守占用一个槽位,因此即使本地进度文件创建或连接失败,该槽位也不会重新使用。搜索最多使用 `maxSimulationRuns - 1` 个槽位,始终为最佳可行候选保留一个绕过缓存的新鲜复验槽位。
计划会给出 `fullGenerationsWithUniqueCandidates = floor((searchRunLimit - populationSize) / populationSize)`。它表示在候选都不重复时,初始种群之外预算还能完整覆盖多少代。值为 `0` 时会返回 `OPTIMIZATION_BUDGET_INITIAL_POPULATION_ONLY` 警告;此时仍可按用户确认运行,但必须明确说明搜索覆盖很弱。若希望至少完整执行两代,预算至少应为 `3 * populationSize + 1`。
`maxWallSeconds` 必须在 10 秒至 7 天之间。它只在启动下一个搜索候选前检查:达到后不再启动新搜索,不取消正在处理慢步的健康仿真。已存在最佳可行候选时,即使搜索墙钟已到,预留的一次新鲜复验仍会运行。
搜索没有目标阈值、目标收敛容差或局部抛光阶段。每个完整 DE 代结束后会统计该代是否产生过新的后端仿真提交;连续 `3` 个完整代没有新提交时提前停止:
- 若当前种群映射为同一个精确 SI 候选,停止原因为 `populationCollapsedAfterDuplicateStagnation`。这只表示重复停滞发生时种群已经塌缩为一个精确候选,不是数值收敛判定,也不证明全局最优;
- 若种群仍映射为多个精确候选,停止原因为 `duplicateProposalStagnation`。这表示候选生成持续重复缓存中的点,不是收敛判定。
仿真预算用完时为 `simulationBudgetExhausted`,搜索墙钟到达时为 `searchWallTimeReached`。为防止任何未预见的重复循环,候选请求另有 `max(100, searchRunLimit * 20)` 的内部防死循环上限;`optimizerCallLimitReached` 仅表示该安全保护触发,绝不能解释成搜索已经收敛。用户中断或结构化错误也会中止运行。
结果中的 `generations` 只统计完整完成的 DE 代;如果预算、墙钟或候选请求保护在一代中途阻止下一个候选,该部分代不会增加计数,也不会产生 `optimization-generation-completed` 事件。完整代进一步分成 `generationsWithNewBackendSubmissions` 和 `generationsWithoutNewBackendSubmissions`;初始种群不算一个 DE 代。“候选都唯一时预算可覆盖的完整代数”只是计划容量,不得当作实际完成或有效搜索代数。
缓存仅在当前 `optimize` 进程内有效。它根据固定参数 ID 顺序和映射后的精确 SI 数值识别重复候选;目标和所有响应约束共享一次仿真。缓存命中不增加后端提交数,最终复验始终绕过缓存。
最终结果使用 schema v2 分开记录搜索、复验、缓存和预算:
- 顶层 `search` 给出停止原因及类别、恒为 `false` 的 `searchConvergenceEstablished`、`populationCollapsedToSingleCandidate`、搜索候选评估数、`submissionSlotsConsumed`、缓存命中率、搜索预算的上限/已用/未用/是否耗尽、完整代及有新提交/无新提交代数、停滞连续代数、最终种群精确候选数和防死循环上限;
- 顶层 `verification` 给出独立复验是否通过、源文件是否未变、比较容差和 `submissionSlotsConsumed`,并明确其含义是候选可复现且可行,而不是搜索收敛;
- `counts.searchProposals`、`searchSubmissionSlotsConsumed`、`verificationSubmissionSlotsConsumed`、`backendSubmissionSlotsConsumed`、分阶段完成/失败记录、`unrecordedSubmissionSlotsConsumed`、`cacheHits` 和 `remainingSearchRunBudget` 提供可直接核对的分项统计。`optimizerCalls` 仅为初版兼容别名,新字段 `candidateRequestsAllStages` 明确包含搜索、缓存命中和复验请求;不带 `SlotsConsumed` 的 `backendSubmissions` 系列也仅是初版兼容别名,不能解释为后端已经接收。`timing` 使用全部已形成的试验记录计算墙钟、仿真耗时总计、最短、最长和平均值。
- `bestSearch` 只保存完整可行的最佳搜索点。没有可行点时它必须为 `null`,约束违反最小的完整不可行点只放入 `bestDiagnosticSearch`,报告必须明确称其为诊断点并展示归一化约束违反总量,不能称为方案或最佳可行点。
面向用户汇报时分别写“搜索停止”和“候选复验状态”,并同时报告搜索候选评估数、按阶段拆分的预算槽位占用与完成/失败记录、没有形成试验记录的槽位、缓存命中、未用搜索额度、完整代中有新仿真的代数与纯重复代数。不得把槽位占用称为后端已接收或已完成的仿真,也不得把 `solutionStatus: verified`、缓存命中次数、纯重复代、种群塌缩或理论预算容量解释为算法收敛证据。
本版没有持久缓存或 resume 命令。`checkpoint.json` 和 `optimization-events.jsonl` 仅用于审计已完成工作,不承诺中断后恢复同一种群。也没有自动重试:后端、网络或产物写入错误会按结构化错误中止,不悄悄再发起一次仿真。
只有 `status == completed`、`success == true`、结果变量键与单位仍匹配计划、所需序列存在、时间窗完整被覆盖且时间/数值结构正确时,才计算目标和约束。`failed`、`stalled`、`stopped`、取消或部分结果作为失败候选且不计分,`objectiveValue` 和 `objectiveLoss` 保持为空。若后端把缺失变量、错误单位或畸形序列标成成功完成,则视为结果合同错误并中止本次优化,避免继续解释不可靠数据。完整但不可行的点仍保留统计值,但不能优先于任何完整可行点。
## 最终复验、产物与措辞
没有完整可行候选时,`solutionStatus` 为 `noFeasibleCandidate`,`bestSearch` 为 `null`;可在 `bestDiagnosticSearch` 中报告归一化约束违反更小的完整不可行候选作诊断,但不生成 `best-*` 产物。存在最佳可行搜索候选时,脚本重新核对源 JSON SHA-256,从基准 XML 生成候选,使用新 simulation ID 并绕过缓存做一次完整新鲜仿真。
复验必须仍完整可行,复验结束时源 JSON SHA-256 仍与计划一致,且目标和每个约束原值均满足 `abs(search - verification) <= absoluteTolerance + relativeTolerance * max(abs(search), abs(verification))`。`validation` 只包含非负的 `relativeTolerance` 和 `absoluteTolerance`;复验次数固定为一,不是规格字段。通过时状态为 `verified`,否则为 `verificationFailed`,不用多次平均掩盖差异。`verified` 只表示最佳搜索候选通过了这次绕过缓存的新鲜复验;它不表示搜索收敛,不证明达到稳态,也不是全局最优证明。复验已经通过时,不再默认建议重跑同一项复验。
若目标统计量是 `final`,schema v2 的 `objectiveEndpointTrend` 会报告一次不影响复验状态的末段趋势诊断。只有新鲜复验本身完整成功,而且目标序列覆盖统计窗口终点或计划仿真终点时才分析;复验未完成、序列无效或未覆盖计划终点时标为 `unavailable` 并给出原因,不能把部分曲线末尾当成计划终点。它优先检查目标统计时段最后 `5%`,为取得至少 `6` 个样本可向前扩展,但最多使用最后 `20%`;仍不足时标为 `insufficientData`。相对量的尺度取“末段最大绝对值、完整时段最大绝对值的 `1e-6` 倍、最小正正规浮点数”三者的最大值。
末段净相对变化至少 `1%` 且非零相邻变化的方向一致率至少 `80%` 时记录 `directionalChange`;零增量不稀释方向一致率。末段相对峰峰范围至少 `2%` 时记录 `tailVariability`。任一条件成立就标为 `materialChangeDetected`。只有方向变化条件成立时才称为上升或下降;仅由范围条件触发时方向为 `fluctuating`,报告明显波动,并分别展示首尾净变化与峰峰范围,不能把振荡描述成单向趋势。结构化结果同时记录末段起止时刻、样本数、起止值、变化量、平均变化率、相对变化、相对范围、方向和检测原因。
这项检查只用于提醒“终点快照可能仍处于动态过程”。`steadyStateProven` 始终为 `false`;`noMaterialChangeDetected` 只能表述为“该启发式检查未发现明显末端变化”,不能写成“系统已达到稳态”。检查发现明显变化时,必须指出 `final` 结果只支持所选终点时刻的比较,不能外推成稳态性能更优。
优化输出包括 `optimization-plan.json`、`optimization-events.jsonl`、`simulation-progress/evaluation-NNNN.jsonl`、`checkpoint.json`、`evaluations.csv`、`optimization-result.json` 和 `report.md`。`evaluations.csv` 对每次形成试验记录的预算请求写一行;若本地准备或连接在形成试验记录前抛错,预算槽位占用可能比 CSV 行数多。`checkpoint.json` 是审计快照而不是 resume 状态。
只有 `solutionStatus == verified` 时才生成 `best-parameters.json`、`best-system.xml`、`best-project.json`、`result.json`、完整 `results.csv` 和目标/响应约束的独立 SVG 曲线。`best-project.json` 将被优化参数的原表达式替换为普通 SI 数值,源 JSON 不变。`optimize` 仅在状态为 `verified` 时返回退出码 `0`,其他结果返回 `4`。
只有 `solutionStatus == verified` 时,最终汇报才使用这一口径:
> 这是实际完成仿真的搜索点中表现最好的可行候选,并已通过一次独立复验;复验不证明搜索收敛、系统达到稳态或全局最优。
不使用“已找到全局最优”、“必然最优”或其他超出有限搜索证据的措辞。最佳候选位于用户确认的参数边界时,只能报告它是当前边界内实际搜索得到的边界点;在用户确认更宽范围符合物理、安全和组件合同前,不建议直接放宽边界或启动扩边界搜索,也不把有限采样点概括成整个连续区间上的严格单调规律。
## 内部命令与 OpenClaw 路径
以下命令供 Skill 实现和故障排查使用;正常交互不得要求用户手工运行命令、创建规格文件或复制确认参数。
从仓库根目录先预览计划:
```powershell
py -3.12 skills/system-simulation/scripts/optimization_skill.py plan PROJECT.json `
--spec optimization-spec.json `
--output-dir OUTPUT_DIR `
--plan-file PLAN_FILE `
--present
```
普通 Skill 流程必须同时使用 `--plan-file` 和 `--present`:完整审计计划以 `0600` 权限独占写入 `PLAN_FILE`,stdout 只返回用户计划所需的白名单字段。`PLAN_FILE` 必须是位于 `OUTPUT_DIR` 外的新文件,不能覆盖既有文件,也不能与输出目录互为祖先或后代;默认用本次输出目录名加 UTC 时间戳或随机后缀生成同级文件,不复用固定的临时文件名。省略这两个选项的旧式完整 stdout 只用于兼容测试或显式审计排障,不用于普通对话。
向用户展示计划并获得明确确认后,在内部从 `PLAN_FILE` 读取并原样使用源 SHA、规格 SHA 和 `confirmationToken`,不要向用户展示:
```powershell
py -3.12 skills/system-simulation/scripts/optimization_skill.py optimize PROJECT.json `
--spec optimization-spec.json `
--output-dir OUTPUT_DIR `
--expected-source-sha256 SOURCE_SHA256 `
--expected-spec-sha256 SPEC_SHA256 `
--confirmation-token CONFIRMATION_TOKEN `
--confirmed
```
Linux 使用已确认的 Python 3.12 解释器和相同参数:
```bash
python3.12 skills/system-simulation/scripts/optimization_skill.py plan PROJECT.json \
--spec optimization-spec.json \
--output-dir OUTPUT_DIR \
--plan-file PLAN_FILE \
--present
python3.12 skills/system-simulation/scripts/optimization_skill.py optimize PROJECT.json \
--spec optimization-spec.json \
--output-dir OUTPUT_DIR \
--expected-source-sha256 SOURCE_SHA256 \
--expected-spec-sha256 SPEC_SHA256 \
--confirmation-token CONFIRMATION_TOKEN \
--confirmed
```
需要非默认后端或读取超时时,全局选项必须放在 `plan` / `optimize` 子命令之前,例如:
```bash
python3.12 skills/system-simulation/scripts/optimization_skill.py \
--base-url http://127.0.0.1:18082 --timeout 60 \
plan PROJECT.json --spec optimization-spec.json --output-dir OUTPUT_DIR \
--plan-file PLAN_FILE --present
```
OpenClaw 中不假设当前目录是仓库根目录,使用 Skill 根目录占位符:
```bash
python3.12 "{baseDir}/scripts/optimization_skill.py" plan PROJECT.json \
--spec optimization-spec.json \
--output-dir OUTPUT_DIR \
--plan-file PLAN_FILE \
--present
```
也可以先进入本 Skill 目录,再使用 `scripts/optimization_skill.py plan ...` 和 `scripts/optimization_skill.py optimize ...`。不根据用户主目录、OpenClaw 数据目录或仓库名称猜测脚本路径。持续消费 JSONL 进展,定期报告已占用/最大仿真预算槽位、已完成和失败记录、当前代数、最佳可行目标、缓存命中和内层仿真阶段;不把槽位占用说成后端已接收或已完成,也不因仿真时间短暂停滞而声称卡死。
通常省略 `--optimization-id` 让脚本生成唯一 ID。若显式指定,同一后端任务保留窗口内必须使用新的 ID;快速复用旧 ID 会被后端按冲突拒绝。
@@ -1,48 +0,0 @@
# 安全文本规范化策略
## 目的
`repair-format` 只解决可解析 JSON v1 或 XML v3 的文本层问题,使文件采用稳定的 UTF-8 和跨平台文本格式。它不是模型迁移器,也不是语义修复器。
## 允许的修改
仅允许脚本已经证明不会改变解析后数据合同的规范化,例如:
- 将可安全解码的输入统一写为 UTF-8;
- 统一 BOM 和换行表现;
- 规范化文件末尾换行;
- 对可解析内容采用脚本规定的稳定文本序列化形式。
以脚本返回的预览、变更摘要和哈希为准;不要在脚本外另写正则替换或自制格式化器。若文件连语法都无法可靠解析,停止并报告诊断,不能尝试猜测闭合括号、XML 标签或截断内容。
## 禁止的修改
本版不得自动执行下列动作:
- 新增、删除、更换或重命名组件和连接;
- 修改组件 ID、类型、端口、`modelVersion` 或 Schema 版本;
- 填猜缺失参数、改变数值、单位、离散选项或介质引用;
- 修改仿真起止时间、采样间隔、最大步长或求解算法;
- 把 XML v1/v2 或旧工程升级到当前版本;
- 根据报错放宽容差、删除失败组件或改变物理拓扑;
- 覆盖源文件,即使用户给出的输出路径通过大小写、相对路径或符号链接指向源文件也不行。
发现上述问题时,可以解释和给出人工处理建议,但不能借“修复格式”的名义实施。
## 强制确认流程
1. 对源文件运行 `inspect`,记录格式、诊断和 SHA-256。
2. 生成或读取 `repair-format` 的规范化预览,向用户说明只会改变哪些文本表现,并展示目标输出路径。
3. 等待用户针对该预览明确确认。笼统的“帮我看看”或先前对其他版本的确认不能复用。
4. 使用同一个源文件 SHA-256、预览返回的 `confirmationToken`、`--confirmed` 和预览中相同的 `--output` 路径执行写入。token 绑定源哈希、规范化输出哈希和目标绝对路径。
5. 如果哈希、规范化结果或目标路径已变化,停止并重新预览;不能绕过 `--expected-sha256` 或确认 token。
6. 对输出文件重新运行 `inspect`。只有重新校验通过且解析后的模型语义未改变时,才能报告完成。
示例命令形状见 [workflows.md](workflows.md)。
## 输出与交付
- 输出名称建议为原名加 `.normalized`,例如 `plant.normalized.json` 或 `plant.normalized.xml`。
- 保留源文件;清楚列出新文件、源 SHA-256、输出 SHA-256 和重新校验结果。
- 如果没有文本差异,说明文件无需规范化,不制造副本冒充修复结果。
- 如果写入失败或输出校验失败,不能把不完整文件当作成功结果交付。
@@ -1,143 +0,0 @@
# 命令与对话工作流
## CLI 合同
从仓库根目录调用:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py [--base-url URL] [--timeout SECONDS] COMMAND
```
若仓库已有 `.venv-win\Scripts\python.exe`,Windows 应优先用它替换 `py -3.12`;Linux 使用 `.venv/bin/python` 或 `python3.12`。不要使用本机可能指向旧版本的裸 `python`。默认服务地址为 `http://127.0.0.1:8000`,超时参数不得低于 10 秒,以保持在后端 5 秒心跳间隔之上。`inspect`、`repair-format`、`status` 和 `cancel` 在标准输出返回一个结构化 JSON;`simulate` 在标准输出给出节流后的 JSONL 进展和精简完成摘要。输出目录的 `progress.jsonl` 保留完整进展/错误事件,但只保存精简结果摘要;完整数值结果另存为 `result.json`,避免时间序列重复占用空间和智能体上下文。
退出码:
| 退出码 | 含义 |
| --- | --- |
| `0` | 命令按合同成功完成 |
| `2` | 输入、参数或安全前置条件错误 |
| `3` | HTTP、连接或后端结构化错误 |
| `4` | 仿真事件流报告失败 |
| `5` | 本地结果文件写入失败 |
不要只看退出码 `0` 就声称仿真数值成功;还要检查最终事件和 `result.json` 中的状态。不要通过匹配本地化消息文本判断状态。
## 检查与解释
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py inspect INPUT --format auto
```
`--format` 可为 `auto`、`json` 或 `xml`。完成后按 [file-contracts.md](file-contracts.md) 解释模型。错误和警告应保留层级、稳定错误码、路径或行号;不要只复述最后一句消息。
对工程 JSON,`inspect` 的编译检查会安全计算受支持的连续参数表达式;原始组件数据仍显示用户输入的表达式。仿真时生成的临时 XML 只包含换算后的 SI 数值,不会回写 JSON。
默认只返回首批 50 个紧凑组件、25 条连接和 20 个结果变量,避免大型工程输出撑满上下文。翻阅模型摘要、按组件查看完整合同或搜索结果变量时使用:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py inspect INPUT `
--component COMPONENT_ID `
--component-offset 0 `
--component-limit 50 `
--connection-offset 0 `
--connection-limit 25 `
--variable-query QUERY `
--variable-offset 0 `
--variable-limit 20
```
分别依据 `componentPage`、`connectionPage` 和 `resultVariablePage` 的 `hasMore`、`nextOffset` 继续分页,不要为寻找一个组件或变量请求全部详细合同。`componentTypes` 始终汇总完整模型,可先用它判断系统构成。`--component` 返回该 ID 的源文件数据和(若参与求解)编译合同,因此物性介质等配置节点也能查看参数。
## 文本规范化修复
先检查并取得源文件 SHA-256。第一次不带 `--confirmed` 调用只返回差异预览、不会写文件:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py repair-format INPUT `
--format auto `
--output OUTPUT `
--expected-sha256 SHA256
```
向用户展示预览中的目标路径、源/输出哈希和 `confirmationToken`,取得明确确认后,再用原样 token 执行写入:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py repair-format INPUT `
--format auto `
--output OUTPUT `
--expected-sha256 SHA256 `
--confirmation-token PREVIEW_TOKEN `
--confirmed
```
token 同时绑定源文件哈希、规范化输出哈希和目标绝对路径;任何一项变化都必须重新预览和确认。脚本拒绝覆盖源文件、哈希/token 不匹配和未确认写入。完整边界见 [repair-policy.md](repair-policy.md)。写入后再次运行 `inspect OUTPUT`。
## 仿真前对话
运行前必须完成以下判断:
1. `inspect` 通过,并取得可用结果变量清单。
2. 用户选择 `separate`、`overlay` 或 `stacked`。
3. 把自然语言对象解析为稳定结果 `key`;重名、缺单位或把输入参数误称为曲线时先澄清。
4. 向用户复述将运行的文件、仿真时段、算法、所选结果变量和曲线方式。
本版没有网页自动预装能力。用户要求“网页查看”时,说明当前只能直接交付 SVG 曲线与 CSV;不要启动浏览器、生成临时 URL,或声称现有页面会自动载入文件。
## 启动并监视仿真
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py simulate INPUT `
--format auto `
--output-dir OUTPUT_DIR `
--variables RESULT_KEY_1 RESULT_KEY_2 `
--chart-mode overlay `
--simulation-id SIMULATION_ID
```
`--variables` 接受结果变量稳定 `key`;不能传组件参数名。`--simulation-id` 可省略并由脚本生成,但应保存最终 ID,供恢复查询或取消使用。
监视规则:
- 消费 JSONL,记录最新 `progress`、`phase`、`simulatedTime/totalTime`、心跳和内部活动快照;
- 长任务期间定期向用户给出简短进展,避免逐条转发事件;
- 仅有仿真时间平台期不能证明卡死。活动序号、RHS、solver step、Jacobian 或闭合计数仍增长时,应报告“正在处理慢步”;
- 网络读取中断后,用已知 simulation ID 查询一次任务快照,再决定是否继续说明、恢复结果或报告连接问题;
- 不因运行缓慢自动取消。只有用户明确要求取消,或既有系统已经把任务判定为 stalled 时,才使用对应取消原因;
- `completed` 才表示完整完成;`stopped`、`stalled`、`failed` 都必须标明是非完整结果。
当前任务状态保存在后端进程内,终态记录只短期保留,服务重启后也不能恢复。本 Skill 不承诺跨进程或长期断线续传;需要查询时应及时保存 simulation ID、事件日志和已经写出的结果文件。
`status` 对已完成任务只输出结果摘要,不在终端重复打印整套时间序列;正常 `simulate` 流程会把完整数据保存为 `result.json` 和 `results.csv`。
恢复查询:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py status SIMULATION_ID
```
用户要求取消时:
```powershell
py -3.12 skills/system-simulation/scripts/simulation_skill.py cancel SIMULATION_ID --reason user
```
`--reason stalled` 只用于已有充分停滞证据的内部流程,不用来表达普通的用户取消。
## 结果文件与交付
正常运行目录应包含:
- `progress.jsonl`:原始进度、心跳和结束事件;
- `result.json`:最终结构化结果;
- `results.csv`:全部可用结果变量的 UTF-8 CSV;
- 根据 `separate`、`overlay` 或 `stacked` 生成的 SVG 曲线。
交付时:
1. 说明最终状态和实际计算到的仿真时间;
2. 返回用户选择的 SVG 曲线;
3. 无论用户只选了几条曲线,都同时返回完整 `results.csv`;
4. 若失败或取消但存在部分序列,明确标注曲线和 CSV 是部分结果;
5. 若没有产生可用时间序列,明确说明没有 CSV,不能创建空文件冒充结果;
6. 保留 `result.json` 和 `progress.jsonl` 作为诊断依据,但通常无需把完整事件日志逐行展示给用户。
本版不会根据结果自动改变模型并重试。诊断后若要改参数、拓扑或算法,先把建议交给用户,等待后续迭代能力或单独授权的人工修改流程。
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-172
View File
@@ -1,172 +0,0 @@
from __future__ import annotations
import unittest
from xml.etree import ElementTree as ET
from app.main import (
ReactFlowNodePayload,
ReactFlowProjectPayload,
build_reactflow_system_xml,
reactflow_project_storage_data,
validate_reactflow_component_contract,
)
from app.parameter_expression import (
ParameterExpressionError,
evaluate_parameter_expression,
)
from app.simulation.registry import get_component_model_spec
from tests.test_amesim_pnvo001_signal_xml import amesim_pnvo001_signal_project
AREA_EXPRESSION = "3.14*10**2/4"
AREA_IN_SQUARE_METRES = 7.85e-5
def pnvo001_project_with_area(
area0: object,
*,
selected_unit: str = "mm2",
) -> ReactFlowProjectPayload:
project = amesim_pnvo001_signal_project().model_copy(deep=True)
valve = next(node for node in project.nodes if node.id == "valve_1")
valve.data.parameters["area0"] = area0
valve.data.parameterUnits["area0"] = selected_unit
return project
def pnvo001_node(project: ReactFlowProjectPayload) -> ReactFlowNodePayload:
return next(node for node in project.nodes if node.id == "valve_1")
class ParameterExpressionParserTests(unittest.TestCase):
def test_supported_arithmetic_constants_and_functions(self) -> None:
cases = {
"=3.14*10^2/4": 78.5,
"3.14*10**2/4": 78.5,
"(2 + 3) * 4": 20.0,
"2^3^2": 512.0,
"-2^2": -4.0,
"2.5E-3": 0.0025,
"sqrt(16) + abs(-2)": 6.0,
"sin(pi/2) + ln(e)": 2.0,
"max(1, 5, 3) + pow(2, 3)": 13.0,
}
for expression, expected in cases.items():
with self.subTest(expression=expression):
self.assertAlmostEqual(
evaluate_parameter_expression(expression),
expected,
places=12,
)
def test_invalid_or_unsafe_expressions_are_rejected(self) -> None:
cases = (
"",
"=",
"1 / 0",
"sqrt(-1)",
"pow(-1, 0.5)",
"unknown + 1",
"window.alert(1)",
"__import__('os')",
"1 + * 2",
"1e309",
"min()",
"max(" + ",".join("1" for _ in range(17)) + ")",
"(" * 34 + "1" + ")" * 34,
"1" * 513,
)
for expression in cases:
with self.subTest(expression=expression[:40]):
with self.assertRaises(ParameterExpressionError):
evaluate_parameter_expression(expression)
class ParameterExpressionExecutionTests(unittest.TestCase):
def test_pnvo001_area_expression_uses_selected_mm2_unit(self) -> None:
project = pnvo001_project_with_area(AREA_EXPRESSION)
valve = pnvo001_node(project)
spec = get_component_model_spec(valve.data.modelType)
parameters = validate_reactflow_component_contract(valve, spec)
self.assertAlmostEqual(
parameters["area0"],
AREA_IN_SQUARE_METRES,
places=15,
)
def test_plain_numeric_si_value_is_not_converted_again(self) -> None:
for stored_value in (AREA_IN_SQUARE_METRES, "7.85e-5"):
with self.subTest(stored_value=stored_value):
project = pnvo001_project_with_area(stored_value)
valve = pnvo001_node(project)
spec = get_component_model_spec(valve.data.modelType)
parameters = validate_reactflow_component_contract(valve, spec)
self.assertEqual(parameters["area0"], AREA_IN_SQUARE_METRES)
def test_storage_preserves_the_original_expression(self) -> None:
project = pnvo001_project_with_area(AREA_EXPRESSION)
stored = reactflow_project_storage_data(project)
stored_valve = next(
node for node in stored["nodes"] if node["id"] == "valve_1"
)
self.assertEqual(
stored_valve["data"]["parameters"]["area0"],
AREA_EXPRESSION,
)
self.assertEqual(
pnvo001_node(project).data.parameters["area0"],
AREA_EXPRESSION,
)
def test_xml_contains_resolved_si_value_without_mutating_project(self) -> None:
project = pnvo001_project_with_area(AREA_EXPRESSION)
xml_bytes = build_reactflow_system_xml(project)
root = ET.fromstring(xml_bytes)
area_parameter = root.find(
"./Components/Component[@id='valve_1']/Parameter[@name='area0']"
)
self.assertIsNotNone(area_parameter)
assert area_parameter is not None
self.assertAlmostEqual(
float(area_parameter.attrib["value"]),
AREA_IN_SQUARE_METRES,
places=15,
)
self.assertNotIn(AREA_EXPRESSION, xml_bytes.decode("utf-8"))
self.assertEqual(
pnvo001_node(project).data.parameters["area0"],
AREA_EXPRESSION,
)
def test_invalid_expression_has_stable_execution_error_code(self) -> None:
project = pnvo001_project_with_area("sqrt(-1)")
with self.assertRaisesRegex(
ValueError,
"PARAMETER_EXPRESSION_INVALID.*area0.*valve_1",
):
build_reactflow_system_xml(project)
def test_discrete_parameter_expression_is_rejected(self) -> None:
project = pnvo001_project_with_area(AREA_IN_SQUARE_METRES)
pnvo001_node(project).data.parameters["flowset"] = "1 + 0"
with self.assertRaisesRegex(
ValueError,
"PARAMETER_EXPRESSION_FORBIDDEN.*flowset.*valve_1",
):
build_reactflow_system_xml(project)
if __name__ == "__main__":
unittest.main()
+767
View File
@@ -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()
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff