完成求解器雅可比矩阵首轮优化,增加更新目录,整理了文档文件夹,增加了服务启动脚本
This commit is contained in:
1 parent
6bb0591d32
commit
16a7eb2d6c
48 files changed
+8172
-217
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
*.sh text eol=lf
|
||||||
@@ -12,6 +12,10 @@ htmlcov/
|
|||||||
# Local virtual environments
|
# Local virtual environments
|
||||||
.venv/
|
.venv/
|
||||||
.venv-win/
|
.venv-win/
|
||||||
|
|
||||||
|
# Local Linux toolchain (downloaded for the startup scripts)
|
||||||
|
.tools/node-*-linux-x64/
|
||||||
|
|
||||||
app/data/
|
app/data/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|||||||
@@ -2,6 +2,67 @@
|
|||||||
|
|
||||||
ReactFlow 系统建模与 `app.simulation` 仿真后端。
|
ReactFlow 系统建模与 `app.simulation` 仿真后端。
|
||||||
|
|
||||||
|
## 开发环境准备
|
||||||
|
|
||||||
|
后端依赖分别安装在平台对应的虚拟环境中。
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3 -m venv .venv-win
|
||||||
|
.\.venv-win\Scripts\python.exe -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
./.venv/bin/python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
前端使用 Vite 8,需要 Node.js `20.19+` 或 `22.12+`。首次启动前安装前端依赖。
|
||||||
|
|
||||||
|
Windows(PowerShell,使用仓库内的便携 Node.js):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$nodeDir = Get-ChildItem .tools -Directory -Filter "node-*-win-x64" |
|
||||||
|
Where-Object { (Test-Path "$($_.FullName)\node.exe") -and (Test-Path "$($_.FullName)\npm.cmd") } |
|
||||||
|
Select-Object -First 1
|
||||||
|
& "$($nodeDir.FullName)\npm.cmd" --prefix frontend ci
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows 启动脚本会自动使用 `.tools/node-*-win-x64` 下兼容的便携 Node.js;Linux 启动脚本优先使用 `.tools/node-*-linux-x64` 下兼容的运行时(如果存在),否则使用 `PATH` 中的 `node` 和 `npm`。`start-all.sh` 需要 Bash 4.3 或更高版本。
|
||||||
|
|
||||||
|
## 启动项目
|
||||||
|
|
||||||
|
脚本统一存放在 `bat/` 目录。三个入口分别用于同时启动、只启动后端、只启动前端。
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
bat\start-all.bat
|
||||||
|
bat\start-backend.bat
|
||||||
|
bat\start-reactflow.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bat/start-all.sh
|
||||||
|
./bat/start-backend.sh
|
||||||
|
./bat/start-reactflow.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
后端地址为 `http://127.0.0.1:8000`,前端地址为 `http://127.0.0.1:5173`。Windows 的 `start-all.bat` 会分别打开两个命令行窗口;Linux 的 `start-all.sh` 会在同一终端管理两个进程,按 `Ctrl+C` 会同时停止它们。
|
||||||
|
|
||||||
## 后端接口
|
## 后端接口
|
||||||
|
|
||||||
- `GET /api/components/catalog`:返回组件库与模型版本、分类、图标键、端口布局和参数契约,供 ReactFlow 启动时自动加载。
|
- `GET /api/components/catalog`:返回组件库与模型版本、分类、图标键、端口布局和参数契约,供 ReactFlow 启动时自动加载。
|
||||||
@@ -19,19 +80,14 @@ ReactFlow 系统建模与 `app.simulation` 仿真后端。
|
|||||||
|
|
||||||
当前网络层可按端口域处理气动压力-流量残差与 stream 焓、标量信号传播,以及一维机械 `x/v` 等值和 `f` 平衡,并使用 SciPy 完成非线性代数闭合和时间积分。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点,同时支持已登记的信号和机械基础件。它不是完整 DAE 或事件求解器,也不等价于严格 Modelica.Fluid 实现。
|
当前网络层可按端口域处理气动压力-流量残差与 stream 焓、标量信号传播,以及一维机械 `x/v` 等值和 `f` 平衡,并使用 SciPy 完成非线性代数闭合和时间积分。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点,同时支持已登记的信号和机械基础件。它不是完整 DAE 或事件求解器,也不等价于严格 Modelica.Fluid 实现。
|
||||||
|
|
||||||
XML 解析依赖 `lxml` 执行本地 XSD 校验。安装或更新 Python 环境时使用:
|
XML 解析依赖 `lxml` 执行本地 XSD 校验,该依赖已包含在 `requirements.txt` 中。
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\.venv-win\Scripts\python.exe -m pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [开发文档索引](docs/README.md)
|
- [开发文档索引](docs/README.md)
|
||||||
- [后端接口版本与定义规范 v1](docs/backend-interface-version-spec-v1.md)
|
- [后端接口版本与定义规范 v1](docs/standard/backend-interface-version-spec-v1.md)
|
||||||
- [组件模型建模规范 v1](docs/component-model-authoring-spec-v1.md)
|
- [组件模型建模规范 v1](docs/standard/component-model-authoring-spec-v1.md)
|
||||||
- [组件库分类、发现与读取规范 v1](docs/component-library-spec-v1.md)
|
- [组件库分类、发现与读取规范 v1](docs/standard/component-library-spec-v1.md)
|
||||||
- [组件目录 JSON Schema v1](schemas/component-catalog-v1.schema.json)
|
- [组件目录 JSON Schema v1](schemas/component-catalog-v1.schema.json)
|
||||||
- [System XML v3 协议(当前规范)](docs/system-xml-v3.md)
|
- [System XML v3 协议(当前规范)](docs/standard/system-xml-v3.md)
|
||||||
- [System XML v3 XSD(当前 Schema)](schemas/system-simulation-v3.xsd)
|
- [System XML v3 XSD(当前 Schema)](schemas/system-simulation-v3.xsd)
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ FastAPI 的 `GET /api/components/catalog` 会把注册表转换成前端组件
|
|||||||
公开临时库入口是 `components/amesim/library.py`。公开模型必须在
|
公开临时库入口是 `components/amesim/library.py`。公开模型必须在
|
||||||
模型类中声明 `MODEL_TYPE / MODEL_VERSION / PORTS / PARAMETERS /
|
模型类中声明 `MODEL_TYPE / MODEL_VERSION / PORTS / PARAMETERS /
|
||||||
RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见
|
RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见
|
||||||
[`组件模型建模规范 v1`](../../docs/component-model-authoring-spec-v1.md)和
|
[`组件模型建模规范 v1`](../../docs/standard/component-model-authoring-spec-v1.md)和
|
||||||
[`组件库分类、发现与读取规范 v1`](../../docs/component-library-spec-v1.md)。
|
[`组件库分类、发现与读取规范 v1`](../../docs/standard/component-library-spec-v1.md)。
|
||||||
|
|
||||||
当前关键文件:
|
当前关键文件:
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ Jacobian 和 System XML XSD,完成后才开始接收请求。它不会运行
|
|||||||
`diagnostics.performance.propertyCache` 中返回。
|
`diagnostics.performance.propertyCache` 中返回。
|
||||||
|
|
||||||
基准原始 JSON 默认放到已忽略的 `app/data/` 下。指标字段、实测结果和使用边界见
|
基准原始 JSON 默认放到已忽略的 `app/data/` 下。指标字段、实测结果和使用边界见
|
||||||
[`仿真性能评估 2026-08-15`](../../docs/仿真性能评估-2026-08-15.md)。
|
[`仿真性能评估 2026-08-15`](../../docs/other/仿真性能评估-2026-08-15.md)。
|
||||||
|
|
||||||
## 当前阶段进度
|
## 当前阶段进度
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ print(result.used_modelica_reference)
|
|||||||
## 基线结果
|
## 基线结果
|
||||||
|
|
||||||
当前基线对比摘要来自:
|
当前基线对比摘要来自:
|
||||||
[`testmodel_modelica_comparison_summary.txt`](../../tests/baselines/simulation/testmodel/testmodel_modelica_comparison_summary.txt)
|
[`testmodel_modelica_comparison_summary.txt`](../../tests/data/testmodel/testmodel_modelica_comparison_summary.txt)
|
||||||
|
|
||||||
当前四个主变量的最大误差为:
|
当前四个主变量的最大误差为:
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from math import isclose, log, log10, pi, sqrt, tanh
|
from math import isclose, isfinite, log, log10, pi, sqrt, tanh
|
||||||
|
|
||||||
from app.simulation.components.amesim.gases import (
|
from app.simulation.components.amesim.gases import (
|
||||||
AMESIM_GAS_INDEX_PARAMETER,
|
AMESIM_GAS_INDEX_PARAMETER,
|
||||||
@@ -22,11 +23,35 @@ from app.simulation.core.metadata import (
|
|||||||
ResultVariableDefinition,
|
ResultVariableDefinition,
|
||||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||||
)
|
)
|
||||||
from app.simulation.core.medium import GasMedium, ThermodynamicProperties
|
from app.simulation.core.medium import (
|
||||||
|
GasMedium,
|
||||||
|
ThermodynamicProperties,
|
||||||
|
ThermodynamicPropertiesLinearization,
|
||||||
|
)
|
||||||
from app.simulation.core.ports import PortDefinition
|
from app.simulation.core.ports import PortDefinition
|
||||||
from app.simulation.core.state import VolumeState
|
from app.simulation.core.state import VolumeState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pnl0001MassFlowLinearization:
|
||||||
|
value: float
|
||||||
|
partial_p_1: float
|
||||||
|
partial_p_2: float
|
||||||
|
partial_temperature: float
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
direction: str = "forward"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pnl0001DerivativeLinearization:
|
||||||
|
derivative: tuple[float, float]
|
||||||
|
tangents: tuple[tuple[float, ...], tuple[float, ...]]
|
||||||
|
properties: ThermodynamicPropertiesLinearization
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
_MAX_REPORTED_FRICTION_FACTOR = 64_000_000.0
|
_MAX_REPORTED_FRICTION_FACTOR = 64_000_000.0
|
||||||
|
|
||||||
|
|
||||||
@@ -829,6 +854,105 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
|
|||||||
)
|
)
|
||||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||||
|
|
||||||
|
def linearize_mass_flow(
|
||||||
|
self,
|
||||||
|
p_1: float,
|
||||||
|
p_2: float,
|
||||||
|
temperature: float,
|
||||||
|
*,
|
||||||
|
relative_step: float = 2.0 ** -26,
|
||||||
|
slope_relative_tolerance: float = 5.0e-3,
|
||||||
|
) -> Pnl0001MassFlowLinearization:
|
||||||
|
"""Audit local flow-law slopes without perturbing the full system RHS."""
|
||||||
|
|
||||||
|
p_1 = float(p_1)
|
||||||
|
p_2 = float(p_2)
|
||||||
|
temperature = float(temperature)
|
||||||
|
direction = "forward" if p_1 > p_2 else "reverse"
|
||||||
|
value = self.mass_flow(p_1, p_2, temperature)
|
||||||
|
|
||||||
|
def invalid(reason: str) -> Pnl0001MassFlowLinearization:
|
||||||
|
return Pnl0001MassFlowLinearization(
|
||||||
|
value=value,
|
||||||
|
partial_p_1=0.0,
|
||||||
|
partial_p_2=0.0,
|
||||||
|
partial_temperature=0.0,
|
||||||
|
valid=False,
|
||||||
|
reason=reason,
|
||||||
|
direction=direction,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not all(isfinite(item) for item in (p_1, p_2, temperature, value)):
|
||||||
|
return invalid("non_finite_primal")
|
||||||
|
pressure_gap = abs(p_1 - p_2)
|
||||||
|
if pressure_gap <= 1.0e-8:
|
||||||
|
return invalid("flow_direction_boundary")
|
||||||
|
if temperature <= 1.0 * (1.0 + 1.0e-10):
|
||||||
|
return invalid("temperature_floor_boundary")
|
||||||
|
if relative_step <= 0.0 or slope_relative_tolerance <= 0.0:
|
||||||
|
raise ValueError("PNL0001 slope audit tolerances must be positive.")
|
||||||
|
|
||||||
|
pressure_step = min(
|
||||||
|
relative_step * max(abs(p_1), abs(p_2), 1.0),
|
||||||
|
0.25 * pressure_gap,
|
||||||
|
)
|
||||||
|
temperature_step = min(
|
||||||
|
relative_step * max(abs(temperature), 1.0),
|
||||||
|
0.25 * (temperature - 1.0),
|
||||||
|
)
|
||||||
|
if pressure_step <= 0.0 or temperature_step <= 0.0:
|
||||||
|
return invalid("unresolved_local_step")
|
||||||
|
|
||||||
|
arguments = (p_1, p_2, temperature)
|
||||||
|
argument_names = ("p_1", "p_2", "temperature")
|
||||||
|
steps = (pressure_step, pressure_step, temperature_step)
|
||||||
|
partials: list[float] = []
|
||||||
|
for argument_index, (argument, step) in enumerate(
|
||||||
|
zip(arguments, steps, strict=True)
|
||||||
|
):
|
||||||
|
lower = list(arguments)
|
||||||
|
upper = list(arguments)
|
||||||
|
lower[argument_index] = argument - step
|
||||||
|
upper[argument_index] = argument + step
|
||||||
|
lower_value = self.mass_flow(*lower)
|
||||||
|
upper_value = self.mass_flow(*upper)
|
||||||
|
left_slope = (value - lower_value) / step
|
||||||
|
right_slope = (upper_value - value) / step
|
||||||
|
slope_scale = max(
|
||||||
|
abs(left_slope),
|
||||||
|
abs(right_slope),
|
||||||
|
abs(value) / max(abs(argument), 1.0),
|
||||||
|
1.0e-12,
|
||||||
|
)
|
||||||
|
if not all(
|
||||||
|
isfinite(item)
|
||||||
|
for item in (
|
||||||
|
lower_value,
|
||||||
|
upper_value,
|
||||||
|
left_slope,
|
||||||
|
right_slope,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return invalid(
|
||||||
|
f"non_finite_local_slope:{argument_names[argument_index]}"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
abs(left_slope - right_slope)
|
||||||
|
> slope_relative_tolerance * slope_scale
|
||||||
|
):
|
||||||
|
return invalid(
|
||||||
|
f"local_slope_disagreement:{argument_names[argument_index]}"
|
||||||
|
)
|
||||||
|
partials.append(0.5 * (left_slope + right_slope))
|
||||||
|
|
||||||
|
return Pnl0001MassFlowLinearization(
|
||||||
|
value=value,
|
||||||
|
partial_p_1=partials[0],
|
||||||
|
partial_p_2=partials[1],
|
||||||
|
partial_temperature=partials[2],
|
||||||
|
direction=direction,
|
||||||
|
)
|
||||||
|
|
||||||
def component_result_values(self) -> Mapping[str, float]:
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
props = self.properties()
|
props = self.properties()
|
||||||
flow = self.mass_flow(self.port_1.p, props.p, props.T)
|
flow = self.mass_flow(self.port_1.p, props.p, props.T)
|
||||||
@@ -913,6 +1037,98 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
|
|||||||
)
|
)
|
||||||
return derivative.as_vector()
|
return derivative.as_vector()
|
||||||
|
|
||||||
|
def linearize_state_derivative(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
*,
|
||||||
|
state_mass_tangent: Sequence[float],
|
||||||
|
state_energy_tangent: Sequence[float],
|
||||||
|
port_mass_flow_tangents: Mapping[str, Sequence[float]],
|
||||||
|
connected_h_tangents: Mapping[str, Sequence[float]],
|
||||||
|
property_linearization: ThermodynamicPropertiesLinearization | None = None,
|
||||||
|
flow_boundary_tolerance: float = 1.0e-12,
|
||||||
|
) -> Pnl0001DerivativeLinearization:
|
||||||
|
"""Linearize the pipe storage balance in a fixed stream mode."""
|
||||||
|
|
||||||
|
port_names = ("port_1", "port_2")
|
||||||
|
vectors = {
|
||||||
|
"state_mass": tuple(float(value) for value in state_mass_tangent),
|
||||||
|
"state_energy": tuple(float(value) for value in state_energy_tangent),
|
||||||
|
}
|
||||||
|
for port_name in port_names:
|
||||||
|
vectors[f"flow:{port_name}"] = tuple(
|
||||||
|
float(value) for value in port_mass_flow_tangents[port_name]
|
||||||
|
)
|
||||||
|
vectors[f"enthalpy:{port_name}"] = tuple(
|
||||||
|
float(value) for value in connected_h_tangents[port_name]
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors.values()}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("PNL0001 tangent vectors must have equal lengths.")
|
||||||
|
width = len(vectors["state_mass"])
|
||||||
|
invalid_reason: str | None = None
|
||||||
|
if not all(isfinite(value) for values in vectors.values() for value in values):
|
||||||
|
invalid_reason = "non_finite_tangent_input"
|
||||||
|
|
||||||
|
properties = property_linearization or self.medium.linearize_properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.volume,
|
||||||
|
vectors["state_mass"],
|
||||||
|
vectors["state_energy"],
|
||||||
|
(0.0,) * width,
|
||||||
|
)
|
||||||
|
if properties.tangents.width != width:
|
||||||
|
raise ValueError(
|
||||||
|
"PNL0001 property tangent width must match balance tangents."
|
||||||
|
)
|
||||||
|
props = properties.properties
|
||||||
|
if not properties.valid:
|
||||||
|
invalid_reason = invalid_reason or properties.reason
|
||||||
|
|
||||||
|
mass_derivative = self.port_1.m_flow + self.port_2.m_flow
|
||||||
|
energy_derivative = self.thermal_energy_flow_w(props.T)
|
||||||
|
mass_tangent = [0.0] * width
|
||||||
|
thermal_coefficient = (
|
||||||
|
0.0 if self.mode == 1 else self.kth * self.exchange_area
|
||||||
|
)
|
||||||
|
energy_tangent = [
|
||||||
|
-thermal_coefficient * properties.tangents.T[index]
|
||||||
|
for index in range(width)
|
||||||
|
]
|
||||||
|
|
||||||
|
for port_name in port_names:
|
||||||
|
port = self.get_port(port_name)
|
||||||
|
flow_tangent = vectors[f"flow:{port_name}"]
|
||||||
|
if (
|
||||||
|
abs(port.m_flow) <= flow_boundary_tolerance
|
||||||
|
and any(value != 0.0 for value in flow_tangent)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
f"flow_direction_boundary:{port_name}"
|
||||||
|
)
|
||||||
|
if port.m_flow > 0.0:
|
||||||
|
inlet_h = connected_h[port_name]
|
||||||
|
inlet_h_tangent = vectors[f"enthalpy:{port_name}"]
|
||||||
|
else:
|
||||||
|
inlet_h = props.h
|
||||||
|
inlet_h_tangent = properties.tangents.h
|
||||||
|
energy_derivative += port.m_flow * inlet_h
|
||||||
|
for index in range(width):
|
||||||
|
mass_tangent[index] += flow_tangent[index]
|
||||||
|
energy_tangent[index] += (
|
||||||
|
inlet_h * flow_tangent[index]
|
||||||
|
+ port.m_flow * inlet_h_tangent[index]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Pnl0001DerivativeLinearization(
|
||||||
|
derivative=(mass_derivative, energy_derivative),
|
||||||
|
tangents=(tuple(mass_tangent), tuple(energy_tangent)),
|
||||||
|
properties=properties,
|
||||||
|
valid=invalid_reason is None,
|
||||||
|
reason=invalid_reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AmesimPnl0002(AmesimPnl0001):
|
class AmesimPnl0002(AmesimPnl0001):
|
||||||
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
|
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from math import pi
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite, pi
|
||||||
|
|
||||||
from app.simulation.components.amesim.gases import (
|
from app.simulation.components.amesim.gases import (
|
||||||
AMESIM_GAS_INDEX_PARAMETER,
|
AMESIM_GAS_INDEX_PARAMETER,
|
||||||
@@ -18,6 +19,18 @@ from app.simulation.core.ports import PortDefinition
|
|||||||
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
|
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pnrp17Linearization:
|
||||||
|
volume: float
|
||||||
|
volume_flow: float
|
||||||
|
pressure_force: float
|
||||||
|
volume_tangent: tuple[float, ...]
|
||||||
|
volume_flow_tangent: tuple[float, ...]
|
||||||
|
pressure_force_tangent: tuple[float, ...]
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AmesimPnrp17(AlgebraicComponent):
|
class AmesimPnrp17(AlgebraicComponent):
|
||||||
"""AMESim PNRP17 pneumatic piston with two mechanical faces.
|
"""AMESim PNRP17 pneumatic piston with two mechanical faces.
|
||||||
|
|
||||||
@@ -230,6 +243,53 @@ class AmesimPnrp17(AlgebraicComponent):
|
|||||||
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
||||||
return {"port_1": (self.chamber_volume, self.chamber_volume_flow)}
|
return {"port_1": (self.chamber_volume, self.chamber_volume_flow)}
|
||||||
|
|
||||||
|
def linearize_geometry_and_force(
|
||||||
|
self,
|
||||||
|
port_4_x_tangent: Sequence[float],
|
||||||
|
port_5_x_tangent: Sequence[float],
|
||||||
|
port_4_v_tangent: Sequence[float],
|
||||||
|
port_5_v_tangent: Sequence[float],
|
||||||
|
port_1_pressure_tangent: Sequence[float],
|
||||||
|
) -> Pnrp17Linearization:
|
||||||
|
"""Return exact piston geometry and pressure-force tangents."""
|
||||||
|
|
||||||
|
vectors = tuple(
|
||||||
|
tuple(float(value) for value in values)
|
||||||
|
for values in (
|
||||||
|
port_4_x_tangent,
|
||||||
|
port_5_x_tangent,
|
||||||
|
port_4_v_tangent,
|
||||||
|
port_5_v_tangent,
|
||||||
|
port_1_pressure_tangent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("PNRP17 tangent vectors must have equal lengths.")
|
||||||
|
valid = all(isfinite(value) for values in vectors for value in values)
|
||||||
|
area = self.effective_area
|
||||||
|
volume_tangent = tuple(
|
||||||
|
area * (right - left)
|
||||||
|
for left, right in zip(vectors[0], vectors[1], strict=True)
|
||||||
|
)
|
||||||
|
volume_flow_tangent = tuple(
|
||||||
|
area * (right - left)
|
||||||
|
for left, right in zip(vectors[2], vectors[3], strict=True)
|
||||||
|
)
|
||||||
|
pressure_force_tangent = tuple(
|
||||||
|
area * value for value in vectors[4]
|
||||||
|
)
|
||||||
|
return Pnrp17Linearization(
|
||||||
|
volume=self.chamber_volume,
|
||||||
|
volume_flow=self.chamber_volume_flow,
|
||||||
|
pressure_force=self.pressure_force,
|
||||||
|
volume_tangent=volume_tangent,
|
||||||
|
volume_flow_tangent=volume_flow_tangent,
|
||||||
|
pressure_force_tangent=pressure_force_tangent,
|
||||||
|
valid=valid,
|
||||||
|
reason=None if valid else "non_finite_tangent_input",
|
||||||
|
)
|
||||||
|
|
||||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
self.port_1.h_outflow = connected_h.get(
|
self.port_1.h_outflow = connected_h.get(
|
||||||
"port_1",
|
"port_1",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from math import expm1
|
from dataclasses import dataclass
|
||||||
|
from math import expm1, isfinite
|
||||||
|
|
||||||
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
||||||
from app.simulation.core.catalog import (
|
from app.simulation.core.catalog import (
|
||||||
@@ -20,6 +21,24 @@ from app.simulation.core.medium import IdealGasMedium
|
|||||||
from app.simulation.core.ports import PortDefinition
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Mecmas21DerivativeLinearization:
|
||||||
|
derivative: tuple[float, float]
|
||||||
|
tangents: tuple[tuple[float, ...], tuple[float, ...]]
|
||||||
|
mode: str
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LstpContactForceLinearization:
|
||||||
|
force: float
|
||||||
|
force_tangent: tuple[float, ...]
|
||||||
|
mode: str
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
_MECMAS21_FRICTION_ENABLED = ParameterCondition("useFriction", (2.0,))
|
_MECMAS21_FRICTION_ENABLED = ParameterCondition("useFriction", (2.0,))
|
||||||
_MECMAS21_NON_RESTITUTION = ParameterCondition("stoptype", (1.0, 2.0, 4.0))
|
_MECMAS21_NON_RESTITUTION = ParameterCondition("stoptype", (1.0, 2.0, 4.0))
|
||||||
_MECMAS21_LIMITS_ENABLED = ParameterCondition("stoptype", (1.0, 2.0, 3.0))
|
_MECMAS21_LIMITS_ENABLED = ParameterCondition("stoptype", (1.0, 2.0, 3.0))
|
||||||
@@ -723,6 +742,204 @@ class AmesimMecmas21(DynamicComponent):
|
|||||||
)
|
)
|
||||||
return [self.acceleration(), velocity]
|
return [self.acceleration(), velocity]
|
||||||
|
|
||||||
|
def linearize_state_derivative(
|
||||||
|
self,
|
||||||
|
port_1_force_tangent: Sequence[float],
|
||||||
|
port_2_force_tangent: Sequence[float],
|
||||||
|
velocity_tangent: Sequence[float],
|
||||||
|
position_tangent: Sequence[float],
|
||||||
|
*,
|
||||||
|
constraint_mode: str = "current",
|
||||||
|
boundary_tolerance: float = 1.0e-12,
|
||||||
|
) -> Mecmas21DerivativeLinearization:
|
||||||
|
"""Linearize one inertia in a declared fixed mechanical mode."""
|
||||||
|
|
||||||
|
vectors = tuple(
|
||||||
|
tuple(float(value) for value in values)
|
||||||
|
for values in (
|
||||||
|
port_1_force_tangent,
|
||||||
|
port_2_force_tangent,
|
||||||
|
velocity_tangent,
|
||||||
|
position_tangent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("MECMAS21 tangent vectors must have equal lengths.")
|
||||||
|
width = len(vectors[0])
|
||||||
|
invalid_reason: str | None = None
|
||||||
|
if not all(isfinite(value) for values in vectors for value in values):
|
||||||
|
invalid_reason = "non_finite_tangent_input"
|
||||||
|
|
||||||
|
requested_mode = constraint_mode
|
||||||
|
if requested_mode == "current":
|
||||||
|
fixed = (
|
||||||
|
self._constraint_acceleration == 0.0
|
||||||
|
and self._constraint_velocity == 0.0
|
||||||
|
)
|
||||||
|
mode = "fixed" if fixed else "free"
|
||||||
|
if self._constraint_acceleration is not None and not fixed:
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
"group_acceleration_requires_aggregate"
|
||||||
|
)
|
||||||
|
elif requested_mode == "free":
|
||||||
|
mode = "free"
|
||||||
|
elif requested_mode in {"lower", "upper"}:
|
||||||
|
mode = requested_mode
|
||||||
|
fixed = (
|
||||||
|
self._constraint_acceleration == 0.0
|
||||||
|
and self._constraint_velocity == 0.0
|
||||||
|
)
|
||||||
|
if not fixed:
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
"constraint_mode_not_statically_fixed"
|
||||||
|
)
|
||||||
|
elif requested_mode == "uninitialized":
|
||||||
|
mode = requested_mode
|
||||||
|
invalid_reason = invalid_reason or "constraint_mode_uninitialized"
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"MECMAS21 constraint_mode must be current, free, lower, upper, "
|
||||||
|
"or uninitialized."
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode in {"fixed", "lower", "upper"}:
|
||||||
|
return Mecmas21DerivativeLinearization(
|
||||||
|
derivative=(self.acceleration(), 0.0),
|
||||||
|
tangents=((0.0,) * width, (0.0,) * width),
|
||||||
|
mode=mode,
|
||||||
|
valid=invalid_reason is None,
|
||||||
|
reason=invalid_reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
force_1_tangent, force_2_tangent, dv, dx = vectors
|
||||||
|
acceleration_tangent = [
|
||||||
|
force_1_tangent[index] + force_2_tangent[index]
|
||||||
|
for index in range(width)
|
||||||
|
]
|
||||||
|
if self.use_friction:
|
||||||
|
for index in range(width):
|
||||||
|
acceleration_tangent[index] += (
|
||||||
|
-self.rvisc * dv[index]
|
||||||
|
- 2.0 * self.wind * abs(self.v) * dv[index]
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.fcoul != 0.0
|
||||||
|
and abs(self.v) <= boundary_tolerance
|
||||||
|
and any(value != 0.0 for value in dv)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or "dry_friction_direction_boundary"
|
||||||
|
|
||||||
|
def add_limit_tangent(
|
||||||
|
*,
|
||||||
|
side: str,
|
||||||
|
stiffness: float,
|
||||||
|
damping: float,
|
||||||
|
damping_penetration: float,
|
||||||
|
bound: float,
|
||||||
|
damping_sign: float,
|
||||||
|
force_sign: float,
|
||||||
|
) -> None:
|
||||||
|
nonlocal invalid_reason
|
||||||
|
if int(self.stoptype) != 2:
|
||||||
|
return
|
||||||
|
penetration = (
|
||||||
|
bound - self.x if side == "lower" else self.x - bound
|
||||||
|
)
|
||||||
|
penetration_tangent = tuple(
|
||||||
|
(-value if side == "lower" else value) for value in dx
|
||||||
|
)
|
||||||
|
scale = max(abs(bound), abs(self.x), 1.0)
|
||||||
|
if penetration <= 0.0:
|
||||||
|
if (
|
||||||
|
abs(penetration) <= boundary_tolerance * scale
|
||||||
|
and any(value != 0.0 for value in penetration_tangent)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
f"soft_endstop_mode_boundary:{side}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if damping_penetration > 0.0:
|
||||||
|
fraction = min(penetration / damping_penetration, 1.0)
|
||||||
|
if penetration < damping_penetration:
|
||||||
|
fraction_tangent = tuple(
|
||||||
|
value / damping_penetration
|
||||||
|
for value in penetration_tangent
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fraction_tangent = (0.0,) * width
|
||||||
|
if (
|
||||||
|
abs(penetration - damping_penetration)
|
||||||
|
<= boundary_tolerance
|
||||||
|
* max(abs(damping_penetration), 1.0)
|
||||||
|
and any(value != 0.0 for value in penetration_tangent)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
f"soft_endstop_damping_boundary:{side}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fraction = 1.0
|
||||||
|
fraction_tangent = (0.0,) * width
|
||||||
|
|
||||||
|
raw_force = (
|
||||||
|
stiffness * penetration
|
||||||
|
+ damping_sign * fraction * damping * self.v
|
||||||
|
)
|
||||||
|
raw_tangent = tuple(
|
||||||
|
stiffness * penetration_tangent[index]
|
||||||
|
+ damping_sign
|
||||||
|
* damping
|
||||||
|
* (
|
||||||
|
fraction * dv[index]
|
||||||
|
+ self.v * fraction_tangent[index]
|
||||||
|
)
|
||||||
|
for index in range(width)
|
||||||
|
)
|
||||||
|
if int(self.discContactOption) != 1 and raw_force <= 0.0:
|
||||||
|
if (
|
||||||
|
abs(raw_force)
|
||||||
|
<= boundary_tolerance
|
||||||
|
* max(abs(stiffness * penetration), 1.0)
|
||||||
|
and any(value != 0.0 for value in raw_tangent)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
f"soft_endstop_force_boundary:{side}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
for index in range(width):
|
||||||
|
acceleration_tangent[index] += (
|
||||||
|
force_sign * raw_tangent[index]
|
||||||
|
)
|
||||||
|
|
||||||
|
add_limit_tangent(
|
||||||
|
side="lower",
|
||||||
|
stiffness=self.Kbmin,
|
||||||
|
damping=self.Dbmin,
|
||||||
|
damping_penetration=self.Pdmin,
|
||||||
|
bound=self.xmin,
|
||||||
|
damping_sign=-1.0,
|
||||||
|
force_sign=1.0,
|
||||||
|
)
|
||||||
|
add_limit_tangent(
|
||||||
|
side="upper",
|
||||||
|
stiffness=self.Kbmax,
|
||||||
|
damping=self.Dbmax,
|
||||||
|
damping_penetration=self.Pdmax,
|
||||||
|
bound=self.xmax,
|
||||||
|
damping_sign=1.0,
|
||||||
|
force_sign=-1.0,
|
||||||
|
)
|
||||||
|
acceleration_tangent = tuple(
|
||||||
|
value / self.mass for value in acceleration_tangent
|
||||||
|
)
|
||||||
|
return Mecmas21DerivativeLinearization(
|
||||||
|
derivative=(self.unconstrained_acceleration(), self.v),
|
||||||
|
tangents=(acceleration_tangent, tuple(dv)),
|
||||||
|
mode=mode,
|
||||||
|
valid=invalid_reason is None,
|
||||||
|
reason=invalid_reason,
|
||||||
|
)
|
||||||
|
|
||||||
def component_result_values(self) -> Mapping[str, float]:
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
return {
|
return {
|
||||||
"a": self.acceleration(),
|
"a": self.acceleration(),
|
||||||
@@ -979,6 +1196,112 @@ class AmesimLstp00a(AlgebraicComponent):
|
|||||||
)
|
)
|
||||||
return force if int(self.discContactOption) == 1 else max(force, 0.0)
|
return force if int(self.discContactOption) == 1 else max(force, 0.0)
|
||||||
|
|
||||||
|
def linearize_contact_force(
|
||||||
|
self,
|
||||||
|
port_1_x_tangent: Sequence[float],
|
||||||
|
port_2_x_tangent: Sequence[float],
|
||||||
|
port_1_velocity_tangent: Sequence[float],
|
||||||
|
port_2_velocity_tangent: Sequence[float],
|
||||||
|
*,
|
||||||
|
boundary_tolerance: float = 1.0e-12,
|
||||||
|
) -> LstpContactForceLinearization:
|
||||||
|
"""Linearize the elastic contact in its current unilateral mode."""
|
||||||
|
|
||||||
|
vectors = tuple(
|
||||||
|
tuple(float(value) for value in values)
|
||||||
|
for values in (
|
||||||
|
port_1_x_tangent,
|
||||||
|
port_2_x_tangent,
|
||||||
|
port_1_velocity_tangent,
|
||||||
|
port_2_velocity_tangent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("LSTP00A tangent vectors must have equal lengths.")
|
||||||
|
width = len(vectors[0])
|
||||||
|
if not all(isfinite(value) for values in vectors for value in values):
|
||||||
|
return LstpContactForceLinearization(
|
||||||
|
force=self.contact_force,
|
||||||
|
force_tangent=(0.0,) * width,
|
||||||
|
mode="invalid",
|
||||||
|
valid=False,
|
||||||
|
reason="non_finite_tangent_input",
|
||||||
|
)
|
||||||
|
|
||||||
|
dx_1, dx_2, dv_1, dv_2 = vectors
|
||||||
|
penetration_tangent = tuple(
|
||||||
|
left - right for left, right in zip(dx_1, dx_2, strict=True)
|
||||||
|
)
|
||||||
|
velocity_tangent = tuple(
|
||||||
|
left - right for left, right in zip(dv_1, dv_2, strict=True)
|
||||||
|
)
|
||||||
|
overlap = -self.gap
|
||||||
|
force = self.contact_force
|
||||||
|
scale = max(abs(self.gap0), abs(self.port_1.x), abs(self.port_2.x), 1.0)
|
||||||
|
if overlap <= 0.0:
|
||||||
|
on_boundary = abs(overlap) <= boundary_tolerance * scale
|
||||||
|
crossing = any(value != 0.0 for value in penetration_tangent)
|
||||||
|
return LstpContactForceLinearization(
|
||||||
|
force=force,
|
||||||
|
force_tangent=(0.0,) * width,
|
||||||
|
mode="boundary" if on_boundary else "inactive",
|
||||||
|
valid=not (on_boundary and crossing),
|
||||||
|
reason=(
|
||||||
|
"contact_mode_boundary"
|
||||||
|
if on_boundary and crossing
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
penetration = overlap
|
||||||
|
if self.Pdis > 0.0:
|
||||||
|
damping_fraction = -expm1(-penetration / self.Pdis)
|
||||||
|
damping_fraction_tangent = tuple(
|
||||||
|
(1.0 - damping_fraction) * value / self.Pdis
|
||||||
|
for value in penetration_tangent
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
damping_fraction = 1.0
|
||||||
|
damping_fraction_tangent = (0.0,) * width
|
||||||
|
relative_velocity = self.penetration_velocity
|
||||||
|
raw_force = (
|
||||||
|
self.kcont * penetration
|
||||||
|
+ damping_fraction * self.rcont * relative_velocity
|
||||||
|
)
|
||||||
|
raw_tangent = tuple(
|
||||||
|
self.kcont * penetration_tangent[index]
|
||||||
|
+ self.rcont
|
||||||
|
* (
|
||||||
|
damping_fraction * velocity_tangent[index]
|
||||||
|
+ relative_velocity * damping_fraction_tangent[index]
|
||||||
|
)
|
||||||
|
for index in range(width)
|
||||||
|
)
|
||||||
|
if int(self.discContactOption) != 1 and raw_force <= 0.0:
|
||||||
|
on_boundary = (
|
||||||
|
abs(raw_force)
|
||||||
|
<= boundary_tolerance
|
||||||
|
* max(abs(self.kcont * penetration), 1.0)
|
||||||
|
)
|
||||||
|
crossing = any(value != 0.0 for value in raw_tangent)
|
||||||
|
return LstpContactForceLinearization(
|
||||||
|
force=force,
|
||||||
|
force_tangent=(0.0,) * width,
|
||||||
|
mode="force_boundary" if on_boundary else "clamped",
|
||||||
|
valid=not (on_boundary and crossing),
|
||||||
|
reason=(
|
||||||
|
"contact_force_boundary"
|
||||||
|
if on_boundary and crossing
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return LstpContactForceLinearization(
|
||||||
|
force=force,
|
||||||
|
force_tangent=raw_tangent,
|
||||||
|
mode="active",
|
||||||
|
)
|
||||||
|
|
||||||
def clear_causal_contact(self) -> None:
|
def clear_causal_contact(self) -> None:
|
||||||
self._causal_penetration = None
|
self._causal_penetration = None
|
||||||
self._causal_contact_force = None
|
self._causal_contact_force = None
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from app.simulation.core.errors import RecoverableTrialStateError
|
from app.simulation.core.errors import RecoverableTrialStateError
|
||||||
@@ -9,6 +10,8 @@ from app.simulation.core.medium import (
|
|||||||
GasMedium,
|
GasMedium,
|
||||||
IdealGasMedium,
|
IdealGasMedium,
|
||||||
ThermodynamicProperties,
|
ThermodynamicProperties,
|
||||||
|
ThermodynamicPropertiesLinearization,
|
||||||
|
ThermodynamicPropertyTangents,
|
||||||
)
|
)
|
||||||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||||
from app.simulation.performance import profile_property, record_property_iterations
|
from app.simulation.performance import profile_property, record_property_iterations
|
||||||
@@ -309,6 +312,153 @@ class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def linearize_properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
dm: Sequence[float],
|
||||||
|
dU: Sequence[float],
|
||||||
|
dV: Sequence[float],
|
||||||
|
*,
|
||||||
|
properties: ThermodynamicProperties | None = None,
|
||||||
|
) -> ThermodynamicPropertiesLinearization:
|
||||||
|
"""Implicitly differentiate the Peng-Robinson m/U/V recovery."""
|
||||||
|
|
||||||
|
dm_values = tuple(float(value) for value in dm)
|
||||||
|
dU_values = tuple(float(value) for value in dU)
|
||||||
|
dV_values = tuple(float(value) for value in dV)
|
||||||
|
if not (len(dm_values) == len(dU_values) == len(dV_values)):
|
||||||
|
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
|
||||||
|
props = properties or self.properties_from_mU(m, U, V)
|
||||||
|
width = len(dm_values)
|
||||||
|
|
||||||
|
def invalid(reason: str) -> ThermodynamicPropertiesLinearization:
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_density = m / V
|
||||||
|
expected_internal_energy = U / m
|
||||||
|
if (
|
||||||
|
abs(props.rho - expected_density)
|
||||||
|
> 1.0e-12 * max(abs(expected_density), 1.0)
|
||||||
|
or abs(props.u - expected_internal_energy)
|
||||||
|
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
|
||||||
|
):
|
||||||
|
return invalid("properties_primal_mismatch")
|
||||||
|
if not all(
|
||||||
|
isfinite(value)
|
||||||
|
for values in (dm_values, dU_values, dV_values)
|
||||||
|
for value in values
|
||||||
|
):
|
||||||
|
return invalid("non_finite_tangent_input")
|
||||||
|
if props.T <= 2.2 * (1.0 + 1.0e-10):
|
||||||
|
return invalid("temperature_floor_boundary")
|
||||||
|
|
||||||
|
pressure_temperature_derivative = (
|
||||||
|
self.fluid.pressure_temperature_derivative_at_density(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pressure_density_derivative = (
|
||||||
|
self.fluid.pressure_density_derivative_at_temperature(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cv = (
|
||||||
|
self.cv_at_temperature(props.T)
|
||||||
|
+ self.fluid.residual_isochoric_heat_capacity_at_density(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recovered_internal_energy = (
|
||||||
|
self.specific_internal_energy(props.T)
|
||||||
|
+ self.fluid.residual_specific_internal_energy_at_density(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recovery_scale = max(
|
||||||
|
abs(props.u),
|
||||||
|
abs(cv * props.T) if isfinite(cv) else 0.0,
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not all(
|
||||||
|
isfinite(value)
|
||||||
|
for value in (
|
||||||
|
pressure_temperature_derivative,
|
||||||
|
pressure_density_derivative,
|
||||||
|
cv,
|
||||||
|
recovered_internal_energy,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or cv <= 0.0
|
||||||
|
):
|
||||||
|
return invalid("invalid_peng_robinson_derivative")
|
||||||
|
if abs(recovered_internal_energy - props.u) > 1.0e-8 * recovery_scale:
|
||||||
|
return invalid("properties_recovery_not_converged")
|
||||||
|
|
||||||
|
internal_energy_density_derivative = (
|
||||||
|
props.p - props.T * pressure_temperature_derivative
|
||||||
|
) / (props.rho * props.rho)
|
||||||
|
drho: list[float] = []
|
||||||
|
du: list[float] = []
|
||||||
|
dT: list[float] = []
|
||||||
|
dp: list[float] = []
|
||||||
|
dh: list[float] = []
|
||||||
|
for mass_tangent, energy_tangent, volume_tangent in zip(
|
||||||
|
dm_values,
|
||||||
|
dU_values,
|
||||||
|
dV_values,
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
density_tangent = (
|
||||||
|
mass_tangent / V - m * volume_tangent / (V * V)
|
||||||
|
)
|
||||||
|
internal_energy_tangent = (
|
||||||
|
energy_tangent / m - U * mass_tangent / (m * m)
|
||||||
|
)
|
||||||
|
temperature_tangent = (
|
||||||
|
internal_energy_tangent
|
||||||
|
- internal_energy_density_derivative * density_tangent
|
||||||
|
) / cv
|
||||||
|
pressure_tangent = (
|
||||||
|
pressure_temperature_derivative * temperature_tangent
|
||||||
|
+ pressure_density_derivative * density_tangent
|
||||||
|
)
|
||||||
|
enthalpy_tangent = (
|
||||||
|
internal_energy_tangent
|
||||||
|
+ pressure_tangent / props.rho
|
||||||
|
- props.p * density_tangent / (props.rho * props.rho)
|
||||||
|
)
|
||||||
|
drho.append(density_tangent)
|
||||||
|
du.append(internal_energy_tangent)
|
||||||
|
dT.append(temperature_tangent)
|
||||||
|
dp.append(pressure_tangent)
|
||||||
|
dh.append(enthalpy_tangent)
|
||||||
|
|
||||||
|
tangent_values = (*drho, *du, *dT, *dp, *dh)
|
||||||
|
if not all(isfinite(value) for value in tangent_values):
|
||||||
|
return invalid("non_finite_property_tangent")
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents(
|
||||||
|
p=tuple(dp),
|
||||||
|
T=tuple(dT),
|
||||||
|
rho=tuple(drho),
|
||||||
|
u=tuple(du),
|
||||||
|
h=tuple(dh),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AmesimGasPropertyModelSpec:
|
class AmesimGasPropertyModelSpec:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite
|
||||||
|
|
||||||
from app.simulation.components.amesim.gases import (
|
from app.simulation.components.amesim.gases import (
|
||||||
AMESIM_GAS_INDEX_PARAMETER,
|
AMESIM_GAS_INDEX_PARAMETER,
|
||||||
@@ -14,11 +16,24 @@ from app.simulation.core.metadata import (
|
|||||||
ResultVariableDefinition,
|
ResultVariableDefinition,
|
||||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||||
)
|
)
|
||||||
from app.simulation.core.medium import GasMedium, ThermodynamicProperties
|
from app.simulation.core.medium import (
|
||||||
|
GasMedium,
|
||||||
|
ThermodynamicProperties,
|
||||||
|
ThermodynamicPropertiesLinearization,
|
||||||
|
)
|
||||||
from app.simulation.core.ports import PortDefinition
|
from app.simulation.core.ports import PortDefinition
|
||||||
from app.simulation.core.state import VolumeState
|
from app.simulation.core.state import VolumeState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pnch012DerivativeLinearization:
|
||||||
|
derivative: tuple[float, float]
|
||||||
|
tangents: tuple[tuple[float, ...], tuple[float, ...]]
|
||||||
|
properties: ThermodynamicPropertiesLinearization
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AmesimPnch023(ThermodynamicVolumeComponent):
|
class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||||
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
||||||
|
|
||||||
@@ -518,6 +533,132 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
|||||||
energy_derivative -= props.p * self.total_volume_rate()
|
energy_derivative -= props.p * self.total_volume_rate()
|
||||||
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
||||||
|
|
||||||
|
def linearize_state_derivative(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
*,
|
||||||
|
state_mass_tangent: Sequence[float],
|
||||||
|
state_energy_tangent: Sequence[float],
|
||||||
|
external_volume_tangent: Sequence[float],
|
||||||
|
external_volume_rate_tangent: Sequence[float],
|
||||||
|
port_mass_flow_tangents: Mapping[str, Sequence[float]],
|
||||||
|
connected_h_tangents: Mapping[str, Sequence[float]],
|
||||||
|
property_linearization: ThermodynamicPropertiesLinearization | None = None,
|
||||||
|
flow_boundary_tolerance: float = 1.0e-12,
|
||||||
|
) -> Pnch012DerivativeLinearization:
|
||||||
|
"""Linearize the chamber balance while keeping stream modes fixed."""
|
||||||
|
|
||||||
|
port_names = ("port_1", "port_2", "port_3", "port_4")
|
||||||
|
vectors = {
|
||||||
|
"state_mass": tuple(float(value) for value in state_mass_tangent),
|
||||||
|
"state_energy": tuple(float(value) for value in state_energy_tangent),
|
||||||
|
"volume": tuple(float(value) for value in external_volume_tangent),
|
||||||
|
"volume_rate": tuple(
|
||||||
|
float(value) for value in external_volume_rate_tangent
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for port_name in port_names:
|
||||||
|
vectors[f"flow:{port_name}"] = tuple(
|
||||||
|
float(value) for value in port_mass_flow_tangents[port_name]
|
||||||
|
)
|
||||||
|
vectors[f"enthalpy:{port_name}"] = tuple(
|
||||||
|
float(value) for value in connected_h_tangents[port_name]
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors.values()}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("PNCH012 tangent vectors must have equal lengths.")
|
||||||
|
width = len(vectors["state_mass"])
|
||||||
|
invalid_reason: str | None = None
|
||||||
|
if not all(isfinite(value) for values in vectors.values() for value in values):
|
||||||
|
invalid_reason = "non_finite_tangent_input"
|
||||||
|
|
||||||
|
raw_volume = (
|
||||||
|
self.cvol0
|
||||||
|
+ sum(self.external_volumes.values())
|
||||||
|
+ self.connected_external_volume()
|
||||||
|
)
|
||||||
|
minimum_volume = self.cvol0 / 100.0
|
||||||
|
volume_scale = max(abs(raw_volume), abs(minimum_volume), 1.0e-18)
|
||||||
|
on_volume_boundary = (
|
||||||
|
abs(raw_volume - minimum_volume) <= 1.0e-12 * volume_scale
|
||||||
|
)
|
||||||
|
supplied_volume_tangent = vectors["volume"]
|
||||||
|
if raw_volume < minimum_volume or on_volume_boundary:
|
||||||
|
used_volume_tangent = (0.0,) * width
|
||||||
|
used_volume_rate_tangent = (0.0,) * width
|
||||||
|
if on_volume_boundary and any(
|
||||||
|
value != 0.0
|
||||||
|
for value in (
|
||||||
|
*supplied_volume_tangent,
|
||||||
|
*vectors["volume_rate"],
|
||||||
|
)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or "volume_floor_boundary"
|
||||||
|
else:
|
||||||
|
used_volume_tangent = supplied_volume_tangent
|
||||||
|
used_volume_rate_tangent = vectors["volume_rate"]
|
||||||
|
|
||||||
|
properties = property_linearization or self.medium.linearize_properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.total_volume(),
|
||||||
|
vectors["state_mass"],
|
||||||
|
vectors["state_energy"],
|
||||||
|
used_volume_tangent,
|
||||||
|
)
|
||||||
|
if properties.tangents.width != width:
|
||||||
|
raise ValueError(
|
||||||
|
"PNCH012 property tangent width must match balance tangents."
|
||||||
|
)
|
||||||
|
props = properties.properties
|
||||||
|
if not properties.valid:
|
||||||
|
invalid_reason = invalid_reason or properties.reason
|
||||||
|
|
||||||
|
mass_derivative = sum(
|
||||||
|
self.get_port(port_name).m_flow for port_name in port_names
|
||||||
|
)
|
||||||
|
volume_rate = self.total_volume_rate()
|
||||||
|
energy_derivative = self.thermal_energy_flow_w(props.T) - props.p * volume_rate
|
||||||
|
mass_tangent = [0.0] * width
|
||||||
|
energy_tangent = [
|
||||||
|
-self.kth * self.sth * properties.tangents.T[index]
|
||||||
|
- volume_rate * properties.tangents.p[index]
|
||||||
|
- props.p * used_volume_rate_tangent[index]
|
||||||
|
for index in range(width)
|
||||||
|
]
|
||||||
|
|
||||||
|
for port_name in port_names:
|
||||||
|
port = self.get_port(port_name)
|
||||||
|
flow_tangent = vectors[f"flow:{port_name}"]
|
||||||
|
if (
|
||||||
|
abs(port.m_flow) <= flow_boundary_tolerance
|
||||||
|
and any(value != 0.0 for value in flow_tangent)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
f"flow_direction_boundary:{port_name}"
|
||||||
|
)
|
||||||
|
if port.m_flow > 0.0:
|
||||||
|
inlet_h = connected_h[port_name]
|
||||||
|
inlet_h_tangent = vectors[f"enthalpy:{port_name}"]
|
||||||
|
else:
|
||||||
|
inlet_h = props.h
|
||||||
|
inlet_h_tangent = properties.tangents.h
|
||||||
|
energy_derivative += port.m_flow * inlet_h
|
||||||
|
for index in range(width):
|
||||||
|
mass_tangent[index] += flow_tangent[index]
|
||||||
|
energy_tangent[index] += (
|
||||||
|
inlet_h * flow_tangent[index]
|
||||||
|
+ port.m_flow * inlet_h_tangent[index]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Pnch012DerivativeLinearization(
|
||||||
|
derivative=(mass_derivative, energy_derivative),
|
||||||
|
tangents=(tuple(mass_tangent), tuple(energy_tangent)),
|
||||||
|
properties=properties,
|
||||||
|
valid=invalid_reason is None,
|
||||||
|
reason=invalid_reason,
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
pressure = self.medium.properties_from_mU(
|
pressure = self.medium.properties_from_mU(
|
||||||
self.state.m,
|
self.state.m,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 元件建模规范与示例
|
# 元件建模规范与示例
|
||||||
|
|
||||||
规范的权威版本位于
|
规范的权威版本位于
|
||||||
[`docs/component-model-authoring-spec-v1.md`](../../../docs/component-model-authoring-spec-v1.md)。
|
[`docs/standard/component-model-authoring-spec-v1.md`](../../../docs/standard/component-model-authoring-spec-v1.md)。
|
||||||
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
|
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
|
||||||
修改中同步,不能让示例形成另一套规则。
|
修改中同步,不能让示例形成另一套规则。
|
||||||
|
|
||||||
@@ -279,4 +279,4 @@ models=(
|
|||||||
10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。
|
10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。
|
||||||
|
|
||||||
组件库、分类和自动发现的完整规则参见
|
组件库、分类和自动发现的完整规则参见
|
||||||
[`组件库分类、发现与读取规范 v1`](../../../docs/component-library-spec-v1.md)。
|
[`组件库分类、发现与读取规范 v1`](../../../docs/standard/component-library-spec-v1.md)。
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Protocol
|
from math import isfinite
|
||||||
|
from typing import Protocol, Sequence
|
||||||
|
|
||||||
from app.simulation.core.errors import RecoverableTrialStateError
|
from app.simulation.core.errors import RecoverableTrialStateError
|
||||||
from app.simulation.performance import profile_property
|
from app.simulation.performance import profile_property
|
||||||
@@ -16,6 +17,36 @@ class ThermodynamicProperties:
|
|||||||
h: float
|
h: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThermodynamicPropertyTangents:
|
||||||
|
"""Directional derivatives of a recovered thermodynamic state."""
|
||||||
|
|
||||||
|
p: tuple[float, ...]
|
||||||
|
T: tuple[float, ...]
|
||||||
|
rho: tuple[float, ...]
|
||||||
|
u: tuple[float, ...]
|
||||||
|
h: tuple[float, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
return len(self.p)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def zeros(cls, width: int) -> "ThermodynamicPropertyTangents":
|
||||||
|
values = (0.0,) * width
|
||||||
|
return cls(p=values, T=values, rho=values, u=values, h=values)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThermodynamicPropertiesLinearization:
|
||||||
|
"""Primal properties and a validity-checked directional linearization."""
|
||||||
|
|
||||||
|
properties: ThermodynamicProperties
|
||||||
|
tangents: ThermodynamicPropertyTangents
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class GasMedium(Protocol):
|
class GasMedium(Protocol):
|
||||||
"""Thermodynamic contract required by pneumatic components.
|
"""Thermodynamic contract required by pneumatic components.
|
||||||
|
|
||||||
@@ -75,6 +106,18 @@ class GasMedium(Protocol):
|
|||||||
V: float,
|
V: float,
|
||||||
) -> ThermodynamicProperties: ...
|
) -> ThermodynamicProperties: ...
|
||||||
|
|
||||||
|
def linearize_properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
dm: Sequence[float],
|
||||||
|
dU: Sequence[float],
|
||||||
|
dV: Sequence[float],
|
||||||
|
*,
|
||||||
|
properties: ThermodynamicProperties | None = None,
|
||||||
|
) -> ThermodynamicPropertiesLinearization: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class IdealGasMedium:
|
class IdealGasMedium:
|
||||||
@@ -224,3 +267,100 @@ class IdealGasMedium:
|
|||||||
u = U / m
|
u = U / m
|
||||||
h = self.specific_enthalpy(T)
|
h = self.specific_enthalpy(T)
|
||||||
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
|
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
|
||||||
|
|
||||||
|
def linearize_properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
dm: Sequence[float],
|
||||||
|
dU: Sequence[float],
|
||||||
|
dV: Sequence[float],
|
||||||
|
*,
|
||||||
|
properties: ThermodynamicProperties | None = None,
|
||||||
|
) -> ThermodynamicPropertiesLinearization:
|
||||||
|
"""Linearize properties_from_mU for several seed directions."""
|
||||||
|
|
||||||
|
dm_values = tuple(float(value) for value in dm)
|
||||||
|
dU_values = tuple(float(value) for value in dU)
|
||||||
|
dV_values = tuple(float(value) for value in dV)
|
||||||
|
if not (len(dm_values) == len(dU_values) == len(dV_values)):
|
||||||
|
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
|
||||||
|
props = properties or self.properties_from_mU(m, U, V)
|
||||||
|
width = len(dm_values)
|
||||||
|
expected_density = m / V
|
||||||
|
expected_internal_energy = U / m
|
||||||
|
if (
|
||||||
|
abs(props.rho - expected_density)
|
||||||
|
> 1.0e-12 * max(abs(expected_density), 1.0)
|
||||||
|
or abs(props.u - expected_internal_energy)
|
||||||
|
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
|
||||||
|
):
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason="properties_primal_mismatch",
|
||||||
|
)
|
||||||
|
if not all(
|
||||||
|
isfinite(value)
|
||||||
|
for values in (dm_values, dU_values, dV_values)
|
||||||
|
for value in values
|
||||||
|
):
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason="non_finite_tangent_input",
|
||||||
|
)
|
||||||
|
|
||||||
|
cv = self.cv_at_temperature(props.T)
|
||||||
|
cp = self.cp_at_temperature(props.T)
|
||||||
|
if not isfinite(cv) or not isfinite(cp) or cv <= 0.0 or cp <= 0.0:
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason="non_positive_heat_capacity",
|
||||||
|
)
|
||||||
|
|
||||||
|
drho: list[float] = []
|
||||||
|
du: list[float] = []
|
||||||
|
dT: list[float] = []
|
||||||
|
dp: list[float] = []
|
||||||
|
dh: list[float] = []
|
||||||
|
for mass_tangent, energy_tangent, volume_tangent in zip(
|
||||||
|
dm_values,
|
||||||
|
dU_values,
|
||||||
|
dV_values,
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
density_tangent = mass_tangent / V - m * volume_tangent / (V * V)
|
||||||
|
internal_energy_tangent = (
|
||||||
|
energy_tangent / m - U * mass_tangent / (m * m)
|
||||||
|
)
|
||||||
|
temperature_tangent = internal_energy_tangent / cv
|
||||||
|
pressure_tangent = self.R_gas * (
|
||||||
|
props.T * density_tangent + props.rho * temperature_tangent
|
||||||
|
)
|
||||||
|
enthalpy_tangent = cp * temperature_tangent
|
||||||
|
drho.append(density_tangent)
|
||||||
|
du.append(internal_energy_tangent)
|
||||||
|
dT.append(temperature_tangent)
|
||||||
|
dp.append(pressure_tangent)
|
||||||
|
dh.append(enthalpy_tangent)
|
||||||
|
|
||||||
|
tangent_values = (*drho, *du, *dT, *dp, *dh)
|
||||||
|
valid = all(isfinite(value) for value in tangent_values)
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents(
|
||||||
|
p=tuple(dp),
|
||||||
|
T=tuple(dT),
|
||||||
|
rho=tuple(drho),
|
||||||
|
u=tuple(du),
|
||||||
|
h=tuple(dh),
|
||||||
|
),
|
||||||
|
valid=valid,
|
||||||
|
reason=None if valid else "non_finite_property_tangent",
|
||||||
|
)
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -12,6 +12,7 @@ CancellationCheck = Callable[[], bool]
|
|||||||
AcceptedStepCallback = Callable[[float], None]
|
AcceptedStepCallback = Callable[[float], None]
|
||||||
IntegrationStatus = Literal["completed", "cancelled", "failed"]
|
IntegrationStatus = Literal["completed", "cancelled", "failed"]
|
||||||
DenseState = Callable[[float], list[float]]
|
DenseState = Callable[[float], list[float]]
|
||||||
|
JacobianCallable = Callable[[float, object], object]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -30,7 +31,9 @@ StateTransitionHandler = Callable[
|
|||||||
_MAX_STATE_TRANSITIONS_AT_SAME_TIME = 64
|
_MAX_STATE_TRANSITIONS_AT_SAME_TIME = 64
|
||||||
|
|
||||||
|
|
||||||
class _IntegrationCancelled(Exception):
|
class IntegrationCancelled(Exception):
|
||||||
|
"""Internal control-flow signal shared by RHS and Jacobian evaluation."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -59,9 +62,19 @@ class SolverSegmentDiagnostics:
|
|||||||
solver_start_count: int = 0
|
solver_start_count: int = 0
|
||||||
state_transition_count: int = 0
|
state_transition_count: int = 0
|
||||||
recoverable_retry_count: int = 0
|
recoverable_retry_count: int = 0
|
||||||
|
jacobian_evaluation_count: int = 0
|
||||||
|
jacobian_full_build_count: int = 0
|
||||||
|
jacobian_secant_reuse_count: int = 0
|
||||||
|
jacobian_audit_failure_count: int = 0
|
||||||
|
finite_difference_rhs_evaluation_count: int = 0
|
||||||
|
jacobian_base_rhs_evaluation_count: int = 0
|
||||||
|
jacobian_jv_audit_rhs_evaluation_count: int = 0
|
||||||
|
exact_column_build_count: int = 0
|
||||||
|
exact_column_fallback_count: int = 0
|
||||||
|
jacobian_assembly_seconds: float = 0.0
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, float | int]:
|
def as_dict(self) -> dict[str, float | int]:
|
||||||
return {
|
result: dict[str, float | int] = {
|
||||||
"startTime": self.start_time,
|
"startTime": self.start_time,
|
||||||
"requestedStopTime": self.requested_stop_time,
|
"requestedStopTime": self.requested_stop_time,
|
||||||
"simulatedUntil": self.simulated_until,
|
"simulatedUntil": self.simulated_until,
|
||||||
@@ -73,6 +86,61 @@ class SolverSegmentDiagnostics:
|
|||||||
"stateTransitionCount": self.state_transition_count,
|
"stateTransitionCount": self.state_transition_count,
|
||||||
"recoverableRetryCount": self.recoverable_retry_count,
|
"recoverableRetryCount": self.recoverable_retry_count,
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
self.jacobian_evaluation_count
|
||||||
|
or self.finite_difference_rhs_evaluation_count
|
||||||
|
or self.jacobian_assembly_seconds
|
||||||
|
):
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"jacobianEvaluationCount": self.jacobian_evaluation_count,
|
||||||
|
"jacobianFullBuildCount": self.jacobian_full_build_count,
|
||||||
|
"jacobianSecantReuseCount": self.jacobian_secant_reuse_count,
|
||||||
|
"jacobianAuditFailureCount": self.jacobian_audit_failure_count,
|
||||||
|
"finiteDifferenceRhsEvaluationCount": (
|
||||||
|
self.finite_difference_rhs_evaluation_count
|
||||||
|
),
|
||||||
|
"jacobianBaseRhsEvaluationCount": (
|
||||||
|
self.jacobian_base_rhs_evaluation_count
|
||||||
|
),
|
||||||
|
"jacobianJvAuditRhsEvaluationCount": (
|
||||||
|
self.jacobian_jv_audit_rhs_evaluation_count
|
||||||
|
),
|
||||||
|
"exactColumnBuildCount": self.exact_column_build_count,
|
||||||
|
"exactColumnFallbackCount": (
|
||||||
|
self.exact_column_fallback_count
|
||||||
|
),
|
||||||
|
"jacobianAssemblySeconds": self.jacobian_assembly_seconds,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
_JACOBIAN_DIAGNOSTIC_KEYS = (
|
||||||
|
"jacobianEvaluationCount",
|
||||||
|
"fullBuildCount",
|
||||||
|
"secantReuseCount",
|
||||||
|
"auditFailureCount",
|
||||||
|
"finiteDifferenceRhsEvaluationCount",
|
||||||
|
"baseRhsEvaluationCount",
|
||||||
|
"jvAuditEvaluationCount",
|
||||||
|
"exactColumnBuildCount",
|
||||||
|
"exactColumnFallbackCount",
|
||||||
|
"assemblySeconds",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _jacobian_diagnostic_snapshot(
|
||||||
|
jac: JacobianCallable | None,
|
||||||
|
) -> dict[str, float]:
|
||||||
|
diagnostics = getattr(jac, "diagnostics", None)
|
||||||
|
if diagnostics is None:
|
||||||
|
return {key: 0.0 for key in _JACOBIAN_DIAGNOSTIC_KEYS}
|
||||||
|
values = diagnostics()
|
||||||
|
return {
|
||||||
|
key: float(values.get(key, 0.0))
|
||||||
|
for key in _JACOBIAN_DIAGNOSTIC_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -309,7 +377,7 @@ def _runge_kutta_4(
|
|||||||
for target_time in t_eval[1:]:
|
for target_time in t_eval[1:]:
|
||||||
while current_time < target_time:
|
while current_time < target_time:
|
||||||
if cancel_check is not None and cancel_check():
|
if cancel_check is not None and cancel_check():
|
||||||
raise _IntegrationCancelled
|
raise IntegrationCancelled
|
||||||
dt = min(config.max_step, target_time - current_time)
|
dt = min(config.max_step, target_time - current_time)
|
||||||
k1 = rhs(current_time, state)
|
k1 = rhs(current_time, state)
|
||||||
k2 = rhs(current_time + 0.5 * dt, _vector_add(state, k1, 0.5 * dt))
|
k2 = rhs(current_time + 0.5 * dt, _vector_add(state, k1, 0.5 * dt))
|
||||||
@@ -374,7 +442,7 @@ def _runge_kutta_4(
|
|||||||
report_step(current_time)
|
report_step(current_time)
|
||||||
|
|
||||||
_append_solution_sample(times, states, target_time, state)
|
_append_solution_sample(times, states, target_time, state)
|
||||||
except _IntegrationCancelled:
|
except IntegrationCancelled:
|
||||||
status = "cancelled"
|
status = "cancelled"
|
||||||
message = "Simulation was stopped before reaching the requested end time."
|
message = "Simulation was stopped before reaching the requested end time."
|
||||||
_append_solution_sample(times, states, current_time, state)
|
_append_solution_sample(times, states, current_time, state)
|
||||||
@@ -451,7 +519,7 @@ def _runge_kutta_4_segmented(
|
|||||||
nonlocal current_time, last_transition, same_time_transition_count, state
|
nonlocal current_time, last_transition, same_time_transition_count, state
|
||||||
while current_time < target_time:
|
while current_time < target_time:
|
||||||
if cancel_check is not None and cancel_check():
|
if cancel_check is not None and cancel_check():
|
||||||
raise _IntegrationCancelled
|
raise IntegrationCancelled
|
||||||
dt = min(config.max_step, target_time - current_time)
|
dt = min(config.max_step, target_time - current_time)
|
||||||
k1 = rhs(current_time, state)
|
k1 = rhs(current_time, state)
|
||||||
k2 = rhs(
|
k2 = rhs(
|
||||||
@@ -565,7 +633,7 @@ def _runge_kutta_4_segmented(
|
|||||||
sample_time = float(sample_times[sample_index])
|
sample_time = float(sample_times[sample_index])
|
||||||
_append_solution_sample(times, states, sample_time, state)
|
_append_solution_sample(times, states, sample_time, state)
|
||||||
sample_index += 1
|
sample_index += 1
|
||||||
except _IntegrationCancelled:
|
except IntegrationCancelled:
|
||||||
status = "cancelled"
|
status = "cancelled"
|
||||||
message = "Simulation was stopped before reaching the requested end time."
|
message = "Simulation was stopped before reaching the requested end time."
|
||||||
_append_solution_sample(times, states, current_time, state)
|
_append_solution_sample(times, states, current_time, state)
|
||||||
@@ -595,6 +663,7 @@ def _integrate_scipy_stepwise(
|
|||||||
breakpoints: Sequence[float] = (),
|
breakpoints: Sequence[float] = (),
|
||||||
state_transition_handler: StateTransitionHandler | None = None,
|
state_transition_handler: StateTransitionHandler | None = None,
|
||||||
jac_sparsity=None,
|
jac_sparsity=None,
|
||||||
|
jac: JacobianCallable | None = None,
|
||||||
) -> ODESolution:
|
) -> ODESolution:
|
||||||
"""Initial stepwise integration path for breakpoints and state resets.
|
"""Initial stepwise integration path for breakpoints and state resets.
|
||||||
|
|
||||||
@@ -617,6 +686,7 @@ def _integrate_scipy_stepwise(
|
|||||||
solver_type = solver_types.get(config.method)
|
solver_type = solver_types.get(config.method)
|
||||||
if solver_type is None:
|
if solver_type is None:
|
||||||
raise ValueError(f"Unsupported integration method: {config.method}")
|
raise ValueError(f"Unsupported integration method: {config.method}")
|
||||||
|
implicit_jac = jac if config.method in {"BDF", "Radau"} else None
|
||||||
|
|
||||||
times = [float(config.t_start)]
|
times = [float(config.t_start)]
|
||||||
states = [[float(value)] for value in initial_state]
|
states = [[float(value)] for value in initial_state]
|
||||||
@@ -632,8 +702,13 @@ def _integrate_scipy_stepwise(
|
|||||||
|
|
||||||
def cancellable_rhs(time, state):
|
def cancellable_rhs(time, state):
|
||||||
if cancel_check():
|
if cancel_check():
|
||||||
raise _IntegrationCancelled
|
raise IntegrationCancelled
|
||||||
return rhs(float(time), [float(value) for value in state])
|
normalized_state = [float(value) for value in state]
|
||||||
|
derivative = rhs(float(time), normalized_state)
|
||||||
|
observer = getattr(implicit_jac, "observe", None)
|
||||||
|
if observer is not None:
|
||||||
|
observer(float(time), normalized_state, derivative)
|
||||||
|
return derivative
|
||||||
|
|
||||||
status: IntegrationStatus = "completed"
|
status: IntegrationStatus = "completed"
|
||||||
message = "The solver successfully reached the end of the integration interval."
|
message = "The solver successfully reached the end of the integration interval."
|
||||||
@@ -683,6 +758,7 @@ def _integrate_scipy_stepwise(
|
|||||||
segment_solver_starts = 0
|
segment_solver_starts = 0
|
||||||
segment_state_transitions = 0
|
segment_state_transitions = 0
|
||||||
segment_recoverable_retries = 0
|
segment_recoverable_retries = 0
|
||||||
|
jacobian_work_start = _jacobian_diagnostic_snapshot(implicit_jac)
|
||||||
|
|
||||||
while has_integration_interval and last_accepted_time < integration_end:
|
while has_integration_interval and last_accepted_time < integration_end:
|
||||||
if cancel_check():
|
if cancel_check():
|
||||||
@@ -695,8 +771,11 @@ def _integrate_scipy_stepwise(
|
|||||||
"atol": config.atol,
|
"atol": config.atol,
|
||||||
"max_step": segment_max_step,
|
"max_step": segment_max_step,
|
||||||
}
|
}
|
||||||
if jac_sparsity is not None and config.method in {"BDF", "Radau"}:
|
if config.method in {"BDF", "Radau"}:
|
||||||
solver_options["jac_sparsity"] = jac_sparsity
|
if implicit_jac is not None:
|
||||||
|
solver_options["jac"] = implicit_jac
|
||||||
|
elif jac_sparsity is not None:
|
||||||
|
solver_options["jac_sparsity"] = jac_sparsity
|
||||||
requested_first_step = (
|
requested_first_step = (
|
||||||
0.1 * segment_max_step
|
0.1 * segment_max_step
|
||||||
if last_recoverable_error is not None
|
if last_recoverable_error is not None
|
||||||
@@ -709,6 +788,9 @@ def _integrate_scipy_stepwise(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
start_segment = getattr(implicit_jac, "start_segment", None)
|
||||||
|
if start_segment is not None:
|
||||||
|
start_segment()
|
||||||
solver = solver_type(
|
solver = solver_type(
|
||||||
cancellable_rhs,
|
cancellable_rhs,
|
||||||
last_accepted_time,
|
last_accepted_time,
|
||||||
@@ -716,7 +798,7 @@ def _integrate_scipy_stepwise(
|
|||||||
integration_end,
|
integration_end,
|
||||||
**solver_options,
|
**solver_options,
|
||||||
)
|
)
|
||||||
except _IntegrationCancelled:
|
except IntegrationCancelled:
|
||||||
status = "cancelled"
|
status = "cancelled"
|
||||||
message = cancellation_message()
|
message = cancellation_message()
|
||||||
break
|
break
|
||||||
@@ -755,7 +837,7 @@ def _integrate_scipy_stepwise(
|
|||||||
step_start_state = list(last_accepted_state)
|
step_start_state = list(last_accepted_state)
|
||||||
try:
|
try:
|
||||||
step_message = solver.step()
|
step_message = solver.step()
|
||||||
except _IntegrationCancelled:
|
except IntegrationCancelled:
|
||||||
status = "cancelled"
|
status = "cancelled"
|
||||||
message = (
|
message = (
|
||||||
"Simulation was stopped before reaching the requested end time."
|
"Simulation was stopped before reaching the requested end time."
|
||||||
@@ -957,6 +1039,11 @@ def _integrate_scipy_stepwise(
|
|||||||
if not restart_at_transition:
|
if not restart_at_transition:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
jacobian_work_end = _jacobian_diagnostic_snapshot(implicit_jac)
|
||||||
|
jacobian_work = {
|
||||||
|
key: jacobian_work_end[key] - jacobian_work_start[key]
|
||||||
|
for key in _JACOBIAN_DIAGNOSTIC_KEYS
|
||||||
|
}
|
||||||
solver_segments.append(
|
solver_segments.append(
|
||||||
SolverSegmentDiagnostics(
|
SolverSegmentDiagnostics(
|
||||||
start_time=float(segment_start_time),
|
start_time=float(segment_start_time),
|
||||||
@@ -971,6 +1058,34 @@ def _integrate_scipy_stepwise(
|
|||||||
solver_start_count=segment_solver_starts,
|
solver_start_count=segment_solver_starts,
|
||||||
state_transition_count=segment_state_transitions,
|
state_transition_count=segment_state_transitions,
|
||||||
recoverable_retry_count=segment_recoverable_retries,
|
recoverable_retry_count=segment_recoverable_retries,
|
||||||
|
jacobian_evaluation_count=int(
|
||||||
|
jacobian_work["jacobianEvaluationCount"]
|
||||||
|
),
|
||||||
|
jacobian_full_build_count=int(
|
||||||
|
jacobian_work["fullBuildCount"]
|
||||||
|
),
|
||||||
|
jacobian_secant_reuse_count=int(
|
||||||
|
jacobian_work["secantReuseCount"]
|
||||||
|
),
|
||||||
|
jacobian_audit_failure_count=int(
|
||||||
|
jacobian_work["auditFailureCount"]
|
||||||
|
),
|
||||||
|
finite_difference_rhs_evaluation_count=int(
|
||||||
|
jacobian_work["finiteDifferenceRhsEvaluationCount"]
|
||||||
|
),
|
||||||
|
jacobian_base_rhs_evaluation_count=int(
|
||||||
|
jacobian_work["baseRhsEvaluationCount"]
|
||||||
|
),
|
||||||
|
jacobian_jv_audit_rhs_evaluation_count=int(
|
||||||
|
jacobian_work["jvAuditEvaluationCount"]
|
||||||
|
),
|
||||||
|
exact_column_build_count=int(
|
||||||
|
jacobian_work["exactColumnBuildCount"]
|
||||||
|
),
|
||||||
|
exact_column_fallback_count=int(
|
||||||
|
jacobian_work["exactColumnFallbackCount"]
|
||||||
|
),
|
||||||
|
jacobian_assembly_seconds=jacobian_work["assemblySeconds"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if status != "completed":
|
if status != "completed":
|
||||||
@@ -1034,6 +1149,7 @@ def integrate_ode(
|
|||||||
breakpoints: Sequence[float] | None = None,
|
breakpoints: Sequence[float] | None = None,
|
||||||
state_transition_handler: StateTransitionHandler | None = None,
|
state_transition_handler: StateTransitionHandler | None = None,
|
||||||
jac_sparsity=None,
|
jac_sparsity=None,
|
||||||
|
jac: JacobianCallable | None = None,
|
||||||
):
|
):
|
||||||
"""Integrate an ODE, optionally restarting at equation discontinuities.
|
"""Integrate an ODE, optionally restarting at equation discontinuities.
|
||||||
|
|
||||||
@@ -1104,10 +1220,26 @@ def integrate_ode(
|
|||||||
normalized_breakpoints,
|
normalized_breakpoints,
|
||||||
state_transition_handler,
|
state_transition_handler,
|
||||||
jac_sparsity,
|
jac_sparsity,
|
||||||
|
jac,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
implicit_jac = jac if config.method in {"BDF", "Radau"} else None
|
||||||
|
solve_rhs = rhs
|
||||||
|
if implicit_jac is not None:
|
||||||
|
observer = getattr(implicit_jac, "observe", None)
|
||||||
|
if observer is not None:
|
||||||
|
def observed_rhs(time, state):
|
||||||
|
derivative = rhs(time, state)
|
||||||
|
observer(float(time), state, derivative)
|
||||||
|
return derivative
|
||||||
|
|
||||||
|
solve_rhs = observed_rhs
|
||||||
|
start_segment = getattr(implicit_jac, "start_segment", None)
|
||||||
|
if start_segment is not None:
|
||||||
|
start_segment()
|
||||||
|
|
||||||
solve_options = {
|
solve_options = {
|
||||||
"fun": rhs,
|
"fun": solve_rhs,
|
||||||
"t_span": (config.t_start, config.t_stop),
|
"t_span": (config.t_start, config.t_stop),
|
||||||
"y0": initial_state,
|
"y0": initial_state,
|
||||||
"method": config.method,
|
"method": config.method,
|
||||||
@@ -1118,6 +1250,8 @@ def integrate_ode(
|
|||||||
}
|
}
|
||||||
if config.first_step is not None:
|
if config.first_step is not None:
|
||||||
solve_options["first_step"] = config.first_step
|
solve_options["first_step"] = config.first_step
|
||||||
if jac_sparsity is not None and config.method in {"BDF", "Radau"}:
|
if implicit_jac is not None:
|
||||||
|
solve_options["jac"] = implicit_jac
|
||||||
|
elif jac_sparsity is not None and config.method in {"BDF", "Radau"}:
|
||||||
solve_options["jac_sparsity"] = jac_sparsity
|
solve_options["jac_sparsity"] = jac_sparsity
|
||||||
return solve_ivp(**solve_options)
|
return solve_ivp(**solve_options)
|
||||||
@@ -0,0 +1,850 @@
|
|||||||
|
"""Proof-gated tangent columns for the three-piston reference network.
|
||||||
|
|
||||||
|
This module is deliberately narrower than the generic algebraic solver. It
|
||||||
|
only compiles a tangent provider after proving the state layout, component
|
||||||
|
types, physical connections, and causal execution plan used by the committed
|
||||||
|
three-piston XML. A failed proof leaves the ordinary seed-0 numerical
|
||||||
|
Jacobian in control; a runtime mode boundary requests the same one-build
|
||||||
|
fallback through :class:`ExactColumnsUnavailable`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from app.simulation.solvers.jacobian import ExactColumnsUnavailable
|
||||||
|
from app.simulation.solvers.mechanical import MechanicalConstraintGroup
|
||||||
|
from app.simulation.systems.network import Endpoint
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.simulation.systems.generic import GenericFluidSystem
|
||||||
|
|
||||||
|
|
||||||
|
_TARGET_BRANCH_NAMES = (
|
||||||
|
(
|
||||||
|
"mass_friction_endstops_10",
|
||||||
|
"pn_brp2_8",
|
||||||
|
"pn_c1_8",
|
||||||
|
"pneumatic_69",
|
||||||
|
"elasticendstop_8",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mass_friction_endstops_11",
|
||||||
|
"pn_brp2_9",
|
||||||
|
"pn_c1_9",
|
||||||
|
"pneumatic_68",
|
||||||
|
"elasticendstop_9",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mass_friction_endstops_12",
|
||||||
|
"pn_brp2_10",
|
||||||
|
"pn_c1_10",
|
||||||
|
"pneumatic_66",
|
||||||
|
"elasticendstop_10",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThreePistonBranch:
|
||||||
|
mass: object
|
||||||
|
piston: object
|
||||||
|
chamber: object
|
||||||
|
pipe: object
|
||||||
|
contact: object
|
||||||
|
velocity_index: int
|
||||||
|
position_index: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThreePistonTangentCompilation:
|
||||||
|
eligible: bool
|
||||||
|
reason: str | None
|
||||||
|
columns: tuple[int, ...] = ()
|
||||||
|
provider: "ThreePistonTangentProvider | None" = None
|
||||||
|
reached_assignment_count: int = 0
|
||||||
|
|
||||||
|
def diagnostics(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"eligible": self.eligible,
|
||||||
|
"fallbackReason": self.reason,
|
||||||
|
"columns": list(self.columns),
|
||||||
|
"columnCount": len(self.columns),
|
||||||
|
"reachedAssignmentCount": self.reached_assignment_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _PrimalContext:
|
||||||
|
time: float
|
||||||
|
state: np.ndarray
|
||||||
|
connected_h: dict[str, dict[str, float]]
|
||||||
|
|
||||||
|
|
||||||
|
def _failed(reason: str) -> ThreePistonTangentCompilation:
|
||||||
|
return ThreePistonTangentCompilation(False, reason)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreePistonTangentProvider:
|
||||||
|
"""Batched six-direction provider compiled for one system instance."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
system: "GenericFluidSystem",
|
||||||
|
branches: tuple[ThreePistonBranch, ...],
|
||||||
|
columns: tuple[int, ...],
|
||||||
|
state_offsets: Mapping[str, tuple[int, int]],
|
||||||
|
reached_assignment_count: int,
|
||||||
|
) -> None:
|
||||||
|
self.system = system
|
||||||
|
self.branches = branches
|
||||||
|
self.columns = columns
|
||||||
|
self.state_offsets = dict(state_offsets)
|
||||||
|
self.reached_assignment_count = reached_assignment_count
|
||||||
|
self._context: _PrimalContext | None = None
|
||||||
|
self._capture_requested = False
|
||||||
|
|
||||||
|
def request_primal_capture(self) -> None:
|
||||||
|
"""Capture exactly the next completed RHS primal closure."""
|
||||||
|
|
||||||
|
self._capture_requested = True
|
||||||
|
|
||||||
|
def cancel_primal_capture(self) -> None:
|
||||||
|
"""Discard a pending capture after an interrupted base RHS."""
|
||||||
|
|
||||||
|
self._capture_requested = False
|
||||||
|
|
||||||
|
def record_primal(
|
||||||
|
self,
|
||||||
|
time: float,
|
||||||
|
state: Sequence[float],
|
||||||
|
connected_h: Mapping[str, Mapping[str, float]],
|
||||||
|
) -> None:
|
||||||
|
if not self._capture_requested:
|
||||||
|
return
|
||||||
|
self._capture_requested = False
|
||||||
|
required_components = {
|
||||||
|
item.name
|
||||||
|
for branch in self.branches
|
||||||
|
for item in (branch.chamber, branch.pipe)
|
||||||
|
}
|
||||||
|
self._context = _PrimalContext(
|
||||||
|
time=float(time),
|
||||||
|
state=np.asarray(state, dtype=float).copy(),
|
||||||
|
connected_h={
|
||||||
|
name: {port: float(value) for port, value in values.items()}
|
||||||
|
for name, values in connected_h.items()
|
||||||
|
if name in required_components
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
time: float,
|
||||||
|
state: np.ndarray,
|
||||||
|
normalized_indexes: tuple[int, ...],
|
||||||
|
) -> np.ndarray:
|
||||||
|
if normalized_indexes != self.columns:
|
||||||
|
raise ValueError("Compiled tangent columns were requested out of order.")
|
||||||
|
context = self._context
|
||||||
|
values = np.asarray(state, dtype=float)
|
||||||
|
if (
|
||||||
|
context is None
|
||||||
|
or float(time) != context.time
|
||||||
|
or values.shape != context.state.shape
|
||||||
|
or not np.array_equal(values, context.state)
|
||||||
|
):
|
||||||
|
raise ExactColumnsUnavailable("stalePrimalContext")
|
||||||
|
return self._evaluate(context)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _zeros(width: int) -> tuple[float, ...]:
|
||||||
|
return (0.0,) * width
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_signal(vector: Sequence[float]) -> bool:
|
||||||
|
return any(float(value) != 0.0 for value in vector)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _require_valid(value: object, prefix: str) -> None:
|
||||||
|
if not bool(getattr(value, "valid", False)):
|
||||||
|
reason = getattr(value, "reason", None) or "invalid"
|
||||||
|
raise ExactColumnsUnavailable(f"{prefix}:{reason}")
|
||||||
|
|
||||||
|
def _evaluate(self, context: _PrimalContext) -> np.ndarray:
|
||||||
|
# The primal RHS may disable the causal fast path after a residual
|
||||||
|
# audit. Recheck after that closure and before replaying its compiled
|
||||||
|
# assignments so a dynamic downgrade uses the ordinary full FD build.
|
||||||
|
if not self.system.pressure_flow_solver.causal_fast_path_enabled:
|
||||||
|
raise ExactColumnsUnavailable("causalFastPathDisabled")
|
||||||
|
width = len(self.columns)
|
||||||
|
zero = self._zeros(width)
|
||||||
|
out = np.zeros((len(context.state), width), dtype=float)
|
||||||
|
seeds = {
|
||||||
|
column: tuple(float(index == seed_index) for index in range(width))
|
||||||
|
for seed_index, column in enumerate(self.columns)
|
||||||
|
}
|
||||||
|
solver = self.system.pressure_flow_solver
|
||||||
|
tangents: dict[str, tuple[float, ...]] = {}
|
||||||
|
|
||||||
|
def tangent(key: str) -> tuple[float, ...]:
|
||||||
|
return tangents.get(key, zero)
|
||||||
|
|
||||||
|
def negative_sum(
|
||||||
|
vectors: Sequence[Sequence[float]],
|
||||||
|
) -> tuple[float, ...]:
|
||||||
|
return tuple(
|
||||||
|
-sum(float(vector[index]) for vector in vectors)
|
||||||
|
for index in range(width)
|
||||||
|
)
|
||||||
|
|
||||||
|
for variable in ("x", "v"):
|
||||||
|
plan = solver._causal_effort_plan_by_variable.get(variable)
|
||||||
|
if plan is None:
|
||||||
|
raise ExactColumnsUnavailable("causalEffortPlanUnavailable")
|
||||||
|
for assignment in plan:
|
||||||
|
matched = [
|
||||||
|
branch
|
||||||
|
for branch in self.branches
|
||||||
|
if any(
|
||||||
|
unknown.component == branch.mass.name
|
||||||
|
for unknown in assignment.members
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if len(matched) > 1:
|
||||||
|
raise ExactColumnsUnavailable("coupledTargetMechanicalSeeds")
|
||||||
|
vector = zero
|
||||||
|
if matched:
|
||||||
|
branch = matched[0]
|
||||||
|
column = (
|
||||||
|
branch.position_index
|
||||||
|
if variable == "x"
|
||||||
|
else branch.velocity_index
|
||||||
|
)
|
||||||
|
vector = seeds[column]
|
||||||
|
for member in assignment.members:
|
||||||
|
tangents[member.id] = vector
|
||||||
|
|
||||||
|
geometry: dict[str, object] = {}
|
||||||
|
chamber_properties: dict[str, object] = {}
|
||||||
|
for branch in self.branches:
|
||||||
|
piston = branch.piston
|
||||||
|
linearization = piston.linearize_geometry_and_force(
|
||||||
|
tangent(f"{piston.name}.port_4.x"),
|
||||||
|
tangent(f"{piston.name}.port_5.x"),
|
||||||
|
tangent(f"{piston.name}.port_4.v"),
|
||||||
|
tangent(f"{piston.name}.port_5.v"),
|
||||||
|
zero,
|
||||||
|
)
|
||||||
|
self._require_valid(linearization, "pistonGeometry")
|
||||||
|
chamber = branch.chamber
|
||||||
|
volume = float(chamber.total_volume())
|
||||||
|
if volume <= float(chamber.cvol0) / 100.0:
|
||||||
|
raise ExactColumnsUnavailable("volumeFloor")
|
||||||
|
primal = chamber.medium.properties_from_mU(
|
||||||
|
chamber.state.m,
|
||||||
|
chamber.state.U,
|
||||||
|
volume,
|
||||||
|
)
|
||||||
|
properties = chamber.medium.linearize_properties_from_mU(
|
||||||
|
chamber.state.m,
|
||||||
|
chamber.state.U,
|
||||||
|
volume,
|
||||||
|
zero,
|
||||||
|
zero,
|
||||||
|
linearization.volume_tangent,
|
||||||
|
properties=primal,
|
||||||
|
)
|
||||||
|
self._require_valid(properties, "chamberProperties")
|
||||||
|
geometry[piston.name] = linearization
|
||||||
|
chamber_properties[chamber.name] = properties
|
||||||
|
|
||||||
|
pressure_plan = solver._causal_effort_plan_by_variable.get("p")
|
||||||
|
if pressure_plan is None:
|
||||||
|
raise ExactColumnsUnavailable("causalPressurePlanUnavailable")
|
||||||
|
for assignment in pressure_plan:
|
||||||
|
matched = [
|
||||||
|
branch
|
||||||
|
for branch in self.branches
|
||||||
|
if any(
|
||||||
|
unknown.component == branch.chamber.name
|
||||||
|
for unknown in assignment.members
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if len(matched) > 1:
|
||||||
|
raise ExactColumnsUnavailable("coupledTargetPressureSeeds")
|
||||||
|
vector = (
|
||||||
|
chamber_properties[matched[0].chamber.name].tangents.p
|
||||||
|
if matched
|
||||||
|
else zero
|
||||||
|
)
|
||||||
|
for member in assignment.members:
|
||||||
|
tangents[member.id] = tuple(vector)
|
||||||
|
|
||||||
|
contacts: dict[str, object] = {}
|
||||||
|
for branch in self.branches:
|
||||||
|
piston = branch.piston
|
||||||
|
linearization = piston.linearize_geometry_and_force(
|
||||||
|
tangent(f"{piston.name}.port_4.x"),
|
||||||
|
tangent(f"{piston.name}.port_5.x"),
|
||||||
|
tangent(f"{piston.name}.port_4.v"),
|
||||||
|
tangent(f"{piston.name}.port_5.v"),
|
||||||
|
tangent(f"{piston.name}.port_1.p"),
|
||||||
|
)
|
||||||
|
self._require_valid(linearization, "pistonPressureForce")
|
||||||
|
geometry[piston.name] = linearization
|
||||||
|
contact = branch.contact
|
||||||
|
contact_linearization = contact.linearize_contact_force(
|
||||||
|
tangent(f"{contact.name}.port_1.x"),
|
||||||
|
tangent(f"{contact.name}.port_2.x"),
|
||||||
|
tangent(f"{contact.name}.port_1.v"),
|
||||||
|
tangent(f"{contact.name}.port_2.v"),
|
||||||
|
)
|
||||||
|
self._require_valid(contact_linearization, "contactMode")
|
||||||
|
contacts[contact.name] = contact_linearization
|
||||||
|
|
||||||
|
equations = {
|
||||||
|
equation.id: equation for equation in solver.equation_templates
|
||||||
|
}
|
||||||
|
branch_component = {
|
||||||
|
item.name: branch
|
||||||
|
for branch in self.branches
|
||||||
|
for item in (
|
||||||
|
branch.mass,
|
||||||
|
branch.piston,
|
||||||
|
branch.chamber,
|
||||||
|
branch.pipe,
|
||||||
|
branch.contact,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pipe_flows: dict[str, object] = {}
|
||||||
|
for stage in solver._explicit_flow_plan:
|
||||||
|
pending: list[tuple[str, tuple[float, ...]]] = []
|
||||||
|
for assignment in stage.assignments:
|
||||||
|
equation = equations.get(assignment.equation_id)
|
||||||
|
if equation is None:
|
||||||
|
raise ExactColumnsUnavailable("causalEquationMissing")
|
||||||
|
dependencies = [
|
||||||
|
tangent(variable)
|
||||||
|
for variable in equation.variables
|
||||||
|
if variable != assignment.unknown.id
|
||||||
|
]
|
||||||
|
if not any(
|
||||||
|
self._has_signal(vector) for vector in dependencies
|
||||||
|
):
|
||||||
|
pending.append((assignment.unknown.id, zero))
|
||||||
|
continue
|
||||||
|
if equation.relation == "sumToZero":
|
||||||
|
vectors = [
|
||||||
|
tangent(variable)
|
||||||
|
for variable in equation.variables
|
||||||
|
if variable != assignment.unknown.id
|
||||||
|
and variable in solver._unknowns_by_id
|
||||||
|
and solver._unknowns_by_id[variable].role == "flow"
|
||||||
|
]
|
||||||
|
pending.append(
|
||||||
|
(assignment.unknown.id, negative_sum(vectors))
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
component = assignment.component
|
||||||
|
model = getattr(component, "MODEL_TYPE", None)
|
||||||
|
if model == "amesim_pnl0001":
|
||||||
|
branch = branch_component.get(component.name)
|
||||||
|
if branch is None or component is not branch.pipe:
|
||||||
|
raise ExactColumnsUnavailable("unexpectedPipeReach")
|
||||||
|
flow_linearization = pipe_flows.get(component.name)
|
||||||
|
if flow_linearization is None:
|
||||||
|
properties = component.medium.properties_from_mU(
|
||||||
|
component.state.m,
|
||||||
|
component.state.U,
|
||||||
|
component.volume,
|
||||||
|
)
|
||||||
|
flow_linearization = component.linearize_mass_flow(
|
||||||
|
component.port_1.p,
|
||||||
|
component.port_2.p,
|
||||||
|
properties.T,
|
||||||
|
)
|
||||||
|
self._require_valid(
|
||||||
|
flow_linearization,
|
||||||
|
"pipeMassFlow",
|
||||||
|
)
|
||||||
|
pipe_flows[component.name] = flow_linearization
|
||||||
|
vector = tuple(
|
||||||
|
flow_linearization.partial_p_1 * first
|
||||||
|
+ flow_linearization.partial_p_2 * second
|
||||||
|
for first, second in zip(
|
||||||
|
tangent(f"{component.name}.port_1.p"),
|
||||||
|
tangent(f"{component.name}.port_2.p"),
|
||||||
|
strict=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif model == "amesim_lstp00a":
|
||||||
|
contact_linearization = contacts.get(component.name)
|
||||||
|
if contact_linearization is None:
|
||||||
|
raise ExactColumnsUnavailable(
|
||||||
|
f"unexpectedContactReach:{component.name}"
|
||||||
|
)
|
||||||
|
sign = (
|
||||||
|
1.0
|
||||||
|
if assignment.unknown.port == "port_1"
|
||||||
|
else -1.0
|
||||||
|
)
|
||||||
|
vector = tuple(
|
||||||
|
sign * value
|
||||||
|
for value in contact_linearization.force_tangent
|
||||||
|
)
|
||||||
|
elif model == "amesim_pnrp17":
|
||||||
|
piston_geometry = geometry.get(component.name)
|
||||||
|
if piston_geometry is None:
|
||||||
|
raise ExactColumnsUnavailable(
|
||||||
|
f"unexpectedPistonReach:{component.name}"
|
||||||
|
)
|
||||||
|
other = next(
|
||||||
|
(
|
||||||
|
tangent(variable)
|
||||||
|
for variable in equation.variables
|
||||||
|
if variable != assignment.unknown.id
|
||||||
|
and variable.endswith(".f")
|
||||||
|
),
|
||||||
|
zero,
|
||||||
|
)
|
||||||
|
sign = (
|
||||||
|
-1.0
|
||||||
|
if equation.id.endswith(
|
||||||
|
"piston_side_force_balance"
|
||||||
|
)
|
||||||
|
else 1.0
|
||||||
|
)
|
||||||
|
vector = tuple(
|
||||||
|
-force + sign * pressure
|
||||||
|
for force, pressure in zip(
|
||||||
|
other,
|
||||||
|
piston_geometry.pressure_force_tangent,
|
||||||
|
strict=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ExactColumnsUnavailable(
|
||||||
|
f"unsupportedReach:{model or 'connection'}"
|
||||||
|
)
|
||||||
|
pending.append((assignment.unknown.id, tuple(vector)))
|
||||||
|
for key, vector in pending:
|
||||||
|
tangents[key] = vector
|
||||||
|
|
||||||
|
outflow: dict[Endpoint, tuple[float, ...]] = {}
|
||||||
|
for branch in self.branches:
|
||||||
|
enthalpy_tangent = tuple(
|
||||||
|
chamber_properties[branch.chamber.name].tangents.h
|
||||||
|
)
|
||||||
|
for port_name in branch.chamber.ports:
|
||||||
|
outflow[Endpoint(branch.chamber.name, port_name)] = (
|
||||||
|
enthalpy_tangent
|
||||||
|
)
|
||||||
|
# PNRP17 mirrors the connected chamber enthalpy on its sole
|
||||||
|
# pneumatic port at the already closed primal point.
|
||||||
|
outflow[Endpoint(branch.piston.name, "port_1")] = enthalpy_tangent
|
||||||
|
|
||||||
|
connected_tangent: dict[
|
||||||
|
str,
|
||||||
|
dict[str, tuple[float, ...]],
|
||||||
|
] = {name: {} for name in self.system.network.components}
|
||||||
|
for connection in self.system.network.connections:
|
||||||
|
if connection.kind != "physical":
|
||||||
|
continue
|
||||||
|
first, second = connection.endpoints
|
||||||
|
connected_tangent[first.component][first.port] = outflow.get(
|
||||||
|
second,
|
||||||
|
zero,
|
||||||
|
)
|
||||||
|
connected_tangent[second.component][second.port] = outflow.get(
|
||||||
|
first,
|
||||||
|
zero,
|
||||||
|
)
|
||||||
|
|
||||||
|
for component_name, port_values in connected_tangent.items():
|
||||||
|
component = self.system.network.components[component_name]
|
||||||
|
if not getattr(component, "PRESSURE_FLOW_DEPENDS_ON_STREAM", False):
|
||||||
|
continue
|
||||||
|
if any(
|
||||||
|
self._has_signal(vector) for vector in port_values.values()
|
||||||
|
):
|
||||||
|
raise ExactColumnsUnavailable(
|
||||||
|
f"streamSensitiveReach:{component_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for branch in self.branches:
|
||||||
|
chamber = branch.chamber
|
||||||
|
chamber_linearization = chamber.linearize_state_derivative(
|
||||||
|
context.connected_h[chamber.name],
|
||||||
|
state_mass_tangent=zero,
|
||||||
|
state_energy_tangent=zero,
|
||||||
|
external_volume_tangent=(
|
||||||
|
geometry[branch.piston.name].volume_tangent
|
||||||
|
),
|
||||||
|
external_volume_rate_tangent=(
|
||||||
|
geometry[branch.piston.name].volume_flow_tangent
|
||||||
|
),
|
||||||
|
port_mass_flow_tangents={
|
||||||
|
name: tangent(f"{chamber.name}.{name}.m_flow")
|
||||||
|
for name in chamber.ports
|
||||||
|
},
|
||||||
|
connected_h_tangents=connected_tangent[chamber.name],
|
||||||
|
property_linearization=chamber_properties[chamber.name],
|
||||||
|
)
|
||||||
|
self._require_valid(chamber_linearization, "chamberDerivative")
|
||||||
|
offset, size = self.state_offsets[chamber.name]
|
||||||
|
if size != 2:
|
||||||
|
raise ValueError("Target chamber state layout changed.")
|
||||||
|
out[offset, :], out[offset + 1, :] = (
|
||||||
|
chamber_linearization.tangents
|
||||||
|
)
|
||||||
|
|
||||||
|
pipe = branch.pipe
|
||||||
|
pipe_properties = pipe.medium.linearize_properties_from_mU(
|
||||||
|
pipe.state.m,
|
||||||
|
pipe.state.U,
|
||||||
|
pipe.volume,
|
||||||
|
zero,
|
||||||
|
zero,
|
||||||
|
zero,
|
||||||
|
)
|
||||||
|
self._require_valid(pipe_properties, "pipeProperties")
|
||||||
|
pipe_linearization = pipe.linearize_state_derivative(
|
||||||
|
context.connected_h[pipe.name],
|
||||||
|
state_mass_tangent=zero,
|
||||||
|
state_energy_tangent=zero,
|
||||||
|
port_mass_flow_tangents={
|
||||||
|
name: tangent(f"{pipe.name}.{name}.m_flow")
|
||||||
|
for name in pipe.ports
|
||||||
|
},
|
||||||
|
connected_h_tangents=connected_tangent[pipe.name],
|
||||||
|
property_linearization=pipe_properties,
|
||||||
|
)
|
||||||
|
self._require_valid(pipe_linearization, "pipeDerivative")
|
||||||
|
offset, size = self.state_offsets[pipe.name]
|
||||||
|
if size != 2:
|
||||||
|
raise ValueError("Target pipe state layout changed.")
|
||||||
|
out[offset, :], out[offset + 1, :] = pipe_linearization.tangents
|
||||||
|
|
||||||
|
target_pneumatic = {
|
||||||
|
item.name
|
||||||
|
for branch in self.branches
|
||||||
|
for item in (branch.chamber, branch.pipe)
|
||||||
|
}
|
||||||
|
for entry in self.system.mechanical_state_reducer.state_entries:
|
||||||
|
if (
|
||||||
|
isinstance(entry, MechanicalConstraintGroup)
|
||||||
|
or entry.name in target_pneumatic
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
for port_name, port in entry.ports.items():
|
||||||
|
definition = port.definition
|
||||||
|
if (
|
||||||
|
definition is not None
|
||||||
|
and definition.kind == "physical"
|
||||||
|
and definition.domain == "pneumatic"
|
||||||
|
and self._has_signal(
|
||||||
|
tangent(f"{entry.name}.{port_name}.m_flow")
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ExactColumnsUnavailable(
|
||||||
|
f"unsupportedDynamicReach:{entry.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
mass_seeds = {
|
||||||
|
branch.mass.name: (
|
||||||
|
seeds[branch.velocity_index],
|
||||||
|
seeds[branch.position_index],
|
||||||
|
)
|
||||||
|
for branch in self.branches
|
||||||
|
}
|
||||||
|
for group in self.system.mechanical_state_reducer.groups:
|
||||||
|
if len(group.components) != 1:
|
||||||
|
raise ExactColumnsUnavailable("reachableRigidMassGroup")
|
||||||
|
mass = group.representative
|
||||||
|
force_1 = tangent(f"{mass.name}.port_1.f")
|
||||||
|
force_2 = tangent(f"{mass.name}.port_2.f")
|
||||||
|
velocity, position = mass_seeds.get(mass.name, (zero, zero))
|
||||||
|
if not any(
|
||||||
|
self._has_signal(vector)
|
||||||
|
for vector in (force_1, force_2, velocity, position)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
fixed = (
|
||||||
|
mass._constraint_acceleration == 0.0
|
||||||
|
and mass._constraint_velocity == 0.0
|
||||||
|
)
|
||||||
|
if fixed and (
|
||||||
|
abs(group.total_unconstrained_force())
|
||||||
|
<= 1.0e-12
|
||||||
|
* max(
|
||||||
|
abs(mass.port_1.f),
|
||||||
|
abs(mass.port_2.f),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ExactColumnsUnavailable("mechanicalReleaseBoundary")
|
||||||
|
mass_linearization = mass.linearize_state_derivative(
|
||||||
|
force_1,
|
||||||
|
force_2,
|
||||||
|
velocity,
|
||||||
|
position,
|
||||||
|
constraint_mode="current" if fixed else "free",
|
||||||
|
)
|
||||||
|
self._require_valid(
|
||||||
|
mass_linearization,
|
||||||
|
"mechanicalDerivative",
|
||||||
|
)
|
||||||
|
offset, size = self.state_offsets[mass.name]
|
||||||
|
if size != 2:
|
||||||
|
raise ValueError("Mechanical state layout changed.")
|
||||||
|
out[offset, :], out[offset + 1, :] = mass_linearization.tangents
|
||||||
|
|
||||||
|
if not np.all(np.isfinite(out)):
|
||||||
|
raise ExactColumnsUnavailable("nonFiniteTangentColumns")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def compile_three_piston_tangent_provider(
|
||||||
|
system: "GenericFluidSystem",
|
||||||
|
) -> ThreePistonTangentCompilation:
|
||||||
|
"""Compile the proof-gated target provider, or return a stable reason."""
|
||||||
|
|
||||||
|
solver = system.pressure_flow_solver
|
||||||
|
if not solver.causal_fast_path_eligible:
|
||||||
|
return _failed("causalFastPathIneligible")
|
||||||
|
if not solver.causal_fast_path_enabled:
|
||||||
|
return _failed("causalFastPathDisabled")
|
||||||
|
closure = system._thermofluid_closure_plan
|
||||||
|
if closure.uses_conservative_global_solver:
|
||||||
|
return _failed("conservativeThermofluidClosure")
|
||||||
|
if system.pneumatic_storage_reducer.groups:
|
||||||
|
return _failed("coupledPneumaticStorage")
|
||||||
|
|
||||||
|
state_offsets: dict[str, tuple[int, int]] = {}
|
||||||
|
cursor = 0
|
||||||
|
group_by_name: dict[str, MechanicalConstraintGroup] = {}
|
||||||
|
for entry in system.mechanical_state_reducer.state_entries:
|
||||||
|
if isinstance(entry, MechanicalConstraintGroup):
|
||||||
|
if len(entry.components) != 1:
|
||||||
|
cursor += 2
|
||||||
|
continue
|
||||||
|
component = entry.representative
|
||||||
|
state_offsets[component.name] = (cursor, 2)
|
||||||
|
group_by_name[component.name] = entry
|
||||||
|
cursor += 2
|
||||||
|
else:
|
||||||
|
state_offsets[entry.name] = (cursor, int(entry.state_size))
|
||||||
|
cursor += int(entry.state_size)
|
||||||
|
|
||||||
|
expected_types = (
|
||||||
|
"amesim_mecmas21",
|
||||||
|
"amesim_pnrp17",
|
||||||
|
"amesim_pnch012",
|
||||||
|
"amesim_pnl0001",
|
||||||
|
"amesim_lstp00a",
|
||||||
|
)
|
||||||
|
branches: list[ThreePistonBranch] = []
|
||||||
|
for names in _TARGET_BRANCH_NAMES:
|
||||||
|
try:
|
||||||
|
components = tuple(system.network.components[name] for name in names)
|
||||||
|
except KeyError:
|
||||||
|
return _failed("targetComponentMissing")
|
||||||
|
if tuple(getattr(item, "MODEL_TYPE", None) for item in components) != expected_types:
|
||||||
|
return _failed("targetComponentTypeMismatch")
|
||||||
|
mass, piston, chamber, pipe, contact = components
|
||||||
|
if mass.name not in group_by_name or mass.name not in state_offsets:
|
||||||
|
return _failed("targetMechanicalStateLayout")
|
||||||
|
offset, size = state_offsets[mass.name]
|
||||||
|
if size != 2:
|
||||||
|
return _failed("targetMechanicalStateLayout")
|
||||||
|
branches.append(
|
||||||
|
ThreePistonBranch(
|
||||||
|
mass=mass,
|
||||||
|
piston=piston,
|
||||||
|
chamber=chamber,
|
||||||
|
pipe=pipe,
|
||||||
|
contact=contact,
|
||||||
|
velocity_index=offset,
|
||||||
|
position_index=offset + 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
required_pairs: set[frozenset[Endpoint]] = set()
|
||||||
|
for branch in branches:
|
||||||
|
required_pairs.update(
|
||||||
|
{
|
||||||
|
frozenset((Endpoint(branch.mass.name, "port_1"), Endpoint(branch.piston.name, "port_2"))),
|
||||||
|
frozenset((Endpoint(branch.piston.name, "port_1"), Endpoint(branch.chamber.name, "port_3"))),
|
||||||
|
frozenset((Endpoint(branch.chamber.name, "port_1"), Endpoint(branch.pipe.name, "port_1"))),
|
||||||
|
frozenset((Endpoint(branch.piston.name, "port_5"), Endpoint(branch.contact.name, "port_1"))),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
actual_pairs = {
|
||||||
|
frozenset(connection.endpoints)
|
||||||
|
for connection in system.network.connections
|
||||||
|
if connection.kind == "physical"
|
||||||
|
}
|
||||||
|
if not required_pairs <= actual_pairs:
|
||||||
|
return _failed("targetTopologyMismatch")
|
||||||
|
|
||||||
|
required_methods = (
|
||||||
|
("piston", "linearize_geometry_and_force"),
|
||||||
|
("chamber", "linearize_state_derivative"),
|
||||||
|
("pipe", "linearize_mass_flow"),
|
||||||
|
("pipe", "linearize_state_derivative"),
|
||||||
|
("contact", "linearize_contact_force"),
|
||||||
|
("mass", "linearize_state_derivative"),
|
||||||
|
)
|
||||||
|
for branch in branches:
|
||||||
|
for owner, method in required_methods:
|
||||||
|
if not callable(getattr(getattr(branch, owner), method, None)):
|
||||||
|
return _failed(f"missingTangentPrimitive:{owner}.{method}")
|
||||||
|
if not callable(getattr(branch.chamber.medium, "linearize_properties_from_mU", None)):
|
||||||
|
return _failed("missingTangentPrimitive:medium.linearize_properties_from_mU")
|
||||||
|
|
||||||
|
if any(
|
||||||
|
len(group.components) != 1
|
||||||
|
for group in system.mechanical_state_reducer.groups
|
||||||
|
):
|
||||||
|
return _failed("rigidMassAggregation")
|
||||||
|
|
||||||
|
target_mass_names = {branch.mass.name for branch in branches}
|
||||||
|
target_chamber_names = {branch.chamber.name for branch in branches}
|
||||||
|
target_pipe_names = {branch.pipe.name for branch in branches}
|
||||||
|
target_piston_names = {branch.piston.name for branch in branches}
|
||||||
|
target_contact_names = {branch.contact.name for branch in branches}
|
||||||
|
reached_ids: set[str] = set()
|
||||||
|
for variable in ("x", "v"):
|
||||||
|
for assignment in solver._causal_effort_plan_by_variable.get(
|
||||||
|
variable,
|
||||||
|
(),
|
||||||
|
):
|
||||||
|
if any(
|
||||||
|
member.component in target_mass_names
|
||||||
|
for member in assignment.members
|
||||||
|
):
|
||||||
|
reached_ids.update(member.id for member in assignment.members)
|
||||||
|
for assignment in solver._causal_effort_plan_by_variable.get("p", ()):
|
||||||
|
if any(
|
||||||
|
member.component in target_chamber_names
|
||||||
|
for member in assignment.members
|
||||||
|
):
|
||||||
|
reached_ids.update(member.id for member in assignment.members)
|
||||||
|
|
||||||
|
equations = {
|
||||||
|
item.id: item for item in solver.equation_templates
|
||||||
|
}
|
||||||
|
reached_assignments = []
|
||||||
|
allowed_reached_models = {
|
||||||
|
"amesim_pnl0001",
|
||||||
|
"amesim_pnrp17",
|
||||||
|
"amesim_lstp00a",
|
||||||
|
}
|
||||||
|
for stage in solver._explicit_flow_plan:
|
||||||
|
stage_reached = []
|
||||||
|
for assignment in stage.assignments:
|
||||||
|
equation = equations.get(assignment.equation_id)
|
||||||
|
if equation is None:
|
||||||
|
return _failed("causalEquationMissing")
|
||||||
|
dependencies = set(equation.variables) - {
|
||||||
|
assignment.unknown.id
|
||||||
|
}
|
||||||
|
if not dependencies.intersection(reached_ids):
|
||||||
|
continue
|
||||||
|
component = assignment.component
|
||||||
|
if equation.relation != "sumToZero":
|
||||||
|
model_type = getattr(component, "MODEL_TYPE", None)
|
||||||
|
if model_type not in allowed_reached_models:
|
||||||
|
return _failed(f"unsupportedReach:{model_type}")
|
||||||
|
expected_names = {
|
||||||
|
"amesim_pnl0001": target_pipe_names,
|
||||||
|
"amesim_pnrp17": target_piston_names,
|
||||||
|
"amesim_lstp00a": target_contact_names,
|
||||||
|
}[model_type]
|
||||||
|
if component.name not in expected_names:
|
||||||
|
return _failed(
|
||||||
|
f"unexpectedReach:{model_type}:{component.name}"
|
||||||
|
)
|
||||||
|
if bool(
|
||||||
|
getattr(
|
||||||
|
component,
|
||||||
|
"PRESSURE_FLOW_DEPENDS_ON_STREAM",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return _failed(f"streamSensitiveReach:{component.name}")
|
||||||
|
stage_reached.append(assignment)
|
||||||
|
reached_assignments.extend(stage_reached)
|
||||||
|
reached_ids.update(item.unknown.id for item in stage_reached)
|
||||||
|
|
||||||
|
# The only non-zero h_outflow seeds are the target PNCH ports. Each
|
||||||
|
# direct neighbour must consume it in a supported target balance, mirror
|
||||||
|
# it through the one-port piston, or terminate at a fixed PNPL01 cap.
|
||||||
|
# Secondary pressure blocks may contain the same causal flow coordinates,
|
||||||
|
# so membership alone is not evidence of a stream derivative. The direct
|
||||||
|
# enthalpy reach proof below, plus the runtime dynamic-owner gate, is the
|
||||||
|
# relevant condition for this target-specific program.
|
||||||
|
neighbor_by_endpoint: dict[Endpoint, Endpoint] = {}
|
||||||
|
for connection in system.network.connections:
|
||||||
|
if connection.kind != "physical":
|
||||||
|
continue
|
||||||
|
first, second = connection.endpoints
|
||||||
|
neighbor_by_endpoint[first] = second
|
||||||
|
neighbor_by_endpoint[second] = first
|
||||||
|
permitted_h_neighbors = {
|
||||||
|
"amesim_pnl0001",
|
||||||
|
"amesim_pnrp17",
|
||||||
|
"amesim_pnpl01",
|
||||||
|
}
|
||||||
|
for branch in branches:
|
||||||
|
for port_name in branch.chamber.ports:
|
||||||
|
neighbor = neighbor_by_endpoint.get(
|
||||||
|
Endpoint(branch.chamber.name, port_name)
|
||||||
|
)
|
||||||
|
if neighbor is None:
|
||||||
|
return _failed("targetStreamBindingMissing")
|
||||||
|
component = system.network.components[neighbor.component]
|
||||||
|
if (
|
||||||
|
getattr(component, "MODEL_TYPE", None)
|
||||||
|
not in permitted_h_neighbors
|
||||||
|
):
|
||||||
|
return _failed(
|
||||||
|
f"unsupportedStreamReach:{component.name}"
|
||||||
|
)
|
||||||
|
if bool(
|
||||||
|
getattr(
|
||||||
|
component,
|
||||||
|
"PRESSURE_FLOW_DEPENDS_ON_STREAM",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return _failed(f"streamSensitiveReach:{component.name}")
|
||||||
|
|
||||||
|
columns = tuple(
|
||||||
|
sorted(
|
||||||
|
index
|
||||||
|
for branch in branches
|
||||||
|
for index in (branch.velocity_index, branch.position_index)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
provider = ThreePistonTangentProvider(
|
||||||
|
system,
|
||||||
|
tuple(branches),
|
||||||
|
columns,
|
||||||
|
state_offsets,
|
||||||
|
reached_assignment_count=len(reached_assignments),
|
||||||
|
)
|
||||||
|
return ThreePistonTangentCompilation(
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
columns,
|
||||||
|
provider,
|
||||||
|
reached_assignment_count=len(reached_assignments),
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from math import floor, isfinite
|
from math import floor, isfinite
|
||||||
|
import os
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from app.simulation.core.base import Component, DynamicComponent
|
from app.simulation.core.base import Component, DynamicComponent
|
||||||
@@ -12,6 +13,10 @@ from app.simulation.performance import performance_span, profile_phase
|
|||||||
from app.simulation.property_cache import with_property_cache
|
from app.simulation.property_cache import with_property_cache
|
||||||
from app.simulation.solvers.algebraic import PressureFlowSolver
|
from app.simulation.solvers.algebraic import PressureFlowSolver
|
||||||
from app.simulation.solvers.algebraic_blocks import StreamPressureBlockSolver
|
from app.simulation.solvers.algebraic_blocks import StreamPressureBlockSolver
|
||||||
|
from app.simulation.solvers.jacobian import (
|
||||||
|
SparseJacobianCompatibilityError,
|
||||||
|
SparseSecantJacobian,
|
||||||
|
)
|
||||||
from app.simulation.solvers.mechanical import (
|
from app.simulation.solvers.mechanical import (
|
||||||
MechanicalConstraintGroup,
|
MechanicalConstraintGroup,
|
||||||
MechanicalStateReducer,
|
MechanicalStateReducer,
|
||||||
@@ -21,15 +26,58 @@ from app.simulation.solvers.pneumatic_storage import (
|
|||||||
ideal_storage_group_is_reducible,
|
ideal_storage_group_is_reducible,
|
||||||
)
|
)
|
||||||
from app.simulation.solvers.pneumatic_volume import PneumaticVolumeResolver
|
from app.simulation.solvers.pneumatic_volume import PneumaticVolumeResolver
|
||||||
from app.simulation.solvers.solver import ODESolution, SolveIVPConfig, integrate_ode
|
from app.simulation.solvers.solver import (
|
||||||
|
IntegrationCancelled,
|
||||||
|
ODESolution,
|
||||||
|
SolveIVPConfig,
|
||||||
|
integrate_ode,
|
||||||
|
)
|
||||||
from app.simulation.solvers.signal import SignalResolver
|
from app.simulation.solvers.signal import SignalResolver
|
||||||
from app.simulation.solvers.stream import StreamResolver
|
from app.simulation.solvers.stream import StreamResolver
|
||||||
|
from app.simulation.solvers.tangent import (
|
||||||
|
ThreePistonTangentCompilation,
|
||||||
|
ThreePistonTangentProvider,
|
||||||
|
compile_three_piston_tangent_provider,
|
||||||
|
)
|
||||||
from app.simulation.systems.network import Endpoint, SimulationNetwork
|
from app.simulation.systems.network import Endpoint, SimulationNetwork
|
||||||
|
|
||||||
|
|
||||||
SimulationProgressCallback = Callable[[float, str], None]
|
SimulationProgressCallback = Callable[[float, str], None]
|
||||||
SimulationCancellationCheck = Callable[[], bool]
|
SimulationCancellationCheck = Callable[[], bool]
|
||||||
SimulationRunStatus = Literal["completed", "cancelled", "failed"]
|
SimulationRunStatus = Literal["completed", "cancelled", "failed"]
|
||||||
|
ODE_JACOBIAN_MODE_ENVIRONMENT_VARIABLE = "SIMULATION_ODE_JACOBIAN_MODE"
|
||||||
|
|
||||||
|
|
||||||
|
def _requested_ode_jacobian_mode() -> Literal[
|
||||||
|
"optimized",
|
||||||
|
"hybrid",
|
||||||
|
"semi-analytic",
|
||||||
|
"scipy",
|
||||||
|
]:
|
||||||
|
value = os.getenv(
|
||||||
|
ODE_JACOBIAN_MODE_ENVIRONMENT_VARIABLE,
|
||||||
|
"scipy",
|
||||||
|
).strip().lower()
|
||||||
|
if value in {"optimized", "colored"}:
|
||||||
|
return "optimized"
|
||||||
|
if value in {"hybrid", "secant"}:
|
||||||
|
return "hybrid"
|
||||||
|
if value in {"semi-analytic", "semi_analytic", "analytic"}:
|
||||||
|
return "semi-analytic"
|
||||||
|
if value in {
|
||||||
|
"scipy",
|
||||||
|
"native",
|
||||||
|
"finite-difference",
|
||||||
|
"0",
|
||||||
|
"false",
|
||||||
|
"no",
|
||||||
|
"off",
|
||||||
|
}:
|
||||||
|
return "scipy"
|
||||||
|
raise ValueError(
|
||||||
|
f"{ODE_JACOBIAN_MODE_ENVIRONMENT_VARIABLE} must be "
|
||||||
|
"'optimized', 'hybrid', 'semi-analytic', or 'scipy'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -398,6 +446,7 @@ class GenericFluidSystem:
|
|||||||
self.signal_propagation_count = 0
|
self.signal_propagation_count = 0
|
||||||
self.pneumatic_volume_propagation_count = 0
|
self.pneumatic_volume_propagation_count = 0
|
||||||
self._jacobian_sparsity = None
|
self._jacobian_sparsity = None
|
||||||
|
self._ode_tangent_provider: ThreePistonTangentProvider | None = None
|
||||||
|
|
||||||
def _request_causal_residual_audit(self) -> None:
|
def _request_causal_residual_audit(self) -> None:
|
||||||
"""Make topology or mode boundaries verify the next causal closure."""
|
"""Make topology or mode boundaries verify the next causal closure."""
|
||||||
@@ -874,6 +923,23 @@ class GenericFluidSystem:
|
|||||||
"colorGroupCount": group_count,
|
"colorGroupCount": group_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _exact_ode_jacobian_rows(self) -> dict[int, dict[int, float]]:
|
||||||
|
"""Return mode-independent kinematic rows safe to evaluate exactly."""
|
||||||
|
|
||||||
|
rows: dict[int, dict[int, float]] = {}
|
||||||
|
cursor = 0
|
||||||
|
for entry in self.mechanical_state_reducer.state_entries:
|
||||||
|
if isinstance(entry, MechanicalConstraintGroup):
|
||||||
|
# A discrete endstop can replace x' = v with x' = 0 for the
|
||||||
|
# active constrained mode. Keep those rows numerical; free
|
||||||
|
# mechanical groups always have d(x')/d(v) = 1.
|
||||||
|
if not entry.discrete_endstop_components:
|
||||||
|
rows[cursor + 1] = {cursor: 1.0}
|
||||||
|
cursor += 2
|
||||||
|
else:
|
||||||
|
cursor += entry.state_size
|
||||||
|
return rows
|
||||||
|
|
||||||
@profile_phase(
|
@profile_phase(
|
||||||
"simulation.closure",
|
"simulation.closure",
|
||||||
minimum_mode="audit",
|
minimum_mode="audit",
|
||||||
@@ -1065,7 +1131,11 @@ class GenericFluidSystem:
|
|||||||
def rhs(self, _time: float, state_vector: list[float]) -> list[float]:
|
def rhs(self, _time: float, state_vector: list[float]) -> list[float]:
|
||||||
self.apply_state_vector(state_vector)
|
self.apply_state_vector(state_vector)
|
||||||
connected_h = self._close_current_state(_time)
|
connected_h = self._close_current_state(_time)
|
||||||
return self._state_derivatives(connected_h)
|
derivatives = self._state_derivatives(connected_h)
|
||||||
|
provider = self._ode_tangent_provider
|
||||||
|
if provider is not None:
|
||||||
|
provider.record_primal(_time, state_vector, connected_h)
|
||||||
|
return derivatives
|
||||||
|
|
||||||
def _append_current_state(self, series: dict[str, list[float]]) -> None:
|
def _append_current_state(self, series: dict[str, list[float]]) -> None:
|
||||||
for component in self.network.components.values():
|
for component in self.network.components.values():
|
||||||
@@ -1156,29 +1226,112 @@ class GenericFluidSystem:
|
|||||||
report_solver_time(time)
|
report_solver_time(time)
|
||||||
return self.rhs(time, state_vector)
|
return self.rhs(time, state_vector)
|
||||||
|
|
||||||
|
jacobian = None
|
||||||
|
jacobian_fallback_reason: str | None = None
|
||||||
|
tangent_compilation: ThreePistonTangentCompilation | None = None
|
||||||
|
selected_tangent_provider: ThreePistonTangentProvider | None = None
|
||||||
|
self._ode_tangent_provider = None
|
||||||
|
requested_jacobian_mode = (
|
||||||
|
_requested_ode_jacobian_mode()
|
||||||
|
if jac_sparsity is not None
|
||||||
|
else "scipy"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
jac_sparsity is not None
|
||||||
|
and requested_jacobian_mode
|
||||||
|
in {"optimized", "hybrid", "semi-analytic"}
|
||||||
|
):
|
||||||
|
state_count = int(jac_sparsity.shape[0])
|
||||||
|
if (
|
||||||
|
requested_jacobian_mode == "hybrid"
|
||||||
|
and not self.pressure_flow_solver.causal_fast_path_enabled
|
||||||
|
):
|
||||||
|
jacobian_fallback_reason = "causalAlgebraicExecutionUnavailable"
|
||||||
|
elif int(jac_sparsity.nnz) >= state_count * state_count:
|
||||||
|
jacobian_fallback_reason = "denseStateDependencyPattern"
|
||||||
|
else:
|
||||||
|
exact_columns = None
|
||||||
|
if requested_jacobian_mode == "semi-analytic":
|
||||||
|
tangent_compilation = (
|
||||||
|
compile_three_piston_tangent_provider(self)
|
||||||
|
)
|
||||||
|
if tangent_compilation.eligible:
|
||||||
|
provider = tangent_compilation.provider
|
||||||
|
if provider is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"An eligible tangent compilation has no provider."
|
||||||
|
)
|
||||||
|
selected_tangent_provider = provider
|
||||||
|
exact_columns = (
|
||||||
|
tangent_compilation.columns,
|
||||||
|
provider,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
jacobian_fallback_reason = (
|
||||||
|
f"semiAnalytic:{tangent_compilation.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
requested_jacobian_mode != "semi-analytic"
|
||||||
|
or tangent_compilation is not None
|
||||||
|
and tangent_compilation.eligible
|
||||||
|
):
|
||||||
|
def evaluate_jacobian_rhs(time, state):
|
||||||
|
if cancel_check is not None and cancel_check():
|
||||||
|
raise IntegrationCancelled
|
||||||
|
return monitored_rhs(
|
||||||
|
time,
|
||||||
|
[float(value) for value in state],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
jacobian = SparseSecantJacobian(
|
||||||
|
evaluate_jacobian_rhs,
|
||||||
|
jac_sparsity,
|
||||||
|
integration_config.atol,
|
||||||
|
exact_rows=self._exact_ode_jacobian_rows(),
|
||||||
|
exact_columns=exact_columns,
|
||||||
|
max_consecutive_reuses=(
|
||||||
|
1
|
||||||
|
if requested_jacobian_mode == "hybrid"
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._ode_tangent_provider = (
|
||||||
|
selected_tangent_provider
|
||||||
|
)
|
||||||
|
except SparseJacobianCompatibilityError as exc:
|
||||||
|
jacobian_fallback_reason = (
|
||||||
|
f"scipyCompatibility:{type(exc).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
def handle_state_transition(*args):
|
def handle_state_transition(*args):
|
||||||
transition = self.mechanical_state_reducer.state_transition(*args)
|
transition = self.mechanical_state_reducer.state_transition(*args)
|
||||||
if transition is not None:
|
if transition is not None:
|
||||||
self._request_causal_residual_audit()
|
self._request_causal_residual_audit()
|
||||||
return transition
|
return transition
|
||||||
|
|
||||||
solution = integrate_ode(
|
try:
|
||||||
rhs=monitored_rhs,
|
solution = integrate_ode(
|
||||||
initial_state=initial_state,
|
rhs=monitored_rhs,
|
||||||
config=integration_config,
|
initial_state=initial_state,
|
||||||
t_eval=t_eval,
|
config=integration_config,
|
||||||
cancel_check=cancel_check,
|
t_eval=t_eval,
|
||||||
accepted_step_callback=(
|
cancel_check=cancel_check,
|
||||||
report_solver_time if cancel_check is not None else None
|
accepted_step_callback=(
|
||||||
),
|
report_solver_time if cancel_check is not None else None
|
||||||
breakpoints=signal_event_times,
|
),
|
||||||
state_transition_handler=(
|
breakpoints=signal_event_times,
|
||||||
handle_state_transition
|
state_transition_handler=(
|
||||||
if self.mechanical_state_reducer.has_state_events
|
handle_state_transition
|
||||||
else None
|
if self.mechanical_state_reducer.has_state_events
|
||||||
),
|
else None
|
||||||
jac_sparsity=jac_sparsity,
|
),
|
||||||
)
|
jac_sparsity=jac_sparsity,
|
||||||
|
jac=jacobian,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._ode_tangent_provider = None
|
||||||
if isinstance(solution, ODESolution):
|
if isinstance(solution, ODESolution):
|
||||||
run_status: SimulationRunStatus = solution.status
|
run_status: SimulationRunStatus = solution.status
|
||||||
integration_error = solution.error
|
integration_error = solution.error
|
||||||
@@ -1212,6 +1365,44 @@ class GenericFluidSystem:
|
|||||||
"recoverableRetryCount": 0,
|
"recoverableRetryCount": 0,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
if jacobian is not None:
|
||||||
|
direct_jacobian = jacobian.diagnostics()
|
||||||
|
solver_segment_diagnostics[0].update(
|
||||||
|
{
|
||||||
|
"jacobianEvaluationCount": int(
|
||||||
|
direct_jacobian["jacobianEvaluationCount"]
|
||||||
|
),
|
||||||
|
"jacobianFullBuildCount": int(
|
||||||
|
direct_jacobian["fullBuildCount"]
|
||||||
|
),
|
||||||
|
"jacobianSecantReuseCount": int(
|
||||||
|
direct_jacobian["secantReuseCount"]
|
||||||
|
),
|
||||||
|
"jacobianAuditFailureCount": int(
|
||||||
|
direct_jacobian["auditFailureCount"]
|
||||||
|
),
|
||||||
|
"finiteDifferenceRhsEvaluationCount": int(
|
||||||
|
direct_jacobian[
|
||||||
|
"finiteDifferenceRhsEvaluationCount"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"jacobianBaseRhsEvaluationCount": int(
|
||||||
|
direct_jacobian["baseRhsEvaluationCount"]
|
||||||
|
),
|
||||||
|
"jacobianJvAuditRhsEvaluationCount": int(
|
||||||
|
direct_jacobian["jvAuditEvaluationCount"]
|
||||||
|
),
|
||||||
|
"exactColumnBuildCount": int(
|
||||||
|
direct_jacobian["exactColumnBuildCount"]
|
||||||
|
),
|
||||||
|
"exactColumnFallbackCount": int(
|
||||||
|
direct_jacobian["exactColumnFallbackCount"]
|
||||||
|
),
|
||||||
|
"jacobianAssemblySeconds": float(
|
||||||
|
direct_jacobian["assemblySeconds"]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
solver_total_keys = (
|
solver_total_keys = (
|
||||||
"nfev",
|
"nfev",
|
||||||
"njev",
|
"njev",
|
||||||
@@ -1225,21 +1416,123 @@ class GenericFluidSystem:
|
|||||||
key: sum(int(segment[key]) for segment in solver_segment_diagnostics)
|
key: sum(int(segment[key]) for segment in solver_segment_diagnostics)
|
||||||
for key in solver_total_keys
|
for key in solver_total_keys
|
||||||
}
|
}
|
||||||
|
jacobian_work_keys = (
|
||||||
|
"jacobianEvaluationCount",
|
||||||
|
"jacobianFullBuildCount",
|
||||||
|
"jacobianSecantReuseCount",
|
||||||
|
"jacobianAuditFailureCount",
|
||||||
|
"finiteDifferenceRhsEvaluationCount",
|
||||||
|
"jacobianBaseRhsEvaluationCount",
|
||||||
|
"jacobianJvAuditRhsEvaluationCount",
|
||||||
|
"exactColumnBuildCount",
|
||||||
|
"exactColumnFallbackCount",
|
||||||
|
"jacobianAssemblySeconds",
|
||||||
|
)
|
||||||
|
for key in jacobian_work_keys:
|
||||||
|
if any(key in segment for segment in solver_segment_diagnostics):
|
||||||
|
solver_totals[key] = sum(
|
||||||
|
segment.get(key, 0)
|
||||||
|
for segment in solver_segment_diagnostics
|
||||||
|
)
|
||||||
jacobian_diagnostics = (
|
jacobian_diagnostics = (
|
||||||
self.jacobian_sparsity_diagnostics()
|
self.jacobian_sparsity_diagnostics()
|
||||||
if integration_config.method in {"BDF", "Radau"}
|
if integration_config.method in {"BDF", "Radau"}
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
runtime_jacobian_diagnostics: dict[str, object] | None = None
|
||||||
if jacobian_diagnostics is not None:
|
if jacobian_diagnostics is not None:
|
||||||
color_group_count = int(jacobian_diagnostics["colorGroupCount"])
|
color_group_count = int(jacobian_diagnostics["colorGroupCount"])
|
||||||
for segment in solver_segment_diagnostics:
|
if jacobian is None:
|
||||||
segment["finiteDifferenceRhsEstimate"] = (
|
for segment in solver_segment_diagnostics:
|
||||||
int(segment["njev"]) * color_group_count
|
segment["finiteDifferenceRhsEstimate"] = (
|
||||||
|
int(segment["njev"]) * color_group_count
|
||||||
|
)
|
||||||
|
runtime_jacobian_diagnostics = {
|
||||||
|
"mode": "scipySparseFiniteDifference",
|
||||||
|
"fallbackReason": jacobian_fallback_reason,
|
||||||
|
"jacobianEvaluationCount": int(solver_totals["njev"]),
|
||||||
|
"fullBuildCount": int(solver_totals["njev"]),
|
||||||
|
"finiteDifferenceRhsEstimateIsExact": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
for segment in solver_segment_diagnostics:
|
||||||
|
segment["finiteDifferenceRhsEstimate"] = int(
|
||||||
|
segment.get("finiteDifferenceRhsEvaluationCount", 0)
|
||||||
|
) + int(
|
||||||
|
segment.get("jacobianJvAuditRhsEvaluationCount", 0)
|
||||||
|
)
|
||||||
|
runtime_jacobian_diagnostics = dict(jacobian.diagnostics())
|
||||||
|
runtime_jacobian_diagnostics.update(
|
||||||
|
{
|
||||||
|
"fallbackReason": jacobian_fallback_reason,
|
||||||
|
"finiteDifferenceRhsEstimateIsExact": True,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
if tangent_compilation is not None:
|
||||||
|
runtime_jacobian_diagnostics["tangentCompilation"] = (
|
||||||
|
tangent_compilation.diagnostics()
|
||||||
|
)
|
||||||
|
if tangent_compilation.eligible and jacobian is not None:
|
||||||
|
runtime_jacobian_diagnostics["mode"] = (
|
||||||
|
"semiAnalyticExactColumns"
|
||||||
|
)
|
||||||
|
exact_builds = int(
|
||||||
|
runtime_jacobian_diagnostics[
|
||||||
|
"exactColumnBuildCount"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
exact_fallbacks = int(
|
||||||
|
runtime_jacobian_diagnostics[
|
||||||
|
"exactColumnFallbackCount"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if exact_builds == 0 and exact_fallbacks == 0:
|
||||||
|
effective_mode = "notEvaluated"
|
||||||
|
elif exact_builds == 0:
|
||||||
|
effective_mode = "numericalFallbackOnly"
|
||||||
|
elif exact_fallbacks:
|
||||||
|
effective_mode = "mixedExactAndNumericalFallback"
|
||||||
|
else:
|
||||||
|
effective_mode = "exactColumns"
|
||||||
|
runtime_jacobian_diagnostics["effectiveMode"] = (
|
||||||
|
effective_mode
|
||||||
|
)
|
||||||
|
if exact_fallbacks:
|
||||||
|
runtime_jacobian_diagnostics[
|
||||||
|
"runtimeFallbackReason"
|
||||||
|
] = runtime_jacobian_diagnostics[
|
||||||
|
"lastExactColumnFallbackReason"
|
||||||
|
]
|
||||||
solver_totals["finiteDifferenceRhsEstimate"] = sum(
|
solver_totals["finiteDifferenceRhsEstimate"] = sum(
|
||||||
int(segment["finiteDifferenceRhsEstimate"])
|
int(segment["finiteDifferenceRhsEstimate"])
|
||||||
for segment in solver_segment_diagnostics
|
for segment in solver_segment_diagnostics
|
||||||
)
|
)
|
||||||
|
if jacobian is None:
|
||||||
|
solver_totals["jacobianRhsEvaluationCountEstimate"] = (
|
||||||
|
int(solver_totals["finiteDifferenceRhsEstimate"])
|
||||||
|
+ int(solver_totals["njev"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
solver_totals["jacobianRhsEvaluationCount"] = (
|
||||||
|
int(
|
||||||
|
solver_totals.get(
|
||||||
|
"finiteDifferenceRhsEvaluationCount",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
+ int(
|
||||||
|
solver_totals.get(
|
||||||
|
"jacobianBaseRhsEvaluationCount",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
+ int(
|
||||||
|
solver_totals.get(
|
||||||
|
"jacobianJvAuditRhsEvaluationCount",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
with performance_span("simulation.postprocessing"):
|
with performance_span("simulation.postprocessing"):
|
||||||
series: dict[str, list[float]] = {"time": []}
|
series: dict[str, list[float]] = {"time": []}
|
||||||
@@ -1286,6 +1579,7 @@ class GenericFluidSystem:
|
|||||||
"integration": {
|
"integration": {
|
||||||
"method": integration_config.method,
|
"method": integration_config.method,
|
||||||
"jacobianSparsity": jacobian_diagnostics,
|
"jacobianSparsity": jacobian_diagnostics,
|
||||||
|
"jacobian": runtime_jacobian_diagnostics,
|
||||||
"segmentCount": len(solver_segment_diagnostics),
|
"segmentCount": len(solver_segment_diagnostics),
|
||||||
"segments": solver_segment_diagnostics,
|
"segments": solver_segment_diagnostics,
|
||||||
"totals": solver_totals,
|
"totals": solver_totals,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions DisableDelayedExpansion
|
||||||
|
|
||||||
|
title SystemSimulationApp Launcher
|
||||||
|
|
||||||
|
set "BACKEND_SCRIPT=%~dp0start-backend.bat"
|
||||||
|
set "FRONTEND_SCRIPT=%~dp0start-reactflow.bat"
|
||||||
|
|
||||||
|
if not exist "%BACKEND_SCRIPT%" (
|
||||||
|
echo [ERROR] start-backend.bat was not found:
|
||||||
|
echo %BACKEND_SCRIPT%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%FRONTEND_SCRIPT%" (
|
||||||
|
echo [ERROR] start-reactflow.bat was not found:
|
||||||
|
echo %FRONTEND_SCRIPT%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Starting FastAPI and ReactFlow in separate windows...
|
||||||
|
|
||||||
|
set "LAUNCH_ERROR=0"
|
||||||
|
ver >nul
|
||||||
|
start "FastAPI - 127.0.0.1:8000" "%ComSpec%" /d /c call "%BACKEND_SCRIPT%"
|
||||||
|
if errorlevel 1 set "LAUNCH_ERROR=1"
|
||||||
|
ver >nul
|
||||||
|
start "ReactFlow - 127.0.0.1:5173" "%ComSpec%" /d /c call "%FRONTEND_SCRIPT%"
|
||||||
|
if errorlevel 1 set "LAUNCH_ERROR=1"
|
||||||
|
|
||||||
|
if "%LAUNCH_ERROR%"=="1" (
|
||||||
|
echo.
|
||||||
|
echo [ERROR] One or more service windows could not be created.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
exit /b 0
|
||||||
Executable
+130
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then
|
||||||
|
echo "[ERROR] start-all.sh requires Bash 4.3 or newer." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
BACKEND_SCRIPT="$SCRIPT_DIR/start-backend.sh"
|
||||||
|
FRONTEND_SCRIPT="$SCRIPT_DIR/start-reactflow.sh"
|
||||||
|
|
||||||
|
if [[ ! -x "$BACKEND_SCRIPT" ]]; then
|
||||||
|
echo "[ERROR] Backend start script is missing or not executable:" >&2
|
||||||
|
echo " $BACKEND_SCRIPT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$FRONTEND_SCRIPT" ]]; then
|
||||||
|
echo "[ERROR] Frontend start script is missing or not executable:" >&2
|
||||||
|
echo " $FRONTEND_SCRIPT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKEND_PID=""
|
||||||
|
FRONTEND_PID=""
|
||||||
|
|
||||||
|
signal_process_group() {
|
||||||
|
local signal="$1"
|
||||||
|
local pid="$2"
|
||||||
|
|
||||||
|
[[ -n "$pid" ]] || return 0
|
||||||
|
kill "-$signal" -- "-$pid" 2>/dev/null || kill "-$signal" "$pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
process_group_is_running() {
|
||||||
|
local pid="$1"
|
||||||
|
|
||||||
|
[[ -n "$pid" ]] && kill -0 -- "-$pid" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local backend_cleared=false
|
||||||
|
local force_kill=false
|
||||||
|
local frontend_cleared=false
|
||||||
|
local launcher_pid=$$
|
||||||
|
local timer_pid
|
||||||
|
|
||||||
|
trap '' INT TERM HUP
|
||||||
|
trap - EXIT
|
||||||
|
trap 'force_kill=true' ALRM
|
||||||
|
|
||||||
|
signal_process_group TERM "$BACKEND_PID"
|
||||||
|
signal_process_group TERM "$FRONTEND_PID"
|
||||||
|
|
||||||
|
(
|
||||||
|
sleep 5
|
||||||
|
kill -ALRM "$launcher_pid" 2>/dev/null || true
|
||||||
|
) &
|
||||||
|
timer_pid=$!
|
||||||
|
|
||||||
|
if [[ -n "$BACKEND_PID" ]]; then
|
||||||
|
wait "$BACKEND_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ "$force_kill" == false && -n "$FRONTEND_PID" ]]; then
|
||||||
|
wait "$FRONTEND_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [[ "$force_kill" == false ]]; do
|
||||||
|
if [[ "$backend_cleared" == false ]] && ! process_group_is_running "$BACKEND_PID"; then
|
||||||
|
backend_cleared=true
|
||||||
|
fi
|
||||||
|
if [[ "$frontend_cleared" == false ]] && ! process_group_is_running "$FRONTEND_PID"; then
|
||||||
|
frontend_cleared=true
|
||||||
|
fi
|
||||||
|
if [[ "$backend_cleared" == true && "$frontend_cleared" == true ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
kill -KILL "$timer_pid" 2>/dev/null || true
|
||||||
|
wait "$timer_pid" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$force_kill" == true ]]; then
|
||||||
|
if [[ "$backend_cleared" == false ]] && ! process_group_is_running "$BACKEND_PID"; then
|
||||||
|
backend_cleared=true
|
||||||
|
fi
|
||||||
|
if [[ "$frontend_cleared" == false ]] && ! process_group_is_running "$FRONTEND_PID"; then
|
||||||
|
frontend_cleared=true
|
||||||
|
fi
|
||||||
|
if [[ "$backend_cleared" == false ]]; then
|
||||||
|
signal_process_group KILL "$BACKEND_PID"
|
||||||
|
fi
|
||||||
|
if [[ "$frontend_cleared" == false ]]; then
|
||||||
|
signal_process_group KILL "$FRONTEND_PID"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$BACKEND_PID" ]]; then
|
||||||
|
wait "$BACKEND_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n "$FRONTEND_PID" ]]; then
|
||||||
|
wait "$FRONTEND_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap - ALRM
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
trap 'exit 129' HUP
|
||||||
|
|
||||||
|
echo "Starting FastAPI and ReactFlow..."
|
||||||
|
echo "Press Ctrl+C to stop both services."
|
||||||
|
echo
|
||||||
|
|
||||||
|
set -m
|
||||||
|
"$BACKEND_SCRIPT" &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
"$FRONTEND_SCRIPT" &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
set +m
|
||||||
|
|
||||||
|
wait -n
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
exit "$EXIT_CODE"
|
||||||
@@ -1,15 +1,28 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal EnableExtensions DisableDelayedExpansion
|
||||||
|
|
||||||
cd /d "%~dp0"
|
for %%I in ("%~dp0..") do set "REPO_ROOT=%%~fI"
|
||||||
title SystemSimulationApp FastAPI - 127.0.0.1:8000
|
title SystemSimulationApp FastAPI - 127.0.0.1:8000
|
||||||
|
|
||||||
set "PYTHON_EXE=%~dp0.venv-win\Scripts\python.exe"
|
set "PYTHON_EXE=%REPO_ROOT%\.venv-win\Scripts\python.exe"
|
||||||
|
|
||||||
if not exist "%PYTHON_EXE%" (
|
if not exist "%PYTHON_EXE%" (
|
||||||
echo [ERROR] Python virtual environment was not found:
|
echo [ERROR] Python virtual environment was not found:
|
||||||
echo %PYTHON_EXE%
|
echo %PYTHON_EXE%
|
||||||
echo.
|
echo.
|
||||||
|
echo Create it and install the backend dependencies first:
|
||||||
|
echo py -3 -m venv "%REPO_ROOT%\.venv-win"
|
||||||
|
echo "%PYTHON_EXE%" -m pip install -r "%REPO_ROOT%\requirements.txt"
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
pushd "%REPO_ROOT%" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Unable to enter the repository directory:
|
||||||
|
echo %REPO_ROOT%
|
||||||
|
echo.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
@@ -27,4 +40,5 @@ if not "%EXIT_CODE%"=="0" (
|
|||||||
pause
|
pause
|
||||||
)
|
)
|
||||||
|
|
||||||
|
popd
|
||||||
exit /b %EXIT_CODE%
|
exit /b %EXIT_CODE%
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
PYTHON_EXE="${SYSTEM_SIMULATION_PYTHON:-$REPO_ROOT/.venv/bin/python}"
|
||||||
|
|
||||||
|
if [[ "$PYTHON_EXE" != /* ]]; then
|
||||||
|
PYTHON_EXE="$REPO_ROOT/$PYTHON_EXE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$PYTHON_EXE" ]]; then
|
||||||
|
echo "[ERROR] Python virtual environment was not found:" >&2
|
||||||
|
echo " $PYTHON_EXE" >&2
|
||||||
|
echo >&2
|
||||||
|
echo "Create it and install the backend dependencies first:" >&2
|
||||||
|
echo " python3 -m venv \"$REPO_ROOT/.venv\"" >&2
|
||||||
|
echo " \"$REPO_ROOT/.venv/bin/python\" -m pip install -r \"$REPO_ROOT/requirements.txt\"" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
echo "Starting FastAPI at http://127.0.0.1:8000"
|
||||||
|
echo "Press Ctrl+C to stop the service."
|
||||||
|
echo
|
||||||
|
|
||||||
|
exec "$PYTHON_EXE" -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions DisableDelayedExpansion
|
||||||
|
|
||||||
|
for %%I in ("%~dp0..") do set "REPO_ROOT=%%~fI"
|
||||||
|
set "FRONTEND_DIR=%REPO_ROOT%\frontend"
|
||||||
|
title SystemSimulationApp ReactFlow - 127.0.0.1:5173
|
||||||
|
|
||||||
|
if not exist "%FRONTEND_DIR%\package.json" (
|
||||||
|
echo [ERROR] Frontend package.json was not found:
|
||||||
|
echo %FRONTEND_DIR%\package.json
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "NODE_DIR="
|
||||||
|
for /d %%D in ("%REPO_ROOT%\.tools\node-*-win-x64") do (
|
||||||
|
if not defined NODE_DIR if exist "%%~fD\node.exe" if exist "%%~fD\npm.cmd" (
|
||||||
|
"%%~fD\node.exe" -e "v=process.versions.node.split('.');M=+v[0];m=+v[1];process.exit((M===20&&m>=19)||(M===22&&m>=12)||M>=23?0:1)" >nul 2>&1
|
||||||
|
if not errorlevel 1 set "NODE_DIR=%%~fD"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not defined NODE_DIR (
|
||||||
|
echo [ERROR] A compatible Node.js portable runtime was not found under:
|
||||||
|
echo %REPO_ROOT%\.tools
|
||||||
|
echo Vite requires Node.js 20.19+ or 22.12+.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "NPM_EXE=%NODE_DIR%\npm.cmd"
|
||||||
|
if not exist "%NPM_EXE%" (
|
||||||
|
echo [ERROR] npm.cmd was not found:
|
||||||
|
echo %NPM_EXE%
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%FRONTEND_DIR%\node_modules\.bin\vite.cmd" (
|
||||||
|
echo [ERROR] Frontend dependencies are not installed.
|
||||||
|
echo Run the following command first:
|
||||||
|
echo cd /d "%FRONTEND_DIR%"
|
||||||
|
echo call "%NPM_EXE%" ci
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
pushd "%FRONTEND_DIR%" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Unable to enter the frontend directory:
|
||||||
|
echo %FRONTEND_DIR%
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "PATH=%NODE_DIR%;%PATH%"
|
||||||
|
|
||||||
|
echo Starting ReactFlow at http://127.0.0.1:5173
|
||||||
|
echo Press Ctrl+C to stop the service.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
call "%NPM_EXE%" run dev -- --strictPort
|
||||||
|
set "EXIT_CODE=%ERRORLEVEL%"
|
||||||
|
|
||||||
|
if not "%EXIT_CODE%"=="0" (
|
||||||
|
echo.
|
||||||
|
echo [ERROR] ReactFlow exited with code %EXIT_CODE%.
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
|
||||||
|
popd
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
FRONTEND_DIR="$REPO_ROOT/frontend"
|
||||||
|
|
||||||
|
node_version_supported() {
|
||||||
|
local version="$1"
|
||||||
|
local major
|
||||||
|
local minor
|
||||||
|
|
||||||
|
[[ "$version" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+) ]] || return 1
|
||||||
|
major=$((10#${BASH_REMATCH[1]}))
|
||||||
|
minor=$((10#${BASH_REMATCH[2]}))
|
||||||
|
(( (major == 20 && minor >= 19) || (major == 22 && minor >= 12) || major >= 23 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -f "$FRONTEND_DIR/package.json" ]]; then
|
||||||
|
echo "[ERROR] Frontend package.json was not found:" >&2
|
||||||
|
echo " $FRONTEND_DIR/package.json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NODE_EXE=""
|
||||||
|
NPM_EXE=""
|
||||||
|
for candidate in "$REPO_ROOT"/.tools/node-*-linux-x64/bin; do
|
||||||
|
if [[ -x "$candidate/node" && -x "$candidate/npm" ]]; then
|
||||||
|
CANDIDATE_VERSION="$("$candidate/node" --version 2>/dev/null || true)"
|
||||||
|
if node_version_supported "$CANDIDATE_VERSION"; then
|
||||||
|
NODE_EXE="$candidate/node"
|
||||||
|
NPM_EXE="$candidate/npm"
|
||||||
|
PATH="$candidate:${PATH:-}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
if [[ -z "$NODE_EXE" ]]; then
|
||||||
|
NODE_EXE="$(command -v node || true)"
|
||||||
|
NPM_EXE="$(command -v npm || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$NODE_EXE" || -z "$NPM_EXE" ]]; then
|
||||||
|
echo "[ERROR] Node.js and npm were not found." >&2
|
||||||
|
echo "Install Node.js 20.19+ or 22.12+ and make node/npm available on PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NODE_VERSION="$("$NODE_EXE" --version 2>/dev/null || true)"
|
||||||
|
if ! node_version_supported "$NODE_VERSION"; then
|
||||||
|
echo "[ERROR] Unsupported Node.js version: $NODE_VERSION" >&2
|
||||||
|
echo "Vite requires Node.js 20.19+ or 22.12+ (Node.js 21 is not supported)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$FRONTEND_DIR/node_modules/.bin/vite" ]]; then
|
||||||
|
echo "[ERROR] Frontend dependencies are not installed." >&2
|
||||||
|
echo "Run the following command first:" >&2
|
||||||
|
echo " cd \"$FRONTEND_DIR\" && \"$NPM_EXE\" ci" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
|
||||||
|
echo "Starting ReactFlow at http://127.0.0.1:5173"
|
||||||
|
echo "Press Ctrl+C to stop the service."
|
||||||
|
echo
|
||||||
|
|
||||||
|
exec "$NPM_EXE" run dev -- --strictPort
|
||||||
+24
-52
@@ -1,65 +1,37 @@
|
|||||||
# 开发文档索引
|
# 文档目录说明
|
||||||
|
|
||||||
本目录是 SystemSimulationApp 协议和开发规范的统一入口。
|
本目录是 SystemSimulationApp 更新日志、现行标准和其他技术报告的统一入口。
|
||||||
|
|
||||||
跨 HTTP、组件目录、模型合同和 System XML 的版本边界统一见
|
## 目录职责
|
||||||
[后端接口版本与定义规范 v1](backend-interface-version-spec-v1.md)。
|
|
||||||
|
|
||||||
## 更新记录
|
| 目录 | 职责 |
|
||||||
|
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| [`update-log/`](update-log/) | 按日期保存每日更新日志,记录当天已经完成的上传、修改及其影响。 |
|
||||||
|
| [`standard/`](standard/) | 保存当前采用的标准、协议和开发规范。实现、评审和 AI 修改代码时,应优先以这里的文档为准。 |
|
||||||
|
| [`other/`](other/) | 保存不属于现行标准的其他文档,例如性能仿真报告、优化报告、调研记录和技术总结。这些文档主要用于分析和参考,不默认作为强制规范。 |
|
||||||
|
|
||||||
- [更新日志 2026-08-15](更新日志-2026-08-15.md)
|
新增或移动文档时,应根据文档用途放入对应目录。目录链接可用于查看其中的全部文档,无需在本文件中逐项维护清单。
|
||||||
|
|
||||||
## 求解与性能
|
## `update-log` 书写规范
|
||||||
|
|
||||||
- [后端求解逻辑与效率优化调研](后端求解逻辑与效率优化调研.md)
|
以下规范适用于新建和后续追加的日志。历史日志缺少准确完成时间时,不猜测或补写时间。
|
||||||
- [仿真性能评估 2026-08-15](仿真性能评估-2026-08-15.md)
|
|
||||||
|
|
||||||
## 模型开发
|
1. 按日期填写日志。每个文件只记录一天的更新,文件名使用 `更新日志-YYYY-MM-DD.md`,标题使用 `# 更新日志 YYYY-MM-DD`。
|
||||||
|
2. 同一天内有多次上传或更新时,按各项工作的实际完成时间分段记录。每段使用北京时间、24 小时制的 `## HH:mm` 标注时间,并按完成时间从早到晚排列。
|
||||||
|
3. 不同时间完成的内容应分别记录,不要合并到同一时间段。一次上传或更新包含多项相关修改时,可以写在同一时间段内。
|
||||||
|
4. 语言应简短且信息充分,让 AI 和人都能快速理解。优先说明完成了什么、结果是什么、影响哪些范围,删除重复描述和无关过程。
|
||||||
|
5. 尽量使用通俗易懂的语言,减少难以理解的术语。必须使用专业术语时,应提供必要的简短说明。
|
||||||
|
|
||||||
1. [组件模型建模规范 v1](component-model-authoring-spec-v1.md)
|
推荐格式:
|
||||||
用于创建或修改模型,包括端口、参数、结果、方程、版本、测试和 AI 修改协议。
|
|
||||||
2. [组件库分类、发现与读取规范 v1](component-library-spec-v1.md)
|
|
||||||
用于理解组件库清单、自动发现、启动校验、目录接口和前端读取流程。
|
|
||||||
3. [组件目录 JSON Schema v1](../schemas/component-catalog-v1.schema.json)
|
|
||||||
`GET /api/components/catalog` 的机器可读结构。
|
|
||||||
4. [AMESim 子模型公开组件迁移矩阵](amesim-component-migration-matrix.md)
|
|
||||||
用于划分 `test_mql` 子模型族的公开组件、内部模型和暂不支持范围。
|
|
||||||
5. [AMESim 氦气 Peng-Robinson 介质模型](amesim-helium-peng-robinson.md)
|
|
||||||
记录本地 AMESim 资料、氦气参数、索引映射和首版计算边界。
|
|
||||||
|
|
||||||
建议人工和 AI 先阅读建模规范,再阅读读取规范,然后参考目标分类中最接近的现有
|
```markdown
|
||||||
模型。不要从前端兜底数据反推后端物理契约。
|
# 更新日志 YYYY-MM-DD
|
||||||
|
|
||||||
## System XML
|
## HH:mm
|
||||||
|
|
||||||
- [System XML v3 协议(当前规范)](system-xml-v3.md)
|
- 完成的修改、结果及影响范围。
|
||||||
- [System XML v3 XSD(当前 Schema)](../schemas/system-simulation-v3.xsd)
|
|
||||||
|
|
||||||
新增模型时,模型类和组件库清单是后端事实来源;System XML v3 只保存求解所需的
|
## HH:mm
|
||||||
组件实例、模型版本、参数、连接和仿真设置。端口契约由注册模型恢复,画布位置、图标
|
|
||||||
方向等编辑信息只属于工程 JSON。XML 解析器不能自行创造模型端口或参数。
|
|
||||||
|
|
||||||
## 当前代码入口
|
- 完成的修改、结果及影响范围。
|
||||||
|
```
|
||||||
| 目的 | 文件 |
|
|
||||||
| --- | --- |
|
|
||||||
| 组件基类 | [`app/simulation/core/base.py`](../app/simulation/core/base.py) |
|
|
||||||
| 端口契约 | [`app/simulation/core/ports.py`](../app/simulation/core/ports.py) |
|
|
||||||
| 参数与结果元数据 | [`app/simulation/core/metadata.py`](../app/simulation/core/metadata.py) |
|
|
||||||
| 库和显示声明 | [`app/simulation/core/catalog.py`](../app/simulation/core/catalog.py) |
|
|
||||||
| 库发现与注册校验 | [`app/simulation/registry.py`](../app/simulation/registry.py) |
|
|
||||||
| 临时库清单 | [`app/simulation/components/experimental/library.py`](../app/simulation/components/experimental/library.py) |
|
|
||||||
| AMESim 第一版公开临时库清单 | [`app/simulation/components/amesim/library.py`](../app/simulation/components/amesim/library.py) |
|
|
||||||
| `test_mql` 固定算例入口 | [`app/simulation/examples/test_mql/system.py`](../app/simulation/examples/test_mql/system.py) |
|
|
||||||
| 元件完整示例 | [`app/simulation/components/example.md`](../app/simulation/components/example.md) |
|
|
||||||
| System XML v3 解析与语义校验 | [`app/system_xml.py`](../app/system_xml.py) |
|
|
||||||
| System XML v3 XSD | [`schemas/system-simulation-v3.xsd`](../schemas/system-simulation-v3.xsd) |
|
|
||||||
|
|
||||||
## AI 使用原则
|
|
||||||
|
|
||||||
- 先读规范和相邻模型,再改代码。
|
|
||||||
- 只从 `library.py` 受控登记公开模型。
|
|
||||||
- 不在前端复制后端端口、参数或默认值作为正式来源。
|
|
||||||
- 不覆盖用户已有改动。
|
|
||||||
- 不自行猜测缺失的物理方程。
|
|
||||||
- 修改后运行针对性测试和完整回归,并报告未完成的验证。
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
适用模型:`AmesimModels/test_mql.ame` / `app.simulation.examples.test_mql.system`
|
适用模型:`AmesimModels/test_mql.ame` / `app.simulation.examples.test_mql.system`
|
||||||
|
|
||||||
配套规范:[`component-model-authoring-spec-v1.md`](component-model-authoring-spec-v1.md)
|
配套规范:[`component-model-authoring-spec-v1.md`](../standard/component-model-authoring-spec-v1.md)
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -406,8 +406,8 @@ STEP0、UD00 等信号源提供离散事件时刻。积分器先推进到事件
|
|||||||
|
|
||||||
### 10.1 进程与线程
|
### 10.1 进程与线程
|
||||||
|
|
||||||
- `start-all.bat` 分别启动 Vite 与 FastAPI(`start-all.bat:19-22`)。
|
- `bat/start-all.bat` 与 `bat/start-all.sh` 分别启动 Vite 与 FastAPI。
|
||||||
- `start-backend.bat` 的 Uvicorn 命令没有 `--workers`,当前脚本即单进程单 worker(`start-backend.bat:17-21`)。
|
- `bat/start-backend.bat` 与 `bat/start-backend.sh` 的 Uvicorn 命令没有 `--workers`,当前脚本即单进程单 worker。
|
||||||
- 每个流式仿真创建一个 daemon `threading.Thread` 和一个无界 `queue.Queue`;没有信号量、线程池或排队上限(`app/main.py:773-880`)。
|
- 每个流式仿真创建一个 daemon `threading.Thread` 和一个无界 `queue.Queue`;没有信号量、线程池或排队上限(`app/main.py:773-880`)。
|
||||||
- 全局任务字典只在读写元数据时持锁,不限制同时启动的求解数量。
|
- 全局任务字典只在读写元数据时持锁,不限制同时启动的求解数量。
|
||||||
- `POST /api/system-xml/simulate` 是 `async def`,但直接执行同步 CPU 求解;若调用该端点,会占用当前 Uvicorn 事件循环。
|
- `POST /api/system-xml/simulate` 是 `async def`,但直接执行同步 CPU 求解;若调用该端点,会占用当前 Uvicorn 事件循环。
|
||||||
@@ -593,5 +593,5 @@ O(组件 + 连接 + 代数结构)
|
|||||||
| 可选性能埋点 | `app/simulation/performance.py` | `profile_run()`、`profile_phase()`、`profile_property()` |
|
| 可选性能埋点 | `app/simulation/performance.py` | `profile_run()`、`profile_phase()`、`profile_property()` |
|
||||||
| 可重复性能基准 | `app/simulation/benchmark_performance.py` | `python -m app.simulation.benchmark_performance` |
|
| 可重复性能基准 | `app/simulation/benchmark_performance.py` | `python -m app.simulation.benchmark_performance` |
|
||||||
| 前端流式协议 | `frontend/src/App.tsx` | `streamSystemSimulation()`、取消/轮询 |
|
| 前端流式协议 | `frontend/src/App.tsx` | `streamSystemSimulation()`、取消/轮询 |
|
||||||
| 启动方式 | `start-backend.bat:17-21` | Uvicorn 单 worker 命令 |
|
| 启动方式 | `bat/start-backend.bat`、`bat/start-backend.sh` | Uvicorn 单 worker 命令 |
|
||||||
| 主路径回归测试 | `tests/test_generic_system_xml_simulation.py`、`tests/test_core_solver.py` | 通用仿真、事件、取消 |
|
| 主路径回归测试 | `tests/test_generic_system_xml_simulation.py`、`tests/test_core_solver.py` | 通用仿真、事件、取消 |
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
| 4 | 网络连接层 | `app/simulation/systems/network.py:83-150`:`SimulationNetwork.connect()` | 真正接线时做最终兼容检查 |
|
| 4 | 网络连接层 | `app/simulation/systems/network.py:83-150`:`SimulationNetwork.connect()` | 真正接线时做最终兼容检查 |
|
||||||
| 5 | JSON/XML | `ReactFlowPortDefinition`、System XML v3 XSD | JSON 搬运画布和端口显示快照;XML 只搬运可执行模型,端口合同由注册表恢复 |
|
| 5 | JSON/XML | `ReactFlowPortDefinition`、System XML v3 XSD | JSON 搬运画布和端口显示快照;XML 只搬运可执行模型,端口合同由注册表恢复 |
|
||||||
|
|
||||||
**[约定]** `docs/README.md:28-29` 和组件建模规范都说明:组件模型类及受控库清单是后端事实来源。XML 或前端不能凭空创造一个模型没有声明的端口。
|
**[约定]** [组件模型建模规范 v1](../standard/component-model-authoring-spec-v1.md)说明:组件模型类及受控库清单是后端事实来源。XML 或前端不能凭空创造一个模型没有声明的端口。
|
||||||
|
|
||||||
## 2. 常见术语翻译表
|
## 2. 常见术语翻译表
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ amesim_step0.out ──> amesim_forc.res [力源] amesim_forc.port_2 ── 机
|
|||||||
|
|
||||||
### 5.3 System XML v3:交给后端的精简求解清单
|
### 5.3 System XML v3:交给后端的精简求解清单
|
||||||
|
|
||||||
XML v3 和工程 JSON 不再追求“保存同一份完整工程”。两者分工很明确:工程 JSON 保存怎样编辑和显示,XML v3 保存后端求解什么。当前结构由 `schemas/system-simulation-v3.xsd` 定义;完整规范见 `docs/system-xml-v3.md`。
|
XML v3 和工程 JSON 不再追求“保存同一份完整工程”。两者分工很明确:工程 JSON 保存怎样编辑和显示,XML v3 保存后端求解什么。当前结构由 `schemas/system-simulation-v3.xsd` 定义;完整规范见 `docs/standard/system-xml-v3.md`。
|
||||||
|
|
||||||
#### 5.3.1 生成出来的 XML 是什么结构
|
#### 5.3.1 生成出来的 XML 是什么结构
|
||||||
|
|
||||||
@@ -554,7 +554,7 @@ XML 语义检查会把未连接端口记为 warning;真正进入通用求解
|
|||||||
|
|
||||||
### 7.6 不从旧格式推断当前行为
|
### 7.6 不从旧格式推断当前行为
|
||||||
|
|
||||||
当前格式只看 `docs/system-xml-v3.md` 和 `schemas/system-simulation-v3.xsd`。旧格式中的 `Port`、布局字段、端点 `role`、`Simulation/@step` 和“旋转改变力方向”都不能继续套用到 v3。
|
当前格式只看 `docs/standard/system-xml-v3.md` 和 `schemas/system-simulation-v3.xsd`。旧格式中的 `Port`、布局字段、端点 `role`、`Simulation/@step` 和“旋转改变力方向”都不能继续套用到 v3。
|
||||||
|
|
||||||
## 8. 当前模型覆盖范围
|
## 8. 当前模型覆盖范围
|
||||||
|
|
||||||
@@ -629,7 +629,7 @@ XML 语义检查会把未连接端口记为 warning;真正进入通用求解
|
|||||||
| 工程 JSON 与编译 | `app/main.py:86-155, 1181-1344` | `ReactFlowPortDefinition`、`compile_reactflow_network()` |
|
| 工程 JSON 与编译 | `app/main.py:86-155, 1181-1344` | `ReactFlowPortDefinition`、`compile_reactflow_network()` |
|
||||||
| JSON 转 XML | `app/main.py:947-1097` | `build_reactflow_system_xml()` |
|
| JSON 转 XML | `app/main.py:947-1097` | `build_reactflow_system_xml()` |
|
||||||
| XML 解析和语义检查 | `app/system_xml.py` | `SystemXmlComponent`、`SystemXmlEndpoint`、`SystemXmlDocument`、`validate_system_xml_document()` |
|
| XML 解析和语义检查 | `app/system_xml.py` | `SystemXmlComponent`、`SystemXmlEndpoint`、`SystemXmlDocument`、`validate_system_xml_document()` |
|
||||||
| XML v3 当前格式 | `schemas/system-simulation-v3.xsd`、`docs/system-xml-v3.md` | `sampleStep`、`modelVersion`、`Parameter`、`Endpoint` |
|
| XML v3 当前格式 | `schemas/system-simulation-v3.xsd`、`docs/standard/system-xml-v3.md` | `sampleStep`、`modelVersion`、`Parameter`、`Endpoint` |
|
||||||
| 目录 JSON 格式 | `schemas/component-catalog-v1.schema.json:52-160` | `$defs.portVariable`、`$defs.port` |
|
| 目录 JSON 格式 | `schemas/component-catalog-v1.schema.json:52-160` | `$defs.portVariable`、`$defs.port` |
|
||||||
| 前端端口和工程类型 | `frontend/src/App.tsx` | `PortDefinition`、`ReactFlowProjectPayload` |
|
| 前端端口和工程类型 | `frontend/src/App.tsx` | `PortDefinition`、`ReactFlowProjectPayload` |
|
||||||
| 前端生成 XML | `frontend/src/App.tsx` | `buildSystemXml()`、`projectConnectionMetadata()` |
|
| 前端生成 XML | `frontend/src/App.tsx` | `buildSystemXml()`、`projectConnectionMetadata()` |
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
# 求解器性能与鲁棒性优化任务清单
|
||||||
|
|
||||||
|
> 用途:记录求解器优化的现状、证据、实施顺序和验收结果,供后续开发前后对比与持续更新。
|
||||||
|
> 首次建立:2026-08-17
|
||||||
|
> 基线代码:`6bb0591d320d0c448ee8d224dd44127bfe3ce00f`(本地 `model-development`)
|
||||||
|
> 基线模型:`tests/data/test_mql-full-branches-01-04.xml`
|
||||||
|
> 模型 SHA-256:`2fb95e65f5de0c85a6a17802aef74ea004087323fd00fd8d01acf0184ff71d48`
|
||||||
|
|
||||||
|
## 1. 使用规则
|
||||||
|
|
||||||
|
本文档不是一次性的建议列表,而是优化工作的验收账本。
|
||||||
|
|
||||||
|
- 状态统一使用:`未开始`、`进行中`、`部分实现`、`已完成`、`阻塞`、`不采用`。
|
||||||
|
- 只有同时完成代码、自动测试、基准复测和本文档更新后,任务才可标记为“已完成”。
|
||||||
|
- 每次性能对比必须记录代码提交、工作树状态、输入哈希、解释器与依赖版本、硬件和运行参数。
|
||||||
|
- 正确性门槛先于速度收益。若结果越过误差契约,即使运行更快也不能合入默认路径。
|
||||||
|
- 容差、模型方程或输出字段发生变化时,必须单独说明;不得将其伪装成纯性能优化。
|
||||||
|
- 墙钟时间只在同一台机器、相同负载和相同环境下直接比较;跨环境以工作量计数和正确性指标为主。
|
||||||
|
- 每项优化都应保留明确的关闭开关或旧路径,直到新路径经过复杂模型和通用回归验证。
|
||||||
|
|
||||||
|
## 2. 当前结论与基线
|
||||||
|
|
||||||
|
### 2.1 关于 2.05 s 卡死
|
||||||
|
|
||||||
|
当前随附 XML 的磁盘配置是 `tStop=0.81 s`,因此原文件本身不会运行到 2.05 s。将停止时间仅在内存中改为 `2.10 s` 后,当前代码已经完整越过 2.05 s 并正常结束:
|
||||||
|
|
||||||
|
- `2.040432 s`:墙钟 `114.065 s`
|
||||||
|
- `2.046141 s`:墙钟 `120.746 s`,期间 CPU 时间持续增长
|
||||||
|
- `2.051691 s`:墙钟 `121.548 s`
|
||||||
|
- `2.100000 s`:完成积分并进入后处理
|
||||||
|
- 总运行完成,无重试、无非线性回退,也没有无进度死锁
|
||||||
|
|
||||||
|
因此,当前证据支持“此前的 2.05 s 卡死在现版本中没有复现”;该区间仍存在数秒级慢推进。`10 s` 尚未验证,不能由本次结果外推保证。
|
||||||
|
|
||||||
|
### 2.2 环境说明
|
||||||
|
|
||||||
|
仓库内 `.venv` 当前不完整,本次复测使用现有 `/opt/srm-trial-review/.venv`:
|
||||||
|
|
||||||
|
| 项目 | 本次值 |
|
||||||
|
| --- | --- |
|
||||||
|
| Python | 3.12.3 |
|
||||||
|
| NumPy | 2.4.6 |
|
||||||
|
| SciPy | 1.17.1 |
|
||||||
|
| 求解器 | BDF |
|
||||||
|
| 输出步长 | 0.01 s |
|
||||||
|
| 执行路径 | stream/cancel-check |
|
||||||
|
|
||||||
|
该环境满足仓库依赖范围,但并非已经锁定的正式项目环境。当前物理解哈希与历史调研文档不同,所以逐位结果基线必须在正式锁定环境中再次确认。
|
||||||
|
|
||||||
|
### 2.3 当前实测基线
|
||||||
|
|
||||||
|
| 指标 | 原始 `0.81 s` | 仅内存延长至 `2.10 s` |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| 状态 | 完成 | 完成,越过 2.05 s |
|
||||||
|
| 墙钟时间 | 63.779 s | 126.211 s |
|
||||||
|
| 积分时间 | 62.116 s | 122.180 s |
|
||||||
|
| 后处理时间 | 1.118 s | 2.703 s |
|
||||||
|
| 最大 RSS | 165,464 KiB | 198,348 KiB |
|
||||||
|
| 输出样本数 | 82 | 213 |
|
||||||
|
| 状态数 | 74 | 74 |
|
||||||
|
| `nfev / njev / nlu` | 3763 / 253 / 761 | 6734 / 487 / 1507 |
|
||||||
|
| 接受步 | 1076 | 1857 |
|
||||||
|
| 分段启动 | 3 | 5 |
|
||||||
|
| 状态切换 | 0 | 2 |
|
||||||
|
| 重试 | 0 | 0 |
|
||||||
|
| 有限差分附加 RHS 估计 | 7843 | 15097 |
|
||||||
|
| 压力闭合次数 | 30,502 | 57,601 |
|
||||||
|
| 非线性/块/稠密回退 | 0 / 0 / 0 | 0 / 0 / 0 |
|
||||||
|
| 最大热流体外迭代 | 3 | 3 |
|
||||||
|
| Jacobian 稀疏度 | 1284 nnz / 31 色 | 1284 nnz / 31 色 |
|
||||||
|
|
||||||
|
补充观察:
|
||||||
|
|
||||||
|
- 积分占总耗时约 97%,当前首要瓶颈不是后处理。
|
||||||
|
- `2.10 s` 运行中,估计总 RHS 工作量约为 `6734 + 15097 = 21831`;有限差分扰动约占 69%。
|
||||||
|
- 压力闭合约为每次估计 RHS 2.64 次,但全部走已播种的因果路径,没有触发 `least_squares`。
|
||||||
|
- 全局因果执行已启用:快速执行 22,216 次,完整残差审计 351 次,审计失败 0 次,旧路径回退 0 次,审计间隔为 64。
|
||||||
|
- 当前结果哈希仅作为本次环境的诊断记录:`0.81 s` 为 `c6354c97...`,`2.10 s` 为 `efef49f8...`;它们暂不作为跨环境验收标准。
|
||||||
|
|
||||||
|
### 2.4 当前模型结构基线
|
||||||
|
|
||||||
|
| 项目 | 数量 |
|
||||||
|
| --- | ---: |
|
||||||
|
| XML 组件 / 编译组件 | 99 / 98 |
|
||||||
|
| 连接 | 106 |
|
||||||
|
| 连续状态 | 74 |
|
||||||
|
| 代数未知量 / 方程 | 472 / 472 |
|
||||||
|
| 原始关联矩阵非零元 / 方程块 | 919 / 58 |
|
||||||
|
| effort 未知量 / flow 未知量 | 272 / 200 |
|
||||||
|
| effort 等价组 / 可消去重复 effort | 68 / 204 |
|
||||||
|
| 显式 flow/force 赋值 | 200 |
|
||||||
|
| stream 块 / stream 未知量 | 9 / 192 |
|
||||||
|
| 结果变量 | 1,021 |
|
||||||
|
|
||||||
|
## 3. 总体验收协议
|
||||||
|
|
||||||
|
每个优化 PR 至少执行以下分层验证;高风险改动不得只用单点输出或单个哈希判断正确性。
|
||||||
|
|
||||||
|
### 3.1 快速结构检查(CI)
|
||||||
|
|
||||||
|
- [ ] 模型输入 SHA-256 与固定 fixture 一致。
|
||||||
|
- [ ] 组件、连接、状态、代数方程和 stream 结构数量符合预期。
|
||||||
|
- [ ] Jacobian 结构至少覆盖已知跨域依赖,并通过稠密数值扰动抽查。
|
||||||
|
- [ ] 因果计划覆盖率、回退原因和审计失败数可观测。
|
||||||
|
|
||||||
|
### 3.2 数值检查点
|
||||||
|
|
||||||
|
至少覆盖以下区间和模式边界:
|
||||||
|
|
||||||
|
- [ ] `0.68–0.71 s`:历史慢区。
|
||||||
|
- [ ] `0.79–0.81 s`:原始模型终点及信号事件附近。
|
||||||
|
- [ ] `2.00–2.10 s`:此前报告卡死区间和状态切换。
|
||||||
|
- [ ] `10 s`:长时间模式变化验证,完成 OPT-09 后启用。
|
||||||
|
|
||||||
|
每个检查点比较:连续状态、关键压力/流量/位移/速度、事件时刻与顺序、模式状态、有限性、最大缩放残差及守恒量。
|
||||||
|
|
||||||
|
### 3.3 性能记录
|
||||||
|
|
||||||
|
每次正式对比至少预热 1 次、测量 3 次并报告中位数,同时保存:
|
||||||
|
|
||||||
|
- 总时间、积分时间、后处理时间、CPU 利用率、峰值 RSS。
|
||||||
|
- `nfev`、`njev`、`nlu`、接受/拒绝步、分段和重试次数。
|
||||||
|
- SciPy 模式的有限差分 RHS 估计;callable 模式的真实扰动、基准和 Jv 审计 RHS 计数;Jacobian 颜色数与构建时间。
|
||||||
|
- 代数闭合次数、快速因果次数、完整审计次数和各类回退次数。
|
||||||
|
- stream/热流体迭代次数、物性缓存命中率、事件候选与定位次数。
|
||||||
|
- 输出标量数、编码字节数、传输字节数和后处理峰值内存。
|
||||||
|
|
||||||
|
## 4. 优化任务总览
|
||||||
|
|
||||||
|
优先级定义:`P0` 为基线或正确性前置,`P1` 为主要性能收益,`P2` 为第二阶段,`P3` 为战略性或条件性工作。
|
||||||
|
|
||||||
|
| ID | 优先级 | 任务 | 当前状态 | 难度 | 预期价值 | 主要依赖 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| OPT-00 | P0 | 固化复现、环境和回归基线 | 进行中 | 中 | 很高 | 无 |
|
||||||
|
| OPT-01 | P1 | 完成因果代数内核与坐标消元 | 部分实现 | 中高 | 中高 | OPT-00 |
|
||||||
|
| OPT-02 | P1 | 建立扁平数值 IR 和数组执行内核 | 未开始 | 很高 | 很高 | OPT-01 |
|
||||||
|
| OPT-03 | P1 | 稀疏 Jacobian 数值层与解析/半解析演进 | 部分实现 | 很高 | 很高 | OPT-00;解析链可与 OPT-02 分阶段 |
|
||||||
|
| OPT-04 | P1 | stream 拓扑传播与物性成组复用 | 部分实现 | 中高 | 中高 | OPT-00 |
|
||||||
|
| OPT-05 | P1 | 状态缩放、分量容差和步长策略 | 未开始 | 中高 | 中高 | OPT-00 |
|
||||||
|
| OPT-06 | P2 | 事件检测与 dense output 按需化 | 部分实现 | 中 | 中 | OPT-00 |
|
||||||
|
| OPT-07 | P2 | 输出、后处理和传输内存优化 | 未开始 | 中 | 中高(长仿真) | OPT-00 |
|
||||||
|
| OPT-08 | P2 | 进度、取消和服务并发鲁棒性 | 部分实现 | 中 | 中 | OPT-00 |
|
||||||
|
| OPT-09 | P0/P2 | 建立 10 s 长时验证与模式覆盖 | 未开始 | 中高 | 很高 | OPT-00 |
|
||||||
|
| OPT-10 | P3 | 明确高指数 DAE/强非光滑系统边界 | 未开始 | 很高 | 条件性 | OPT-09 |
|
||||||
|
|
||||||
|
推荐实施顺序:`OPT-00 → OPT-03/OPT-01 → OPT-04/OPT-05 → OPT-02 → OPT-06/OPT-07/OPT-08 → OPT-09`。其中 OPT-02 与 OPT-03 可先做最小原型,再根据端到端数据调整顺序。
|
||||||
|
|
||||||
|
## 5. 详细任务
|
||||||
|
|
||||||
|
### OPT-00 固化复现、环境和回归基线
|
||||||
|
|
||||||
|
**目标**:先让“是否更快、是否仍正确、是否又卡住”可以稳定复现和自动判断。
|
||||||
|
|
||||||
|
**当前状态**:已有手工 `0.81 s` 和 `2.10 s` 复测及若干结构回归;复杂 XML、正式运行环境、分层性能门槛尚未完整固化。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 将复杂 XML 作为正式测试 fixture 纳入版本控制,并在测试中校验哈希。
|
||||||
|
- [ ] 修复或重建项目 `.venv`,锁定 Python、NumPy、SciPy 及平台信息。
|
||||||
|
- [ ] 将临时探针整理为仓库内可重复运行的 benchmark,不依赖 `/tmp` 文件。
|
||||||
|
- [ ] 添加 `0.81 s` 和仅改 `tStop=2.10 s` 的标准运行入口。
|
||||||
|
- [ ] 添加模型结构快照断言;结构有意变化时显式更新原因。
|
||||||
|
- [ ] 定义 `physical-state-v2`:仅包含物理状态、关键代数量、事件与模式,不包含展示字段和易变诊断字段。
|
||||||
|
- [ ] 将完整 API 输出哈希与物理解哈希分开,分别用于输出契约和数值回归。
|
||||||
|
- [ ] 建立短 CI、夜间 `0.81/2.10 s`、定期 `10 s` 三层任务。
|
||||||
|
- [ ] 保存机器可读的 JSON 基准结果,避免只在文档中抄写数字。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 干净环境一条命令可复现;失败时能区分超时、无进度、数值失败和服务失败。
|
||||||
|
- [ ] 正式环境连续 3 次完成 `2.10 s`,结果满足数值契约且无非预期回退。
|
||||||
|
- [ ] 性能报告完整记录环境、提交、工作树、输入哈希和统计口径。
|
||||||
|
|
||||||
|
**前后对比**:
|
||||||
|
|
||||||
|
| 指标 | 当前 | 完成后 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 正式锁定环境 | 无 | 待填 |
|
||||||
|
| 复杂模型自动回归 | 部分 | 待填 |
|
||||||
|
| 物理解哈希 | 环境相关 v1 | 待填 |
|
||||||
|
| `2.10 s` 连续成功率 | 单次证据 | 待填 |
|
||||||
|
|
||||||
|
### OPT-01 完成因果代数内核与坐标消元
|
||||||
|
|
||||||
|
**目标**:在已存在的因果快速路径上,真正移除运行时冗余坐标和对象访问,而不是再次实现一套同类快速路径。
|
||||||
|
|
||||||
|
**当前状态**:主要思路已经实现。全局和 secondary stream 块可以执行显式因果计划,完整残差按 64 次间隔审计;`2.10 s` 中快速执行 22,216 次、审计 351 次、失败和旧路径回退均为 0。仍保留 472 个运行时未知量,204 个重复 effort 坐标尚未在执行层消除,且存在清零、复制、缩放、`getattr/setattr` 和完整对象遍历成本。
|
||||||
|
|
||||||
|
一次已预热的 A/B 微基准显示,整个 RHS 的因果快速模式中位数约 `200.8 ms/100 次`,强制完整检查约 `297.4 ms/100 次`,即现有路径已经取得约 `1.48×` 的整 RHS 收益。单纯继续增大审计间隔预计收益有限。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 将 68 个 effort 等价组压缩为独立运行时坐标,消除 204 个重复 effort 槽。
|
||||||
|
- [ ] 将 200 条显式 flow/force 规则预编译为稳定顺序和整数槽索引。
|
||||||
|
- [ ] 用预分配连续数组代替热路径对象读写、临时字典和重复缩放。
|
||||||
|
- [ ] 仅清理会被当前计划写入的槽,避免每次全量清零和复制。
|
||||||
|
- [ ] 保留初始化、事件后、接受步或固定间隔的完整残差审计。
|
||||||
|
- [ ] 自定义组件、声明缺失、审计失败或奇异结构必须自动回退旧求解器。
|
||||||
|
- [ ] 输出编译统计:消元数、显式规则覆盖率、审计率、失败原因和回退次数。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 复杂模型因果覆盖率大于 98%,完整 `0.81/2.10 s` 运行审计失败为 0。
|
||||||
|
- [ ] 新旧路径的状态、事件、关键代数量和残差均满足统一数值契约。
|
||||||
|
- [ ] 自定义组件、接触模型和非因果结构的回退测试全部通过。
|
||||||
|
- [ ] 在完整模型上证明端到端收益;不得只提交代数微基准。
|
||||||
|
|
||||||
|
**风险与回滚**:别名写回、事件后模式改变和不完整依赖声明可能造成静默错误。新路径必须可通过配置关闭,并在审计失败时记录首个违规方程与变量。
|
||||||
|
|
||||||
|
| 指标 | 当前 | 完成后 |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| 运行时代数槽 | 472 | 待填 |
|
||||||
|
| 重复 effort 槽 | 204 | 待填 |
|
||||||
|
| 已预热 Python 调用/单 RHS | 约 15,774 | 待填 |
|
||||||
|
| 因果审计失败 | 0 | 待填 |
|
||||||
|
| `0.81/2.10 s` 墙钟中位数 | 63.779 / 126.211 s(单次环境值) | 待填 |
|
||||||
|
|
||||||
|
### OPT-02 建立扁平数值 IR 和数组执行内核
|
||||||
|
|
||||||
|
**目标**:把组件对象、字典查找和端口读写转换成稳定的数值执行计划,为 NumPy、Numba 或原生后端提供共同基础。
|
||||||
|
|
||||||
|
**当前状态**:构建阶段已有一定预绑定,但 RHS 仍以 Python 对象和方法调用为主。已预热、启用物性缓存时,采样剖析约有 15,774 次 Python 调用/RHS;不同缓存上下文会明显改变该数字,所以后续必须统一测量口径。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 定义最小数值 IR:连续槽、常量、参数、状态、代数量、模式位和操作码。
|
||||||
|
- [ ] 将组件方程、因果规则、stream 传播和结果提取分成明确执行阶段。
|
||||||
|
- [ ] 先实现可逐项对照的纯 Python/NumPy 参考后端。
|
||||||
|
- [ ] 添加 IR 与当前对象执行器逐操作/逐阶段差分测试。
|
||||||
|
- [ ] 评估 Numba 与 C/C++ 后端;在 IR 稳定前不绑定单一编译技术。
|
||||||
|
- [ ] 对动态自定义组件保留对象适配层和明确的性能降级提示。
|
||||||
|
- [ ] 缓存编译结果,并以模型结构、组件版本和数值后端作为缓存键。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 全部现有组件族通过新旧执行器差分测试。
|
||||||
|
- [ ] 事件切换后能正确重编译或选择预编译模式计划。
|
||||||
|
- [ ] 明显降低 Python 调用数、对象分配和 RHS 中位时间,并改善完整仿真墙钟。
|
||||||
|
- [ ] 不以牺牲异常信息、取消检查或回退能力换取速度。
|
||||||
|
|
||||||
|
| 指标 | 当前 | 原型后 | 完成后 |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| Python 调用/单 RHS | 约 15,774 | 待填 | 待填 |
|
||||||
|
| 临时分配字节/单 RHS | 待测 | 待填 | 待填 |
|
||||||
|
| RHS 中位时间 | 约 2 ms(现有微基准口径) | 待填 | 待填 |
|
||||||
|
| `2.10 s` 积分时间 | 122.180 s | 待填 | 待填 |
|
||||||
|
|
||||||
|
### OPT-03 稀疏 Jacobian 数值层与解析/半解析演进
|
||||||
|
|
||||||
|
**目标**:先建立可审计、可回滚的 callable sparse Jacobian 数值层,再逐步把组件、因果代数计划、stream 和物性的局部导数传播进来。完整稀疏有限差分、受审计 secant 和真正的解析/半解析 Jacobian 是三个不同阶段,必须分别记录和验收。
|
||||||
|
|
||||||
|
**当前状态**:数值层基础与实验候选已经实现;首批“证明门控”的三活塞 6 列半解析切片已经接入,但通用组件、stream SCC 和其余状态列仍未覆盖,因此 OPT-03 总体继续标记为“部分实现”。默认执行路径继续使用 SciPy `jac_sparsity`,半解析路径只允许通过 `SIMULATION_ODE_JACOBIAN_MODE=semi-analytic` 显式启用。
|
||||||
|
|
||||||
|
现有实现包括:
|
||||||
|
|
||||||
|
- direct 和 stepwise BDF/Radau 均可接收 callable `jac`;信号断点、状态事件和可恢复重启会清空 Jacobian 数值状态并重新构建,显式积分器完全忽略该对象。
|
||||||
|
- 新增独立的 sparse numerical Jacobian 内核,隔离并检查 SciPy 私有 `num_jac/group_columns` 接口。
|
||||||
|
- 每个 solver segment 记录完整构建、有限差分扰动、基准 RHS、Jv 审计、secant 复用/失败和装配时间;SciPy 模式的估计值不再伪装成 callable 模式的真实计数。
|
||||||
|
- 4 条无离散端挡模式歧义的机械运动学行直接装配为 `d(x')/d(v)=1`;带端挡的行继续数值差分。
|
||||||
|
- callable 内核新增 `exact_columns=(indices, provider)`:已提供精确导数的列从分组有限差分中移除,其余列仍按原始保守结构做 subset FD;原始色数、剩余色数、单次真实 FD、精确列构建及回退次数/原因都进入分段诊断。
|
||||||
|
- 精确列提供器用类型化 `ExactColumnsUnavailable` 表达当前点不可用;同一次构建会恢复原始 seed 0 的完整数值 Jacobian,避免把未知导数静默当成 0。模型编译证明失败、SciPy 私有接口不兼容或配置关闭时则直接保留原生 SciPy 路径。
|
||||||
|
- 首批目标是三条同构活塞支路的 6 个机械状态列 `(20, 21, 38, 39, 54, 55)`。编译器只有在组件类型、连接拓扑、因果赋值计划、机械等价组和 stream 影响范围都满足证明条件时才启用;该 XML 中共覆盖 34 条 reachable assignments,FD 颜色由 31 降至 25,另由提供器装配 6 列。
|
||||||
|
- 已增加 Ideal/PR 介质 `m/U/V` 物性线性化,以及 PNRP、PNCH012、PNL0001、LSTP、MECMAS 的局部切向原语;每个原语都返回 `valid/reason`,以便在非光滑接触、临界流动或不支持的模式上拒绝解析近似。
|
||||||
|
- Jacobian 内部每次 RHS 都执行取消检查;不安全的共享模型基准缓存已经撤销。随后实现的一次性 generation/dirty token 安全版本在正式 `0.81 s` 中 `253` 次 Jacobian 请求命中 `0` 次:BDF 首次构建前会做初始步长试算,后续构建前也会留下 Newton 试探状态,模型并不位于请求的基准点。该版本没有节省 RHS,最终也已删除。
|
||||||
|
- `SIMULATION_ODE_JACOBIAN_MODE=scipy` 是默认和回滚路径;小型全稠密结构或 SciPy 私有接口不兼容时也回到该路径。
|
||||||
|
|
||||||
|
显式 `SIMULATION_ODE_JACOBIAN_MODE=optimized` 仍构造完整稀疏有限差分 Jacobian,不使用 secant。最终实现严格固定 SciPy seed 0,并从原始 `1284 nnz` 保守结构生成 31 色扰动批次;移除精确行不会重新着色。曾试验的 seed 54 为 30 色,结构虽未删边,却改变了事件敏感模型的运行轨迹,因此多 seed 自动择优已经从代码中删除。
|
||||||
|
|
||||||
|
历史 30 色候选有性能收益,但没有通过事件/状态等价验收:
|
||||||
|
|
||||||
|
- 最终安全版本的 `0.81 s` 单次相邻 A/B 中,optimized 积分 `55.034 s`、总墙钟 `56.957 s`,SciPy 基线积分 `59.924 s`、总墙钟 `61.953 s`,分别约改善 `8.2% / 8.1%`。
|
||||||
|
- `0.81 s` 中 callable 实际 Jacobian RHS(扰动加基准)为 `7,103`,SciPy 估计为 `8,096`,约减少 `12.3%`;`nfev/njev/nlu` 为 `3467/228/670`,基线为 `3763/253/761`。
|
||||||
|
- 两条 `0.81 s` 轨迹具有相同结果键、采样时刻、0 次状态切换和约 `1e-16` 的最大代数残差,但最终 74 维状态的最大差异为 `51.59 × (atol + rtol·|y|)`,最差状态相对差约 `5.2e-5`,超过当前拟定的严格等价门槛。
|
||||||
|
- `2.10 s` optimized 仍成功越过 2.05 s,积分 `115.928 s`,而 SciPy 基线为 `122.180 s`;但 optimized 出现 `4` 次状态切换、`7` 次 solver 启动和 `215` 个样本,基线为 `2 / 5 / 213`。因此该候选的事件等价验收失败,不能设为默认。
|
||||||
|
- 短变体隔离显示:seed 0 callable(有或没有 4 条精确行)在 `0.01 s` 的最终 74 维状态与 SciPy 逐项一致;轨迹分叉来自 30 色 seed 54,而不是精确运动学行。这提示事件敏感模型需要更多运行中 Jacobian 漏边/弱依赖审计,不能只依赖初始点结构测试。
|
||||||
|
|
||||||
|
最终 seed 0 安全候选的正式 `0.81 s` 探针与 SciPy 基线具有相同的物理解哈希 `c6354c97...`、`3763/253/761` 的 `nfev/njev/nlu`、`1076` 个接受步、3 次 solver 启动、0 次状态切换和 `30,502` 次压力闭合。实际 Jacobian 内部 RHS 为 `7,872 + 253 = 8,125`;安全 token 缓存命中为 0。积分时间 `60.972 s`、探针总墙钟 `62.945 s`,相邻 SciPy 基线为 `59.924/61.953 s`,没有净收益并略有退化。因此安全缓存已删除,seed 0 callable 只保留为后续解析行接入与诊断基础,不进入默认路径;无需为一个已经失败收益门槛的候选继续做 `2.10 s` 性能复测。
|
||||||
|
|
||||||
|
`SIMULATION_ODE_JACOBIAN_MODE=hybrid` 另提供实验性的数值 secant 原型:最多连续复用一次,复用前执行确定性方向 Jv 审计,失败或审计无信息量会在同一次调用中完整刷新。目标模型的早期探针中候选审计普遍失败;用 seed 0 的旧完整 Jacobian 做 `0.01 s` 探针时,39 次复用审计全部失败,额外产生 39 次 Jv RHS,实际复用仍为 0。因此它目前既不是解析 Jacobian,也没有可声明的端到端收益。
|
||||||
|
|
||||||
|
**已完成的数值层工作**:
|
||||||
|
|
||||||
|
- [x] direct/stepwise BDF、Radau callable `jac` 接线;显式方法隔离。
|
||||||
|
- [x] breakpoint、事件、可恢复重启后的强制重建与分段计数。
|
||||||
|
- [x] 完整稀疏有限差分内核、严格 seed 0 着色、4 条安全精确行。
|
||||||
|
- [x] exact-columns subset FD、类型化同次完整回退和原始/剩余色数及回退诊断。
|
||||||
|
- [x] 真实 RHS/装配计数,以及 SciPy 估计口径分离。
|
||||||
|
- [x] Jacobian 内部有界取消检查;撤销不安全缓存及命中为 0 的安全 token 缓存。
|
||||||
|
- [x] 稠密结构、兼容问题和配置关闭时保留 SciPy 路径。
|
||||||
|
- [x] 最多一次复用、Jv 审计、无信息审计拒绝和失败完整刷新测试。
|
||||||
|
- [x] 复杂 XML `0.81/2.10 s` 单次性能与事件探针。
|
||||||
|
- [x] 同一代码版本完成 3 组相邻 `0.81 s` A/B,报告中位数与范围。
|
||||||
|
- [ ] 为事件敏感模型定义并通过状态、事件时刻/顺序和模式等价契约。
|
||||||
|
- [ ] 在正式锁定环境完成独立预热后的 3 次 A/B,复核中位数与离散度。
|
||||||
|
|
||||||
|
**解析/半解析后续工作**:
|
||||||
|
|
||||||
|
- [x] 为首批 Ideal/PR、PNRP、PNCH012、PNL0001、LSTP、MECMAS 路径定义带有效性诊断的局部切向契约。
|
||||||
|
- [x] 对目标三活塞 6 列沿 34 条可证明因果赋值传播导数,并从 FD 分组中排除这些列。
|
||||||
|
- [ ] 将局部导数/JVP 契约扩展到其余基础与自定义组件。
|
||||||
|
- [ ] 将因果传播推广到目标切片以外的状态列和代数计划。
|
||||||
|
- [ ] 对 stream SCC 推导显式或隐式小块导数。
|
||||||
|
- [ ] 对物性函数提供解析导数、可靠自动微分或受控局部差分接口。
|
||||||
|
- [ ] 在接触、饱和、开关和临界模式附近使用分段导数与局部回退。
|
||||||
|
- [ ] 对自定义组件缺失的导数声明生成明确诊断,不得静默置零。
|
||||||
|
- [ ] 在 `0.68–0.71`、`0.79–0.81`、事件两侧和 `2.00–2.10 s` 检查点执行稠密数值漏边审计与随机方向 JVP。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [x] 历史 external-volume 跨域结构护栏与初始点稠密数值漏边测试继续通过。
|
||||||
|
- [x] callable 接线、分段重置、取消、显式方法隔离、secant 上限和审计失败回退有自动测试。
|
||||||
|
- [ ] `0.81/2.10 s` 的连续状态、事件时刻/顺序、模式和残差满足统一契约;当前 30 色候选未通过。
|
||||||
|
- [ ] 默认候选在锁定环境的 3 次中位墙钟有净收益,小模型无显著退化。
|
||||||
|
- [x] 首批目标切向原语和 6 列通过逐列中心差分、模式分支与局部回退验证。
|
||||||
|
- [ ] 通用组件级解析/半解析导数通过随机方向 JVP、逐列抽查和局部回退验证。
|
||||||
|
|
||||||
|
**风险与回滚**:历史 external-volume 漏边说明“颜色更少”本身不是正确性证据。不同合法颜色组合也可能暴露保守结构中未声明的弱依赖,并改变非光滑接触附近的事件序列。默认保持 `scipy`;`optimized/hybrid` 仅显式实验。非光滑点的解析或 secant 近似未必可靠,事件分段重置、审计和旧路径必须长期保留。
|
||||||
|
|
||||||
|
| 历史实验指标 | SciPy 基线 | 已撤销的 30 色候选 | 验收 |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| 保守结构 / 实际 FD 颜色 | 1284 nnz / 31 | 1284 nnz / 30(seed 54) | 结构不删边 |
|
||||||
|
| 精确装配行 | 0 | 4 条运动学行 | 短变体证明不改变轨迹 |
|
||||||
|
| `0.81 s` Jacobian RHS(含基准) | 估计 8,096 | 实际 7,103 | `-12.3%` |
|
||||||
|
| `0.81 s` `nfev/njev/nlu` | 3763 / 253 / 761 | 3467 / 228 / 670 | 工作量下降 |
|
||||||
|
| `0.81 s` 积分 / 总墙钟 | 59.924 / 61.953 s | 55.034 / 56.957 s | 单次约 `-8.2% / -8.1%` |
|
||||||
|
| `0.81 s` 最大最终状态误差尺度 | 参考 | 51.59 | 未通过 |
|
||||||
|
| `2.10 s` Jacobian RHS(含基准) | 估计 15,584 | 实际 14,556 | `-6.6%` |
|
||||||
|
| `2.10 s` `nfev/njev/nlu` | 6734 / 487 / 1507 | 6606 / 468 / 1469 | 工作量小幅下降 |
|
||||||
|
| `2.10 s` 积分时间 | 122.180 s | 115.928 s | 单次约 `-5.1%` |
|
||||||
|
| `2.10 s` 状态切换 / solver 启动 / 样本 | 2 / 5 / 213 | 4 / 7 / 215 | 未通过 |
|
||||||
|
|
||||||
|
| 最终安全候选指标(`0.81 s`) | SciPy 基线 | seed 0 callable | 验收 |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| 保守结构 / FD 颜色 | 1284 nnz / 31 | 1284 nnz / 31(seed 0) | 相同扰动批次 |
|
||||||
|
| `nfev/njev/nlu` | 3763 / 253 / 761 | 3763 / 253 / 761 | 相同 |
|
||||||
|
| 接受步 / solver 启动 / 状态切换 | 1076 / 3 / 0 | 1076 / 3 / 0 | 相同 |
|
||||||
|
| 压力闭合 | 30,502 | 30,502 | 相同 |
|
||||||
|
| 物理解哈希 | `c6354c97...` | `c6354c97...` | 通过 |
|
||||||
|
| 安全基准 RHS 缓存命中 | 不适用 | 0 / 253 | 无收益,代码已删除 |
|
||||||
|
| 积分 / 探针总墙钟 | 59.924 / 61.953 s | 60.972 / 62.945 s | 略有退化,未通过收益门槛 |
|
||||||
|
|
||||||
|
#### 2026-08-17 / 工作树基于 `6bb0591d`
|
||||||
|
|
||||||
|
- 状态:未开始 → 部分实现(数值接入层完成;30 色候选未通过事件等价,seed 0 候选未通过收益门槛;解析/半解析传播未开始)
|
||||||
|
- 代码备份:`backup/jacobian-before-20260817-6bb0591`,精确指向 `6bb0591d320d0c448ee8d224dd44127bfe3ce00f`。该分支只备份 tracked 代码基线,不包含当时未跟踪的本文档。
|
||||||
|
- 运行环境:Python 3.12.3、NumPy 2.4.6、SciPy 1.17.1;输入 SHA-256 `2fb95e65...`;`2.10 s` 仅内存覆盖停止时间,磁盘 XML 未修改。
|
||||||
|
- 正确性结果:Jacobian 内核、core solver、Generic sparsity 和 Generic XML 共 57 项通过;压力因果、stream 块、机械接触、PNRP17、代数稀疏与方程块另 52 项通过,external-volume 漏边护栏继续通过。热流体闭合计划 13 项中 12 项通过,剩余 1 项因用户已将 fixture 移至 `tests/data/fixtures/`、旧测试仍读取 `tests/fixtures/` 而报既有 `FileNotFoundError`,与本次改动无关。30 色候选在 `2.10 s` 的事件数由 2 变为 4;最终 seed 0 候选在 `0.81 s` 恢复相同物理解哈希与求解统计。
|
||||||
|
- 性能结果:见上表。数字均为同机相邻单次结果,不是 3 次中位数;最终 seed 0 候选没有减少求解工作并略慢。
|
||||||
|
- 卡死结果:历史 30 色探针在 `2.040187 s @ 106.499 s`、`2.051323 s @ 113.996 s`、`2.065299 s @ 115.472 s` 持续推进并完成到 2.10 s;默认 SciPy 的正式探针同样越过 2.05 s 并完成,无重试、无死锁。
|
||||||
|
- 安全收口:默认保持 SciPy;移除多 seed 自动择优、不安全共享缓存和零命中的安全 token 缓存;Jacobian 内部保留取消检查;无信息 Jv 审计强制刷新;显式 solver 不观察或重置 Jacobian。
|
||||||
|
- 决策:保留严格 seed 0 的 callable/诊断/精确行基础和显式实验开关;30 色、基准缓存与 secant 均不进入默认路径。下一阶段优先建立多检查点弱依赖审计和组件级局部导数,不再以颜色数或数值缓存单独作为优化成功标准。
|
||||||
|
- 证据文件:`app/simulation/solvers/jacobian.py`、`app/simulation/solvers/solver.py`、`app/simulation/systems/generic.py`、`tests/test_sparse_secant_jacobian.py`、`tests/test_core_solver.py`、`tests/test_generic_jacobian_sparsity.py`、`tests/test_generic_system_xml_simulation.py`
|
||||||
|
|
||||||
|
#### 2026-08-17 / 首批三活塞半解析 6 列切片
|
||||||
|
|
||||||
|
- 状态:部分实现 → 部分实现(首批目标切片完成并通过局部导数验证;OPT-03 的通用解析/半解析覆盖尚未完成)。
|
||||||
|
- 实现范围:新增 exact-columns subset FD 接口、类型化同次完整数值回退和分段诊断;为三条目标活塞支路编译状态列 `(20, 21, 38, 39, 54, 55)`,沿 34 条可达因果赋值传播切向量,使剩余 FD 颜色从 31 降到 25。
|
||||||
|
- 局部导数:实现 Ideal/PR 介质 `m/U/V` 物性线性化,以及 PNRP、PNCH012、PNL0001、LSTP、MECMAS 的几何、压力、质量/能量、流量/力和接触模式切向原语;原语显式报告 `valid/reason`。
|
||||||
|
- 证明与回退:组件类型、连接拓扑、因果计划、机械组和静态 stream 影响范围必须全部满足编译证明。causal/stream/custom/兼容性证明不成立时不安装 callable,继续使用原生 SciPy;运行点进入非光滑接触边界、临界流动、陈旧 primal 或其他不支持模式时抛出类型化 `ExactColumnsUnavailable`,同一次构建恢复原始 seed 0 完整数值 Jacobian。任何不可证明项都不会静默填 0。
|
||||||
|
- 配置边界:默认仍为 `SIMULATION_ODE_JACOBIAN_MODE=scipy`;首批路径仅通过 `semi-analytic` 显式 opt-in,不替换生产默认值。
|
||||||
|
- 自动测试:focused 套件 86 项、adjacent 套件 164 项,共 250 项通过。热流体 closure 计划另为 12/13 项通过;唯一失败仍是旧测试读取 `tests/fixtures/`、而 fixture 已被用户移至 `tests/data/fixtures/` 导致的既有 `FileNotFoundError`,与本轮 Jacobian 改动无关。
|
||||||
|
- 局部正确性:在平滑检查点,SciPy 分组有限差分漏掉 `J[19,20] ≈ -3201.486`;半解析列相对独立中心差分的最大相对误差为 `1.897e-8`。inactive/active 接触分支、过期 primal、非因果计划和不支持拓扑均覆盖了成功或回退路径。
|
||||||
|
- 轨迹正确性:默认容差下,两条 `0.81 s` 轨迹最差点为 `t=0.65 s` 的能量状态 `state[33]`,原始相对差 `8.24e-5`,缩放误差 `82.36`;事件数和顺序一致,但尚未满足拟定的严格逐点轨迹门槛。提高精度后互差收敛:`rtol=1e-7` 时最大绝对/相对差为 `0.081965 / 1.592e-6`,`rtol=1e-8` 时为 `0.0175357 / 3.09062e-7`,分别缩小约 `4.67× / 5.15×`,且两组事件均一致。这支持“求解路径差异随容差收敛”,但不足以把候选升为默认。
|
||||||
|
- 性能口径:`0.81 s` 已在同机、同一工作树连续完成 3 组相邻 A/B;表中时间为中位数,括号给出 3 次范围。测试使用现有 `/opt/srm-trial-review/.venv`,没有独立预热且依赖版本未由项目锁文件固定,因此仍需在正式锁定环境复核,不能单独作为切换默认值的依据。`2.10 s` 为最终 one-shot primal 捕获版本的单次复跑;此前数学路径相同的预备运行墙钟为 `111.068 s`,本次为 `116.512 s`,长程时间仍需重复测量。
|
||||||
|
|
||||||
|
| 最终 `0.81 s` 三次指标 | SciPy 基线 | `semi-analytic` 候选 | 变化/说明 |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| FD 颜色 / 精确状态列 | 31 / 0 | 25 / 6 | 目标列为 20、21、38、39、54、55 |
|
||||||
|
| `nfev/njev/nlu` | 3763 / 253 / 761 | 3650 / 228 / 711 | 求解工作下降 |
|
||||||
|
| 接受步 / solver 启动 / 状态事件 / 样本 | 1076 / 3 / 0 / 82 | 1056 / 3 / 0 / 82 | 事件和输出网格一致 |
|
||||||
|
| Jacobian RHS | 8,096(估计) | 5,985(实计) | `-26.1%` |
|
||||||
|
| 精确列构建 / 类型化回退 | 不适用 | 224 / 4 | 4 次恢复完整数值构建 |
|
||||||
|
| 压力闭合 | 30,502 | 25,672 | `-15.8%` |
|
||||||
|
| 积分时间中位数(范围) | 59.725 s(59.568–60.188) | 55.631 s(55.432–56.307) | 中位数 `-6.85%` |
|
||||||
|
| 总墙钟中位数(范围) | 61.203 s(61.070–61.704) | 56.708 s(56.508–57.410) | 中位数 `-7.34%`;逐组改善 6.96%–7.47% |
|
||||||
|
|
||||||
|
| 延长至 `2.10 s` 单次指标 | SciPy 基线 | `semi-analytic` 候选 | 变化/说明 |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| 状态 | 完成,越过 2.05 s | 完成,越过 2.05 s | 最终版本越过 2.05 s 的墙钟为 111.660 s |
|
||||||
|
| `nfev/njev/nlu` | 6734 / 487 / 1507 | 6246 / 445 / 1328 | 求解工作下降 |
|
||||||
|
| 接受步 | 1857 | 1753 | `-104` |
|
||||||
|
| solver 启动 / 状态切换 / 样本 | 5 / 2 / 213 | 5 / 2 / 213 | 事件计数和输出网格一致 |
|
||||||
|
| Jacobian RHS | 15,584(估计) | 11,771(实计) | `-24.5%` |
|
||||||
|
| 类型化回退 | 不适用 | 26 | 非平滑/不支持点恢复完整数值构建 |
|
||||||
|
| 压力闭合 | 57,601 | 48,248 | `-16.2%` |
|
||||||
|
| 积分时间 | 122.180 s | 112.825 s | 单次 `-7.7%` |
|
||||||
|
| 总墙钟 | 126.211 s | 116.512 s | 单次 `-7.7%` |
|
||||||
|
|
||||||
|
- 卡死复核:最终 `semi-analytic` 候选在墙钟 `111.660 s` 越过模拟时刻 `2.05 s`,随后于 `116.512 s` 完成到 `2.10 s`;与 SciPy 基线一样未出现无进度死锁。
|
||||||
|
- 未覆盖范围:通用 stream SCC 导数、目标三支路以外的组件/状态列、自定义组件导数契约、正式锁定环境的独立预热复测,以及 `10 s` 长时模式覆盖。
|
||||||
|
- 决策:保留首批半解析切片和自动回退作为显式实验路径;OPT-03 继续为“部分实现”,默认继续使用 SciPy。完成上述通用覆盖、严格轨迹契约和重复基准前,不切换默认值。
|
||||||
|
- 代码备份:仍使用进入 Jacobian 优化前建立的 `backup/jacobian-before-20260817-6bb0591`,精确指向 `6bb0591d320d0c448ee8d224dd44127bfe3ce00f`。
|
||||||
|
- 证据文件:`app/simulation/solvers/jacobian.py`、`app/simulation/solvers/tangent.py`、`app/simulation/solvers/solver.py`、`app/simulation/systems/generic.py`、`app/simulation/core/medium.py`、`app/simulation/components/amesim/media/mediums.py`、`app/simulation/components/amesim/mechanical/pistons.py`、`app/simulation/components/amesim/storage/chambers.py`、`app/simulation/components/amesim/flow/pipes.py`、`app/simulation/components/amesim/mechanical/translational.py`、`tests/test_sparse_secant_jacobian.py`、`tests/test_analytic_tangent_primitives.py`、`tests/test_three_piston_tangent.py`
|
||||||
|
|
||||||
|
### OPT-04 stream 拓扑传播与物性成组复用
|
||||||
|
|
||||||
|
**目标**:让无环 stream 网络一次传播,只对真正的强连通块迭代;同一状态反算的物性量成组计算和复用。
|
||||||
|
|
||||||
|
**当前状态**:stream 求解器已预绑定组件、端口和连接,物性层也有单次运行精确缓存;但每次求解仍构造临时字典/列表、重复调用连接焓计算,尚未编译 SCC/DAG。热流体外层固定点最多 25 次,本模型实测最多 3 次。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 构建 stream 图的 SCC,并将缩点图编译为拓扑顺序。
|
||||||
|
- [ ] 对单节点和无环段使用一次传播,仅在循环 SCC 内迭代。
|
||||||
|
- [ ] 使用预分配数组和原地误差统计,避免每轮临时字典/列表。
|
||||||
|
- [ ] 缓存同一求解阶段的连接焓结果,避免返回前重复计算。
|
||||||
|
- [ ] 将 `p/T/rho/h/s` 等同源物性组织为状态包,按精确输入键成组复用。
|
||||||
|
- [ ] 增加缓存命中、SCC 迭代、失效原因和物性调用次数指标。
|
||||||
|
- [ ] 评估脏标记传播,但必须证明事件和反向流切换时不会复用陈旧值。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 无环、单环、多环、反向流和事件后拓扑测试全部通过。
|
||||||
|
- [ ] 复杂模型的最大 stream/热流体迭代不增加,残差不恶化。
|
||||||
|
- [ ] 量化减少物性调用、临时分配、压力闭合或 RHS 时间。
|
||||||
|
|
||||||
|
| 指标 | 当前 | 完成后 |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| stream 块 / 未知量 | 9 / 192 | 待填 |
|
||||||
|
| 最大热流体迭代 | 3 | 待填 |
|
||||||
|
| `2.10 s` 压力闭合 | 57,601 | 待填 |
|
||||||
|
| 物性调用 / 缓存命中率 | 待测 | 待填 |
|
||||||
|
|
||||||
|
### OPT-05 状态缩放、分量容差和步长策略
|
||||||
|
|
||||||
|
**目标**:减少量纲差异造成的不必要小步和 Jacobian 重建,同时维持事件与守恒精度。
|
||||||
|
|
||||||
|
**当前状态**:模型中不同物理量的量级差异大。历史试验显示机械绝对容差放宽可能带来约 16% 收益,但属于精度策略变化;热流体固定点容差的简单放宽曾使表现变差,不能直接采用。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 按状态物理量、标称值和工程容差建立分量 `atol`/缩放规则。
|
||||||
|
- [ ] 为未提供标称值的组件定义安全默认值并输出诊断。
|
||||||
|
- [ ] 分开积分误差、代数残差、stream 固定点和事件定位容差。
|
||||||
|
- [ ] 统计限制步长的状态分量、误差拒步和 Jacobian 重建原因。
|
||||||
|
- [ ] 对事件前后、接触临界区和稳态区分别评估步长上限策略。
|
||||||
|
- [ ] 建立严格/标准/快速配置,但默认配置必须有明确精度契约。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 每个配置都有状态、事件、残差和守恒误差界限。
|
||||||
|
- [ ] 标准配置在复杂模型上减少拒步或分解工作,不引入模式遗漏。
|
||||||
|
- [ ] 所有收益报告同时给出误差变化,禁止只报告墙钟。
|
||||||
|
|
||||||
|
### OPT-06 事件检测与 dense output 按需化
|
||||||
|
|
||||||
|
**目标**:避免在绝大多数没有事件候选、也不跨输出采样点的接受步上创建 dense output。
|
||||||
|
|
||||||
|
**当前状态**:已有事件候选筛选和部分非事件优化,但只要存在状态转换处理器,接受步仍可能构造 dense output。`2.10 s` 有 1857 个接受步而只有 2 次状态切换,存在减少插值构造的空间。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 在构造 dense output 前执行低成本端点符号/模式候选检查。
|
||||||
|
- [ ] 仅在跨输出采样点或存在事件候选时创建插值器。
|
||||||
|
- [ ] 将输出插值与事件定位的生命周期和精度需求分离。
|
||||||
|
- [ ] 统计候选数、误报数、定位次数、dense output 构造数和耗时。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 同时事件、擦边事件、抖动防护和多模式顺序测试通过。
|
||||||
|
- [ ] 事件时刻误差不超契约,事件顺序和最终模式不变。
|
||||||
|
- [ ] 完整模型 dense output 构造数与耗时明显下降。
|
||||||
|
|
||||||
|
### OPT-07 输出、后处理和传输内存优化
|
||||||
|
|
||||||
|
**目标**:在长仿真中控制结果生成、JSON 编码、前端复制和峰值内存。
|
||||||
|
|
||||||
|
**当前状态**:当前模型有 1,021 个结果变量;`10 s / 0.01 s` 约产生 1,022,021 个标量。现路径会对每个样本重新闭合、追加全部结果,并把完整结果作为一个 NDJSON 消息发送。它不是本次 2.05 s 慢推进的主因,但会成为长时间运行的显著成本。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 支持结果变量白名单、分组和按需派生量。
|
||||||
|
- [ ] 将积分内部采样、结果存储采样和显示采样分离。
|
||||||
|
- [ ] 对显示路径提供服务端降采样,同时保留可选完整数据模式。
|
||||||
|
- [ ] 分块编码和传输结果,或返回 `resultId` 后分页/流式获取。
|
||||||
|
- [ ] 评估前端 TypedArray/列式数据,减少嵌套对象和重复复制。
|
||||||
|
- [ ] 避免后处理中对每个样本重复执行不必要的完整闭合。
|
||||||
|
- [ ] 记录原始标量数、编码/传输字节数、后处理时间和峰值 RSS。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 完整输出模式保持现有 API 契约,或通过显式版本升级迁移。
|
||||||
|
- [ ] 精简模式的变量选择和降采样行为可预测、可测试。
|
||||||
|
- [ ] `10 s` 基准中后处理时间、传输字节和峰值 RSS 有量化改善。
|
||||||
|
|
||||||
|
### OPT-08 进度、取消和服务并发鲁棒性
|
||||||
|
|
||||||
|
**目标**:区分“内部慢步”和“真正无进度”,并让长任务可取消、可限流、不会拖垮服务进程。
|
||||||
|
|
||||||
|
**当前状态**:已有 stream 进度和取消检查;前端无进度阈值约 60 s。本次 2.05 s 附近可见最大间隔约 7.5 s,且中间有接受步与 CPU 活动,因此没有触发真实无进度条件。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 分别上报模拟时间、接受步、内部 RHS/闭合活动和墙钟心跳。
|
||||||
|
- [ ] 将“运行中但步很慢”与“求解器无活动”使用不同状态和超时策略。
|
||||||
|
- [ ] 在代数闭合、stream 迭代、Jacobian 构建和后处理内加入有界取消检查。
|
||||||
|
- [ ] 限制并发仿真 worker、队列长度和单任务 CPU/内存预算。
|
||||||
|
- [ ] 超时报告最后活动阶段、模拟时刻、步长和关键计数,而非只返回通用错误。
|
||||||
|
- [ ] 添加故意慢 RHS、死循环防护、客户端断连和多任务竞争测试。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 正常慢步不会被误判为死锁,真实无活动能在约定时间内终止并给出诊断。
|
||||||
|
- [ ] 取消请求在每个主要阶段都能在有界时间内生效。
|
||||||
|
- [ ] 并发压力下服务仍能响应健康检查和新请求拒绝/排队逻辑。
|
||||||
|
|
||||||
|
### OPT-09 建立 10 s 长时验证与模式覆盖
|
||||||
|
|
||||||
|
**目标**:用实测替代“0.81 s 或 2.10 s 可以外推到 10 s”的假设。
|
||||||
|
|
||||||
|
**当前状态**:`2.10 s` 已成功;`10 s` 尚未运行和建立资源预算。模型可能在后续出现新的事件、模式、接触切换或数值尺度问题。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 在正式锁定环境运行未优化基线 `10 s`,设置心跳、资源上限和可恢复日志。
|
||||||
|
- [ ] 保存事件、模式、步长、拒步、Jacobian、闭合和内存随模拟时间的时间线。
|
||||||
|
- [ ] 为长跑设置阶段性检查点,支持定位首次偏差而非只比较终点。
|
||||||
|
- [ ] 将每项 P1 优化分别加入 `10 s` A/B,不把多个改动混成一个结果。
|
||||||
|
- [ ] 根据首次基线制定合理的 CI 频率和资源门槛。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 连续 3 次完成 `10 s`,没有无解释回退、NaN/Inf 或资源失控。
|
||||||
|
- [ ] 全程模式、事件、关键状态和守恒量满足契约。
|
||||||
|
- [ ] 可从日志快速判断任何慢区属于积分、Jacobian、闭合、事件还是输出。
|
||||||
|
|
||||||
|
### OPT-10 明确高指数 DAE 和强非光滑系统边界
|
||||||
|
|
||||||
|
**目标**:明确当前通用求解能力的工程边界,并决定是否值得引入真正的 DAE/互补问题求解器。
|
||||||
|
|
||||||
|
**当前状态**:当前架构更适合结构明确、可唯一闭合、状态较连续的规则 index-1 类系统。超硬非光滑接触、临界抖动、近奇异代数系统、更高指数 DAE 和依赖声明不完整的自定义组件仍是薄弱点。
|
||||||
|
|
||||||
|
**工作项**:
|
||||||
|
|
||||||
|
- [ ] 建立小型基准族:刚性接触、反复开闭、近奇异闭合、尺度跨越、自定义漏依赖和 index-2/3 示例。
|
||||||
|
- [ ] 对每类系统定义“支持”“降级支持”“明确拒绝”,并给出诊断。
|
||||||
|
- [ ] 评估质量矩阵 DAE、指数约简、互补/半光滑方法与现有架构的成本。
|
||||||
|
- [ ] 只有真实模型需求和基准证明必要时,才启动通用 DAE 后端项目。
|
||||||
|
|
||||||
|
**验收条件**:
|
||||||
|
|
||||||
|
- [ ] 文档与运行时错误能明确说明能力边界,不出现静默错误。
|
||||||
|
- [ ] 若启动新后端,有独立设计、基准和迁移计划,不与普通 RHS 性能优化混合。
|
||||||
|
|
||||||
|
## 6. 统一回归矩阵
|
||||||
|
|
||||||
|
| 场景 | 结构 | 数值状态 | 事件/模式 | 回退 | 性能 | 长时内存 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 小型线性组件 | 必测 | 必测 | 不适用 | 必测 | 冒烟 | 不适用 |
|
||||||
|
| 非线性压力/流量 | 必测 | 必测 | 可选 | 必测 | 必测 | 可选 |
|
||||||
|
| stream 无环/成环/反向流 | 必测 | 必测 | 必测 | 必测 | 必测 | 可选 |
|
||||||
|
| 接触与模式切换 | 必测 | 必测 | 必测 | 必测 | 必测 | 可选 |
|
||||||
|
| 自定义组件与漏依赖 | 必测 | 必测 | 可选 | 必测 | 可选 | 不适用 |
|
||||||
|
| 本文复杂 XML `0.81 s` | 必测 | 必测 | 必测 | 必测 | 必测 | 必测 |
|
||||||
|
| 本文复杂 XML `2.10 s` | 必测 | 必测 | 必测 | 必测 | 必测 | 必测 |
|
||||||
|
| 本文复杂 XML `10 s` | 必测 | 必测 | 必测 | 必测 | 必测 | 必测 |
|
||||||
|
|
||||||
|
当前相关回归套件包括:
|
||||||
|
|
||||||
|
- `tests/test_sparse_secant_jacobian.py`
|
||||||
|
- `tests/test_generic_jacobian_sparsity.py`
|
||||||
|
- `tests/test_pressure_flow_causal_execution.py`
|
||||||
|
- `tests/test_stream_pressure_block_solver.py`
|
||||||
|
- `tests/test_core_solver.py`
|
||||||
|
|
||||||
|
这些测试目前覆盖部分关键机制,但不能替代复杂 XML 的端到端数值和长时回归。
|
||||||
|
|
||||||
|
## 7. 单项更新模板
|
||||||
|
|
||||||
|
完成一个原型或 PR 后,在对应任务下追加以下记录:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
#### YYYY-MM-DD / <commit-or-branch>
|
||||||
|
|
||||||
|
- 状态:未开始 → 进行中 / 部分实现 → 已完成
|
||||||
|
- 实现范围:
|
||||||
|
- 未覆盖范围:
|
||||||
|
- 运行环境:
|
||||||
|
- 输入与配置:
|
||||||
|
- 正确性结果:
|
||||||
|
- 性能结果(中位数与离散度):
|
||||||
|
- 回退/审计结果:
|
||||||
|
- 风险或已知退化:
|
||||||
|
- 决策:合入默认路径 / 继续实验 / 回滚 / 不采用
|
||||||
|
- 证据文件或 CI 链接:
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 总体更新记录
|
||||||
|
|
||||||
|
| 日期 | 代码/分支 | 任务 | 变化 | 正确性 | 性能 | 决策 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-08-17 | `6bb0591d` | 基线 | 原始 `0.81 s` 完成;内存延长 `2.10 s` 完成并越过 2.05 s | 无卡死;当前环境哈希与历史不同,待正式环境复核 | 63.779 s / 126.211 s(单次) | 建立任务清单,先完成 OPT-00 |
|
||||||
|
| 2026-08-17 | 工作树基于 `6bb0591d`;备份 `backup/jacobian-before-20260817-6bb0591` | OPT-03 | callable sparse Jacobian、真实计数、分段重置、取消、严格 seed 0 与实验 secant | 121 项相关测试通过;另 1 项既有 fixture 路径错误;30 色候选事件不等价,seed 0 候选恢复相同哈希 | 30 色历史候选有收益但不正确;seed 0 候选略慢且缓存 0 命中 | 默认 SciPy;移除多 seed/缓存;保留接入基础;解析/半解析继续后续 |
|
||||||
|
| 2026-08-17 | 工作树基于 `6bb0591d`;同一备份分支 | OPT-03 首批半解析切片 | exact-columns subset FD、类型化回退/诊断、三活塞 6 列与 34 条因果赋值;31→25 个 FD 颜色;新增 Ideal/PR、PNRP、PNCH012、PNL0001、LSTP、MECMAS 切向原语 | focused 86 + adjacent 164 = 250 项通过;closure 12/13,唯一失败为既有 fixture 路径;局部列对中心 FD 最大相对误差 `1.897e-8`;默认容差轨迹仍超严格逐点门槛,但随 rtol 收紧约 4.67×/5.15× 收敛且事件一致 | `0.81 s` 三次墙钟中位数 61.203→56.708 s,Jac RHS 8096(估计)→5985(实计);最终 `2.10 s` 单次 126.211→116.512 s,正常越过 2.05 s,事件/启动/样本均与基线一致 | 首批目标切片完成,OPT-03 总体仍部分实现;默认 SciPy,`semi-analytic` 显式 opt-in;待通用 stream/其余列、正式锁定环境独立预热和 10 s 验证 |
|
||||||
|
| 2026-08-17 | 同一 OPT-03 工作树;3 组相邻 A/B | OPT-03 重复性能复核 | 原始 `0.81 s`,每组先 SciPy 后 `semi-analytic`,运行期间无并发仿真负载 | 三组求解统计、哈希、事件和输出网格各自完全稳定;Jacobian RHS 8096(估计)→5985(实计) | 总墙钟中位数 61.203→56.708 s(`-7.34%`),积分中位数 59.725→55.631 s(`-6.85%`) | 保持显式 opt-in;仍需正式锁定环境独立预热、严格轨迹契约和 10 s 验证 |
|
||||||
|
|
||||||
|
## 9. 相关文档
|
||||||
|
|
||||||
|
- [后端求解逻辑与效率优化调研](./后端求解逻辑与效率优化调研.md)
|
||||||
|
- [仿真性能评估-2026-08-15](./仿真性能评估-2026-08-15.md)
|
||||||
|
- [文档目录说明](../README.md)
|
||||||
+1
-1
@@ -111,7 +111,7 @@ System XML v3 是当前唯一支持的 XML 求解输入。根元素固定使用
|
|||||||
|
|
||||||
`modelVersion` 必须与当前注册模型完全一致。版本不一致时返回
|
`modelVersion` 必须与当前注册模型完全一致。版本不一致时返回
|
||||||
`COMPONENT_MODEL_VERSION_MISMATCH`,不会静默使用当前模型解释旧输入。完整结构见
|
`COMPONENT_MODEL_VERSION_MISMATCH`,不会静默使用当前模型解释旧输入。完整结构见
|
||||||
`docs/system-xml-v3.md` 和 `schemas/system-simulation-v3.xsd`。
|
`docs/standard/system-xml-v3.md` 和 `schemas/system-simulation-v3.xsd`。
|
||||||
|
|
||||||
## 6. HTTP API
|
## 6. HTTP API
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ class ExampleComponent(Component):
|
|||||||
7. 模型的方程不能依赖图标方向、界面分类或画布位置。
|
7. 模型的方程不能依赖图标方向、界面分类或画布位置。
|
||||||
|
|
||||||
完整方程示例参见
|
完整方程示例参见
|
||||||
[`app/simulation/components/example.md`](../app/simulation/components/example.md)。
|
[`app/simulation/components/example.md`](../../app/simulation/components/example.md)。
|
||||||
|
|
||||||
## 7. 界面显示声明
|
## 7. 界面显示声明
|
||||||
|
|
||||||
+9
-9
@@ -47,7 +47,7 @@
|
|||||||
应放在对应 `examples/` 或专用系统目录,不能与公开模型混放后依赖扫描规则排除。
|
应放在对应 `examples/` 或专用系统目录,不能与公开模型混放后依赖扫描规则排除。
|
||||||
|
|
||||||
当前示例是
|
当前示例是
|
||||||
[`app/simulation/examples/testmodel/dynamic_pipe.py`](../app/simulation/examples/testmodel/dynamic_pipe.py)。
|
[`app/simulation/examples/testmodel/dynamic_pipe.py`](../../app/simulation/examples/testmodel/dynamic_pipe.py)。
|
||||||
|
|
||||||
### 2.4 新增物理域
|
### 2.4 新增物理域
|
||||||
|
|
||||||
@@ -70,11 +70,11 @@
|
|||||||
1. 本文档。
|
1. 本文档。
|
||||||
2. 目标库的 `library.py`。
|
2. 目标库的 `library.py`。
|
||||||
3. 同分类中物理行为最接近的现有模型。
|
3. 同分类中物理行为最接近的现有模型。
|
||||||
4. [`core/base.py`](../app/simulation/core/base.py)。
|
4. [`core/base.py`](../../app/simulation/core/base.py)。
|
||||||
5. [`core/ports.py`](../app/simulation/core/ports.py)。
|
5. [`core/ports.py`](../../app/simulation/core/ports.py)。
|
||||||
6. [`core/metadata.py`](../app/simulation/core/metadata.py)。
|
6. [`core/metadata.py`](../../app/simulation/core/metadata.py)。
|
||||||
7. [`core/catalog.py`](../app/simulation/core/catalog.py)。
|
7. [`core/catalog.py`](../../app/simulation/core/catalog.py)。
|
||||||
8. [`registry.py`](../app/simulation/registry.py) 中的启动校验。
|
8. [`registry.py`](../../app/simulation/registry.py) 中的启动校验。
|
||||||
9. 与目标模型最接近的测试。
|
9. 与目标模型最接近的测试。
|
||||||
|
|
||||||
不要只根据文件名、前端图标或旧 XML 猜测模型语义。
|
不要只根据文件名、前端图标或旧 XML 猜测模型语义。
|
||||||
@@ -564,11 +564,11 @@ class ExampleRestriction(AlgebraicComponent):
|
|||||||
真实现有模型可参考:
|
真实现有模型可参考:
|
||||||
|
|
||||||
- 储能元件:
|
- 储能元件:
|
||||||
[`cylinder.py`](../app/simulation/components/experimental/storage/cylinder.py)
|
[`cylinder.py`](../../app/simulation/components/experimental/storage/cylinder.py)
|
||||||
- 阻性元件:
|
- 阻性元件:
|
||||||
[`orifice.py`](../app/simulation/components/experimental/flow/orifice.py)
|
[`orifice.py`](../../app/simulation/components/experimental/flow/orifice.py)
|
||||||
- 多端口连接元件:
|
- 多端口连接元件:
|
||||||
[`tee.py`](../app/simulation/components/experimental/junctions/tee.py)
|
[`tee.py`](../../app/simulation/components/experimental/junctions/tee.py)
|
||||||
|
|
||||||
## 14. 注册模型
|
## 14. 注册模型
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
System XML v3 是 SystemSimulationApp 当前唯一的 XML 求解输入格式。它只描述可执行模型,不再承担 ReactFlow 画布存档职责。
|
System XML v3 是 SystemSimulationApp 当前唯一的 XML 求解输入格式。它只描述可执行模型,不再承担 ReactFlow 画布存档职责。
|
||||||
|
|
||||||
机器可读结构见 [`schemas/system-simulation-v3.xsd`](../schemas/system-simulation-v3.xsd)。当前校验、解析、编译和仿真接口固定按 v3 处理,不会根据 `schemaVersion` 自动切换到 v1 或 v2。
|
机器可读结构见 [`schemas/system-simulation-v3.xsd`](../../schemas/system-simulation-v3.xsd)。当前校验、解析、编译和仿真接口固定按 v3 处理,不会根据 `schemaVersion` 自动切换到 v1 或 v2。
|
||||||
|
|
||||||
## 1. 设计边界
|
## 1. 设计边界
|
||||||
|
|
||||||
File renamed without changes.
@@ -0,0 +1,53 @@
|
|||||||
|
# 更新日志 2026-08-17
|
||||||
|
|
||||||
|
## 11:15
|
||||||
|
|
||||||
|
- 复测复杂系统 XML:磁盘文件的终止时间为 `0.81 s`,原样运行正常完成;仅在内存中延长至 `2.10 s` 后也正常越过此前报告的 `2.05 s` 慢区并结束,没有发生重试、回退或无进度卡死。
|
||||||
|
- 性能分析确认约 97% 时间消耗在积分阶段,有限差分 Jacobian 估计约占 RHS 工作量的 69%;后续优化重点确定为半解析 Jacobian、扁平数值中间表示和 stream/物性传播,而不是继续优先优化未触发的全局非线性求解。
|
||||||
|
- Jacobian、因果快路径、stream 方程块和逐步积分相关的 44 项回归测试通过;`10 s` 长时运行尚未验证,不能由 `2.10 s` 结果外推保证。
|
||||||
|
|
||||||
|
## 11:18
|
||||||
|
|
||||||
|
- 重构 `docs/README.md`,明确 `update-log/`、`standard/`、`other/` 三类目录职责,并建立按日期命名、按实际完成时间分段的更新日志规范与模板。
|
||||||
|
|
||||||
|
## 11:28
|
||||||
|
|
||||||
|
- 完成求解器架构复核:当前 472 个代数未知量已形成覆盖 200 个 flow/force 未知量的因果赋值计划,复杂模型运行中因果快路径审计没有失败或回退;确认下一阶段应在现有计划之上建设可求导、可数组化的数值内核,而不是重复实现因果求解器。
|
||||||
|
|
||||||
|
## 11:30
|
||||||
|
|
||||||
|
- 精简文档索引,删除容易过期的专题清单、代码入口和单日日志链接,只保留目录职责与更新日志规范;明确历史工作缺少准确完成时间时不得猜测补写。
|
||||||
|
|
||||||
|
## 11:35
|
||||||
|
|
||||||
|
- 新增 `docs/other/求解器性能优化任务清单.md`,记录 `0.81 s`、`2.10 s` 基线,建立 OPT-00~OPT-10 优化任务、验收标准、回滚要求、性能对比口径以及 `10 s` 长时验证计划。
|
||||||
|
|
||||||
|
## 11:38
|
||||||
|
|
||||||
|
- 修复文档重新分类产生的 18 条 Markdown 断链、4 处旧路径文字和 1 处易失效的行号引用;保留历史日志原文不变,全仓本地 Markdown 相对链接复扫为 0 条断链,`git diff --check` 通过。
|
||||||
|
|
||||||
|
## 15:04
|
||||||
|
|
||||||
|
- 新增 callable 稀疏 Jacobian 数值层,并为 BDF/Radau 接入真实 RHS 计数、求解分段重建、取消检查、诊断统计和类型化完整数值回退;显式积分器继续忽略 Jacobian。
|
||||||
|
- 完成三条活塞支路 6 个机械状态列的首批半解析传播,覆盖 34 条因果赋值,并增加 Ideal/Peng–Robinson 介质及 PNRP17、PNCH012、PNL0001、LSTP00A、MECMAS21 等组件的局部切向原语;有限差分颜色数由 31 降至 25。
|
||||||
|
- 新路径仅在 `SIMULATION_ODE_JACOBIAN_MODE=semi-analytic` 时显式启用,默认仍使用 SciPy;无法证明拓扑、进入非光滑边界或局部导数不可用时恢复完整数值 Jacobian,不会把未知导数静默填为零。
|
||||||
|
- 相关测试共 250 项通过;局部半解析列相对独立中心差分的最大相对误差为 `1.897e-8`。另有一项热流体闭合测试因测试资源已移动而仍读取旧路径失败,与本次 Jacobian 修改无关。
|
||||||
|
- `0.81 s` 三组同机对比中,实验路径总墙钟中位数由 `61.203 s` 降至 `56.708 s`,Jacobian RHS 减少约 26.1%;`2.10 s` 单次由 `126.211 s` 降至 `116.512 s`,事件、启动和样本数量保持一致并正常越过 `2.05 s`。
|
||||||
|
- 默认容差下两条路径尚未满足拟定的严格逐点轨迹门槛,且通用 stream 导数、自定义组件、正式锁定环境复测和 `10 s` 长时覆盖尚未完成,因此 OPT-03 仍为部分实现,没有切换生产默认路径。
|
||||||
|
|
||||||
|
## 15:14
|
||||||
|
|
||||||
|
- 将后端、前端和一键启动入口统一到 `bat/` 目录,保留三组 Windows `.bat` 脚本,并新增一一对应的 Linux `.sh` 脚本;删除仓库根目录和 `frontend/` 下已被替代的旧启动入口。
|
||||||
|
- Windows 脚本改为从自身位置解析仓库路径,分别使用 `.venv-win` 和仓库内兼容的便携 Node.js;Linux 脚本使用 `.venv`,并优先选择 `.tools/node-*-linux-x64`,缺少环境、依赖或兼容 Node.js 时会给出明确提示。
|
||||||
|
- Linux 一键启动脚本可在同一终端管理 FastAPI 与 Vite;收到 `Ctrl+C`、终止信号或任一服务退出时会清理两个进程组,超时后仅强制结束尚未退出的进程,避免遗留后台服务。
|
||||||
|
- 新增脚本换行规则并设置 Linux 脚本可执行权限;本地 Linux Node.js 运行时加入忽略规则,避免 `.venv`、`node_modules` 和便携工具污染版本库。
|
||||||
|
- 根 `README.md` 补充 Windows/Linux 环境准备、Node.js 版本要求、六个启动命令、服务地址及停止方式,相关技术文档中的旧启动路径同步为 `bat/` 下的新入口。
|
||||||
|
- 完成本机开发环境准备:创建 Python `.venv` 并按 `requirements.txt` 安装依赖,`pip check` 无冲突;安装项目本地 Node.js `v24.18.0`、npm `11.16.0`,并通过 `npm ci` 安装前端依赖。
|
||||||
|
- Linux 脚本语法、启动和进程清理验证通过;FastAPI `8000` 与 Vite `5173` 均返回 HTTP 200,测试结束后端口无残留监听。Windows 脚本已完成静态检查,尚未在 Windows 实机运行。
|
||||||
|
- npm 审计报告 2 个高危依赖问题;本次未执行可能改变锁定依赖版本的自动修复,留待单独评估处理。
|
||||||
|
|
||||||
|
## 15:27
|
||||||
|
|
||||||
|
- 根据同日其他项目会话的最终记录补全上述文档管理、性能诊断和求解器优化工作,并与当前源码、测试及优化任务账本交叉核对。
|
||||||
|
- 在当前项目 `.venv` 中重新运行 Jacobian、切向原语、三活塞、core solver、稀疏结构和 XML 仿真的定向测试,共 86 项全部通过;`git diff --check` 通过。
|
||||||
|
- 热流体闭合套件当前仍为 12/13,通过项不受影响;唯一失败是测试继续读取已经移动的旧 fixture 路径。测试资源移动属于用户操作,本日志未将其计入其他会话的完成成果。
|
||||||
@@ -1 +0,0 @@
|
|||||||
powershell -ExecutionPolicy Bypass -File "%~dp0start-dev.ps1"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
||||||
Set-Location $PSScriptRoot
|
|
||||||
$nodeDir = Get-ChildItem -Path (Join-Path $repoRoot ".tools") -Directory -Filter "node-*-win-x64" |
|
|
||||||
Sort-Object Name -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
if (-not $nodeDir) {
|
|
||||||
throw "Node.js portable runtime was not found under .tools."
|
|
||||||
}
|
|
||||||
|
|
||||||
$env:Path = "$($nodeDir.FullName);$env:Path"
|
|
||||||
& (Join-Path $nodeDir.FullName "npm.cmd") run dev -- --strictPort
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
|
|
||||||
cd /d "%~dp0"
|
|
||||||
title SystemSimulationApp Launcher
|
|
||||||
|
|
||||||
if not exist "%~dp0start-backend.bat" (
|
|
||||||
echo [ERROR] start-backend.bat was not found.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if not exist "%~dp0start-reactflow.bat" (
|
|
||||||
echo [ERROR] start-reactflow.bat was not found.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Starting FastAPI and ReactFlow in separate windows...
|
|
||||||
|
|
||||||
start "FastAPI - 127.0.0.1:8000" "%ComSpec%" /d /c call "%~dp0start-backend.bat"
|
|
||||||
start "ReactFlow - 127.0.0.1:5173" "%ComSpec%" /d /c call "%~dp0start-reactflow.bat"
|
|
||||||
|
|
||||||
exit /b 0
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
|
|
||||||
title SystemSimulationApp ReactFlow - 127.0.0.1:5173
|
|
||||||
|
|
||||||
set "FRONTEND_DIR=%~dp0frontend"
|
|
||||||
set "START_SCRIPT=%FRONTEND_DIR%\start-dev.bat"
|
|
||||||
|
|
||||||
if not exist "%START_SCRIPT%" (
|
|
||||||
echo [ERROR] ReactFlow start script was not found:
|
|
||||||
echo %START_SCRIPT%
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Starting ReactFlow at http://127.0.0.1:5173
|
|
||||||
echo Press Ctrl+C to stop the service.
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%FRONTEND_DIR%"
|
|
||||||
call "%START_SCRIPT%"
|
|
||||||
set "EXIT_CODE=%ERRORLEVEL%"
|
|
||||||
|
|
||||||
if not "%EXIT_CODE%"=="0" (
|
|
||||||
echo.
|
|
||||||
echo [ERROR] ReactFlow exited with code %EXIT_CODE%.
|
|
||||||
pause
|
|
||||||
)
|
|
||||||
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
File renamed without changes.
File diff suppressed because it is too large.
Load diff
File renamed without changes.
File renamed without changes.
|
500 Internal Server Error
An error occurred: An error occurred Gitea Version: 28.0.0 |