完善 PNL00R 摩擦模型与八路回归

This commit is contained in:
huojiarong committed 2026-08-18 09:30:29 +00:00
1 parent 684d28752a
commit f725f038b6
10 files changed
+492 -92

No files matched your search

+56 -17
View File
@@ -54,6 +54,22 @@ class Pnl0001DerivativeLinearization:
_MAX_REPORTED_FRICTION_FACTOR = 64_000_000.0 _MAX_REPORTED_FRICTION_FACTOR = 64_000_000.0
# ``pn2pipefr`` blends the laminar and turbulent friction branches over a
# much wider Reynolds range than the former hard 2300..4000 interpolation.
# These constants are a uniform-Re least-squares fit to the unchanged
# Simcenter Amesim 2404 test_mql result for PNL00R/PNL0001/PNL0003 at both
# rr=0.045/14 and rr=0.045/20. The shifted Hill exponent is greater than 2,
# so its zero-valued join to the laminar branch is C2 continuous.
_PN2PIPEFR_TRANSITION_START_REYNOLDS = 89.96829989
_PN2PIPEFR_TRANSITION_SCALE_REYNOLDS = 2741.96700831
_PN2PIPEFR_TRANSITION_SHARPNESS = 8.37293695
# PNL00R keeps the exact closed-form laminar solve where the fitted
# transition contribution is below four parts per million. Extending this
# shortcut to Re=2300 bypassed the real AMESim transition and introduced a
# solver-visible branch point.
_PN2PIPEFR_ANALYTIC_LAMINAR_MAX_REYNOLDS = 1000.0
def _reported_friction_factor(value: float) -> float: def _reported_friction_factor(value: float) -> float:
return min(float(value), _MAX_REPORTED_FRICTION_FACTOR) return min(float(value), _MAX_REPORTED_FRICTION_FACTOR)
@@ -75,8 +91,8 @@ class AmesimPnl00r(AlgebraicComponent):
"""AMESim PNL00R pneumatic pipe friction resistance. """AMESim PNL00R pneumatic pipe friction resistance.
The public model exposes the AMESim PNL00R catalog/XML contract and uses The public model exposes the AMESim PNL00R catalog/XML contract and uses
an auditable Darcy-Weisbach resistance with Reynolds/roughness-dependent the `pn2pipefr` compressible gas relation with a Reynolds/roughness-
friction. Exact `pn2pipefr_` parity is left for the later model tuning pass. dependent equivalent flow coefficient.
""" """
MODEL_TYPE = "amesim_pnl00r" MODEL_TYPE = "amesim_pnl00r"
@@ -244,7 +260,7 @@ class AmesimPnl00r(AlgebraicComponent):
if reynolds_number <= 0.0: if reynolds_number <= 0.0:
return 64_000_000.0 return 64_000_000.0
laminar = 64.0 / reynolds_number laminar = 64.0 / reynolds_number
if reynolds_number <= 2300.0: if reynolds_number <= _PN2PIPEFR_TRANSITION_START_REYNOLDS:
return laminar return laminar
# pn2pipefr does not apply the fully rough correction at every # pn2pipefr does not apply the fully rough correction at every
@@ -274,10 +290,13 @@ class AmesimPnl00r(AlgebraicComponent):
turbulent = smooth_turbulent + roughness_weight * ( turbulent = smooth_turbulent + roughness_weight * (
fully_rough - smooth_turbulent fully_rough - smooth_turbulent
) )
if reynolds_number >= 4000.0: transition_coordinate = (
return turbulent (reynolds_number - _PN2PIPEFR_TRANSITION_START_REYNOLDS)
fraction = (reynolds_number - 2300.0) / 1700.0 / _PN2PIPEFR_TRANSITION_SCALE_REYNOLDS
return laminar + fraction**0.58 * (turbulent - laminar) )
transition_power = transition_coordinate**_PN2PIPEFR_TRANSITION_SHARPNESS
transition_weight = transition_power / (1.0 + transition_power)
return laminar + transition_weight * (turbulent - laminar)
def darcy_pressure_drop( def darcy_pressure_drop(
self, self,
@@ -329,13 +348,17 @@ class AmesimPnl00r(AlgebraicComponent):
if isclose(p_1, p_2, rel_tol=0.0, abs_tol=1.0e-8): if isclose(p_1, p_2, rel_tol=0.0, abs_tol=1.0e-8):
return 0.0 return 0.0
pressure_difference = p_1 - p_2 pressure_difference = p_1 - p_2
upstream_pressure = max(p_1, p_2, 1.0) upstream_temperature = self._port_temperature(
upstream_temperature = self._port_temperature("port_1" if pressure_difference > 0.0 else "port_2") "port_1" if pressure_difference > 0.0 else "port_2"
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12) )
magnitude = self._mass_flow_for_pressure_drop( magnitude = AmesimPnl0001._one_way_pn2pipefr_mass_flow(
abs(pressure_difference), self,
density=density, upstream_pressure=max(p_1, p_2),
temperature=upstream_temperature, downstream_pressure=min(p_1, p_2),
upstream_temperature=upstream_temperature,
resistance_length=self.le,
max_iterations=64,
analytic_laminar=True,
) )
return magnitude if pressure_difference > 0.0 else -magnitude return magnitude if pressure_difference > 0.0 else -magnitude
@@ -347,17 +370,19 @@ class AmesimPnl00r(AlgebraicComponent):
) )
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12) density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
reynolds = self.reynolds_number(m_flow, upstream_temperature) reynolds = self.reynolds_number(m_flow, upstream_temperature)
friction = self.friction_factor(reynolds)
flow_coefficient = sqrt(self.diam / (self.le * friction))
velocity = m_flow / (density * self.area) velocity = m_flow / (density * self.area)
cm = ( cm = (
abs(m_flow) abs(m_flow)
* sqrt(upstream_temperature) * sqrt(upstream_temperature)
/ max(self.area * upstream_pressure, 1.0e-18) / max(flow_coefficient * self.area * upstream_pressure, 1.0e-18)
) )
return { return {
"re": reynolds, "re": reynolds,
"cm": cm, "cm": cm,
"v": velocity, "v": velocity,
"ff": _reported_friction_factor(self.friction_factor(reynolds)), "ff": _reported_friction_factor(friction),
} }
def pressure_flow_equation_values(self) -> tuple[float, ...]: def pressure_flow_equation_values(self) -> tuple[float, ...]:
@@ -754,6 +779,8 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
downstream_pressure: float, downstream_pressure: float,
upstream_temperature: float, upstream_temperature: float,
resistance_length: float, resistance_length: float,
max_iterations: int = 16,
analytic_laminar: bool = False,
) -> float: ) -> float:
"""AMESim pn2pipefr-style compressible friction flow.""" """AMESim pn2pipefr-style compressible friction flow."""
@@ -824,6 +851,18 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
value *= tanh(max(smoothing_argument, 0.0)) value *= tanh(max(smoothing_argument, 0.0))
return value return value
if analytic_laminar:
flow_parameter = mass_flow_parameter(pressure_ratio)
viscosity = self._dynamic_viscosity(T_up)
laminar_mass_flow = (
self.area * p_up * flow_parameter
) ** 2 / (16.0 * pi * viscosity * resistance_length * T_up)
if (
self.reynolds_number(laminar_mass_flow, T_up)
<= _PN2PIPEFR_ANALYTIC_LAMINAR_MAX_REYNOLDS
):
return laminar_mass_flow
def target_flow(mass_flow: float) -> float: def target_flow(mass_flow: float) -> float:
reynolds = self.reynolds_number(mass_flow, T_up) reynolds = self.reynolds_number(mass_flow, T_up)
friction = self.friction_factor(reynolds) friction = self.friction_factor(reynolds)
@@ -846,7 +885,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
* mass_flow_parameter(pressure_ratio) * mass_flow_parameter(pressure_ratio)
/ sqrt(T_up) / sqrt(T_up)
) )
for _iteration in range(16): for _iteration in range(max_iterations):
next_magnitude = target_flow(magnitude) next_magnitude = target_flow(magnitude)
if abs(next_magnitude - magnitude) <= max( if abs(next_magnitude - magnitude) <= max(
1.0e-12, 1.0e-12,
+36 -6
View File
@@ -241,8 +241,9 @@ class ResistancePnl0001SeriesBinding:
resistance_port: str resistance_port: str
resistance_other_port: str resistance_other_port: str
positive_flow_port: str positive_flow_port: str
pipe: AmesimPnl0001 pipe: AmesimPnl00r | AmesimPnl0001
pipe_port: str pipe_port: str
pipe_other_port: str | None
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -2338,24 +2339,31 @@ class PressureFlowSolver:
) -> tuple[ResistancePnl0001SeriesBinding, ...]: ) -> tuple[ResistancePnl0001SeriesBinding, ...]:
bindings: list[ResistancePnl0001SeriesBinding] = [] bindings: list[ResistancePnl0001SeriesBinding] = []
resistance_types = (AmesimPnor001, AmesimPnvo001FixedOpening) resistance_types = (AmesimPnor001, AmesimPnvo001FixedOpening)
pipe_types = (AmesimPnl00r, AmesimPnl0001)
for connection in self.network.connections: for connection in self.network.connections:
first_endpoint, second_endpoint = connection.endpoints first_endpoint, second_endpoint = connection.endpoints
first = self.network.components[first_endpoint.component] first = self.network.components[first_endpoint.component]
second = self.network.components[second_endpoint.component] second = self.network.components[second_endpoint.component]
if isinstance(first, resistance_types) and isinstance( if isinstance(first, resistance_types) and isinstance(
second, AmesimPnl0001 second, pipe_types
): ):
resistance, resistance_port = first, first_endpoint.port resistance, resistance_port = first, first_endpoint.port
pipe, pipe_port = second, second_endpoint.port pipe, pipe_port = second, second_endpoint.port
elif isinstance(second, resistance_types) and isinstance( elif isinstance(second, resistance_types) and isinstance(
first, AmesimPnl0001 first, pipe_types
): ):
resistance, resistance_port = second, second_endpoint.port resistance, resistance_port = second, second_endpoint.port
pipe, pipe_port = first, first_endpoint.port pipe, pipe_port = first, first_endpoint.port
else: else:
continue continue
if isinstance(pipe, AmesimPnl0001):
if isinstance(pipe, AmesimPnl0002) or pipe_port != "port_1": if isinstance(pipe, AmesimPnl0002) or pipe_port != "port_1":
continue continue
pipe_other_port = None
else:
pipe_other_port = (
"port_2" if pipe_port == "port_1" else "port_1"
)
if isinstance(resistance, AmesimPnor001): if isinstance(resistance, AmesimPnor001):
positive_flow_port, negative_flow_port = "port_1", "port_2" positive_flow_port, negative_flow_port = "port_1", "port_2"
else: else:
@@ -2374,12 +2382,13 @@ class PressureFlowSolver:
positive_flow_port=positive_flow_port, positive_flow_port=positive_flow_port,
pipe=pipe, pipe=pipe,
pipe_port=pipe_port, pipe_port=pipe_port,
pipe_other_port=pipe_other_port,
) )
) )
return tuple(bindings) return tuple(bindings)
def _seed_resistance_pnl0001_series_pressures(self) -> None: def _seed_resistance_pnl0001_series_pressures(self) -> None:
"""Causalize pressure between an orifice/valve and a PNL0001 R port.""" """Causalize pressure between an orifice/valve and a PNL pipe."""
from scipy.optimize import brentq from scipy.optimize import brentq
@@ -2390,7 +2399,14 @@ class PressureFlowSolver:
pipe_port = binding.pipe_port pipe_port = binding.pipe_port
pressure_a = resistance.get_port(binding.resistance_other_port).p pressure_a = resistance.get_port(binding.resistance_other_port).p
pressure_b = pipe.properties().p if isinstance(pipe, AmesimPnl00r):
assert binding.pipe_other_port is not None
pressure_b = pipe.get_port(binding.pipe_other_port).p
pipe_temperature = None
else:
pipe_properties = pipe.properties()
pressure_b = pipe_properties.p
pipe_temperature = pipe_properties.T
lower = min(pressure_a, pressure_b) lower = min(pressure_a, pressure_b)
upper = max(pressure_a, pressure_b) upper = max(pressure_a, pressure_b)
@@ -2405,10 +2421,24 @@ class PressureFlowSolver:
pressure_a, pressure_a,
intermediate_pressure, intermediate_pressure,
) )
if isinstance(pipe, AmesimPnl00r):
pipe.get_port(pipe_port).p = intermediate_pressure
if pipe_port == "port_1":
pipe_flow_into_connection = pipe.mass_flow( pipe_flow_into_connection = pipe.mass_flow(
intermediate_pressure, intermediate_pressure,
pressure_b, pressure_b,
pipe.properties().T, )
else:
pipe_flow_into_connection = -pipe.mass_flow(
pressure_b,
intermediate_pressure,
)
else:
assert pipe_temperature is not None
pipe_flow_into_connection = pipe.mass_flow(
intermediate_pressure,
pressure_b,
pipe_temperature,
) )
return resistance_flow_into_connection + pipe_flow_into_connection return resistance_flow_into_connection + pipe_flow_into_connection
@@ -38,7 +38,7 @@
| `PNVO001` | 8 | 信号调制气动孔口 | 第一版公开 | `amesim_pnvo001`,`flow` | 已接入名为 `res` 的标量信号输入端口,可由 `amesim_step0` 驱动开度;精确事件语义和 AMESim baseline 仍留后续修模。固定开度变体 `amesim_pnvo001_fixed` 继续保留。 | | `PNVO001` | 8 | 信号调制气动孔口 | 第一版公开 | `amesim_pnvo001`,`flow` | 已接入名为 `res` 的标量信号输入端口,可由 `amesim_step0` 驱动开度;精确事件语义和 AMESim baseline 仍留后续修模。固定开度变体 `amesim_pnvo001_fixed` 继续保留。 |
| `PN3NODE2` | 8 | 三端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_pn3node2`,`junctions` | 已接入三端等压、流量守恒基础版;AMESim port 2 参考温压语义和 stream 混合仍需单独测试。 | | `PN3NODE2` | 8 | 三端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_pn3node2`,`junctions` | 已接入三端等压、流量守恒基础版;AMESim port 2 参考温压语义和 stream 混合仍需单独测试。 |
| `P4NODE2` | 8 | 四端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_p4node2`,`junctions` | 已接入四端等压、流量守恒基础版;仍需复核 port 2 参考温压和多支路混合。 | | `P4NODE2` | 8 | 四端气动节点,压力温度由 port 2 固定 | 第一版公开 | `amesim_p4node2`,`junctions` | 已接入四端等压、流量守恒基础版;仍需复核 port 2 参考温压和多支路混合。 |
| `PNL00R` | 4 | 管路纯阻性摩擦段 | 第一版公开 | `amesim_pnl00r`,`flow` | 已接入准稳态阻性管公开契约;仍需按 AMESim `PNL00R` 参数和摩擦公式复核。 | | `PNL00R` | 4 | 管路纯阻性摩擦段 | 第一版公开 | `amesim_pnl00r`,`flow` | 已按 AMESim `pn2pipefr` 接入可压缩 `Cm/Cq` 关系、同侧上游温度和裸 `Cm` 诊断;使用未修改 AMESim 结果包直接保存的 `Re/ff` 标定 Re≈1400–3400 的 shifted-Hill C2 过渡曲线,并通过固定点、单调性和第八路 `0–0.81 s` 回归。 |
| `PNL0001` | 20 | C-R 动态管路 | 第一版公开 | `amesim_pnl0001`,`flow` | 已按公开契约接入两状态管内容积 + port 1 摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 | | `PNL0001` | 20 | C-R 动态管路 | 第一版公开 | `amesim_pnl0001`,`flow` | 已按公开契约接入两状态管内容积 + port 1 摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 |
| `PNL0002` | 8 | R-C-R 动态管路 | 第一版公开 | `amesim_pnl0002`,`flow` | 已按公开契约接入中心两状态容积 + 两端半长摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 | | `PNL0002` | 8 | R-C-R 动态管路 | 第一版公开 | `amesim_pnl0002`,`flow` | 已按公开契约接入中心两状态容积 + 两端半长摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 |
| `PNL0003` | 8 | C-R-C 动态管路 | 第一版公开 | `amesim_pnl0003`,`flow` | 已按公开契约接入两端四状态容积 + 中心摩擦流 + mode 2 换热项;大压差动态闭合和 AMESim baseline 误差仍留后续修模。 | | `PNL0003` | 8 | C-R-C 动态管路 | 第一版公开 | `amesim_pnl0003`,`flow` | 已按公开契约接入两端四状态容积 + 中心摩擦流 + mode 2 换热项;大压差动态闭合和 AMESim baseline 误差仍留后续修模。 |
@@ -89,6 +89,6 @@
## 下一步执行建议 ## 下一步执行建议
1. 以 `app.simulation.components.amesim.library` 的 22 个模型为公开清单唯一来源。 1. 以 `app.simulation.components.amesim.library` 的 22 个模型为公开清单唯一来源。
2. 优先校准 `PNCH023 / PNOR001 / PN3NODE2 / P4NODE2 / PNL00R` 与动态管路的 AMESim baseline 误差。 2. 优先校准 `PNCH023 / PNOR001 / PN3NODE2 / P4NODE2` 与动态管路的 AMESim baseline 误差;`PNL00R` 保持现有层流、过渡区固定点和单调性回归。
3. 每次调整模型都同步补充目录校验、参数边界、System XML 编译和最小仿真测试。 3. 每次调整模型都同步补充目录校验、参数边界、System XML 编译和最小仿真测试。
4. 用完整或代表性的 `test_mql` 画布校准 `PNRP17 + PNCH012` 的压力、力、位移和容积轨迹,并补齐事件边界语义。 4. 用完整或代表性的 `test_mql` 画布校准 `PNRP17 + PNCH012` 的压力、力、位移和容积轨迹,并补齐事件边界语义。
@@ -1,23 +1,23 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"id": "test_mql_8-progressive-v2", "id": "test_mql_8-progressive-v3",
"description": "Primary generic-solver regression target for the renamed 0.01 s-grid authoritative inputs, with a short smoke gate before staged extension.", "description": "Primary generic-solver regression target for the restored eight-branch 0.005 s-grid authoritative inputs, with a short smoke gate before staged extension.",
"source": { "source": {
"path": "tests/data/test-mql-8.xml", "path": "tests/data/test-mql-8.xml",
"sha256": "170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3", "sha256": "6eb2753a15af66fd8e91c169c4587e66b0ace2164c08c2cb23a67aab5fd09164",
"bytes": 101013, "bytes": 104109,
"xmlIsAuthoritative": true, "xmlIsAuthoritative": true,
"companionProject": { "companionProject": {
"path": "tests/data/test-mql-8.json", "path": "tests/data/test-mql-8.json",
"sha256": "258c50ee4850baa72fb7c2cc24536d0a631fc6a7f1fa6cedb7b6eea7c857cbaa", "sha256": "8d7a263691a9f09f39c92aba19ae1350911dc66b4904e98bae5f20592c5a3fcd",
"bytes": 264675, "bytes": 272591,
"executionInput": false "executionInput": false
}, },
"simulation": { "simulation": {
"tStart": 0.0, "tStart": 0.0,
"tStop": 0.2, "tStop": 5.0,
"sampleStep": 0.01, "sampleStep": 0.005,
"maxStep": 0.01, "maxStep": 0.005,
"method": "BDF" "method": "BDF"
} }
}, },
@@ -37,22 +37,38 @@
"status": "historicalOnly", "status": "historicalOnly",
"compatibleWithCurrentSource": false, "compatibleWithCurrentSource": false,
"reason": "This deferred-stage decision was calculated from the former 0.002 s source report and is not a runtime prediction for the renamed v2 source." "reason": "This deferred-stage decision was calculated from the former 0.002 s source report and is not a runtime prediction for the renamed v2 source."
},
{
"path": "tests/baselines/simulation/test_mql_8/runs/2026-08-17-production-v2-0.2.json",
"sha256": "93baac975413703bebe940308ccd0de5dbd753f7c1c4fe015907315f112618a5",
"sourceXmlSha256": "170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
"status": "historicalOnly",
"compatibleWithCurrentSource": false,
"reason": "This accepted 0.2 s report predates the four restored PNL0001 branches and cannot validate the v3 source."
},
{
"path": "tests/baselines/simulation/test_mql_8/runs/2026-08-17-production-v2-extension-decision.json",
"sha256": "c2dd8acfaae71e9c5f2eaef35d529dd1ce1d420b48703faff3d111678496b671",
"sourceXmlSha256": "170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
"status": "historicalOnly",
"compatibleWithCurrentSource": false,
"reason": "This runtime extension decision was derived from the v2 source and is retained only as historical evidence."
} }
], ],
"structure": { "structure": {
"componentCount": 152, "componentCount": 156,
"connectionCount": 174, "connectionCount": 178,
"dynamicComponentCount": 54, "dynamicComponentCount": 58,
"stateCount": 124, "stateCount": 132,
"resultVariableCount": 1716, "resultVariableCount": 1784,
"pressureFlowUnknownCount": 760, "pressureFlowUnknownCount": 776,
"pressureFlowEquationCount": 760, "pressureFlowEquationCount": 776,
"pressureFlowIsSquare": true, "pressureFlowIsSquare": true,
"logicalEffortCoordinateCount": 112, "logicalEffortCoordinateCount": 116,
"eliminatedEffortAliasCount": 320, "eliminatedEffortAliasCount": 324,
"canonicalCoordinateCount": 440, "canonicalCoordinateCount": 452,
"compatibilityScatterCount": 760, "compatibilityScatterCount": 776,
"jacobianNonzeroCount": 3296, "jacobianNonzeroCount": 3280,
"jacobianColorGroupCount": 52, "jacobianColorGroupCount": 52,
"hasMechanicalStateEvents": true "hasMechanicalStateEvents": true
}, },
@@ -74,7 +90,7 @@
"productionEquivalentOutput": false "productionEquivalentOutput": false
}, },
"production": { "production": {
"description": "Acceptance lane: changes only tStop and preserves the source 0.01 s output and internal-step grid.", "description": "Acceptance lane: changes only tStop and preserves the source 0.005 s output and internal-step grid.",
"samplingMode": "source", "samplingMode": "source",
"maxStepMode": "source", "maxStepMode": "source",
"instrumentationMode": "standard", "instrumentationMode": "standard",
@@ -105,13 +121,7 @@
"expectedSignalEventTimes": [ "expectedSignalEventTimes": [
0.04 0.04
], ],
"expectedMechanicalTransitionTimes": [], "expectedMechanicalTransitionTimes": []
"goldens": {
"production": {
"path": "tests/baselines/simulation/test_mql_8/goldens/production-0.2s-v1.json",
"sha256": "99fa7b3631a89f59f86551847175f3701a1567d8d0bd10d87545734f17515d72"
}
}
}, },
"1s": { "1s": {
"stopTime": 1.0, "stopTime": 1.0,
@@ -178,7 +188,7 @@
"eventTimeAbsoluteToleranceSeconds": 0.00002, "eventTimeAbsoluteToleranceSeconds": 0.00002,
"signalEventTimeAbsoluteToleranceSeconds": 1e-12, "signalEventTimeAbsoluteToleranceSeconds": 1e-12,
"mechanicalTransitionTimesAvailable": true, "mechanicalTransitionTimesAvailable": true,
"note": "Physical state checkpoints/events and the value-free output-shape contract are separate. The production 0.2 s variant has an approved state golden; longer horizons and critical algebraic projections remain pending." "note": "The restored v3 source currently enforces structure, finite series, residuals, checkpoints, and events. Its v2 state golden is historical-only; a new reviewed v3 state golden remains pending."
}, },
"execution": { "execution": {
"causalExecutorV2Default": true, "causalExecutorV2Default": true,
+257 -9
View File
@@ -2,8 +2,12 @@ from __future__ import annotations
import unittest import unittest
from app.simulation.components.amesim.flow.pipes import AmesimPnl00r from app.simulation.components.amesim.flow.pipes import (
AmesimPnl0001,
AmesimPnl00r,
)
from app.simulation.components.amesim.junctions.nodes import AmesimPn3Node2 from app.simulation.components.amesim.junctions.nodes import AmesimPn3Node2
from app.simulation.components.amesim.media.mediums import AmesimHeliumPengRobinsonMedium
from app.simulation.components.experimental.storage.cylinder import Cylinder from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.components.experimental.storage.tank import Tank from app.simulation.components.experimental.storage.tank import Tank
from app.simulation.core.medium import IdealGasMedium from app.simulation.core.medium import IdealGasMedium
@@ -94,12 +98,189 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
self.assertGreater(above, below) self.assertGreater(above, below)
self.assertLess(reverse, 0.0) self.assertLess(reverse, 0.0)
self.assertAlmostEqual(below, -reverse, delta=abs(below) * 1.0e-9) self.assertAlmostEqual(below, -reverse, delta=abs(below) * 1.0e-9)
self.assertLess(abs(above - below), abs(above) * 0.05) self.assertLess(abs(above - below), abs(above) * 0.10)
self.assertEqual(pipe.mass_flow(base_pressure, base_pressure), 0.0) self.assertEqual(pipe.mass_flow(base_pressure, base_pressure), 0.0)
self.assertGreater(pipe.mass_flow(15.3e6 + 0.1, 15.3e6), 0.0) self.assertGreater(pipe.mass_flow(15.3e6 + 0.1, 15.3e6), 0.0)
def test_matches_amesim_pn2pipefr_laminar_baselines(self) -> None:
medium = AmesimHeliumPengRobinsonMedium()
pipe = AmesimPnl00r(
"pnl_1",
medium,
diam=0.014,
le=1.0,
rr=0.045 / 14.0,
)
samples = (
(
13_887_929.5734,
288.959589373,
13_887_887.6633,
288.959239289,
1.79075554200e-4,
838.404977868,
3.32483767035e-6,
0.0521569680484,
0.076335424633,
),
(
13_093_276.1461,
285.548411921,
13_093_238.0341,
285.546325432,
1.40922897107e-4,
665.123640745,
3.09743309373e-6,
0.0429212663992,
0.0962227112065,
),
(
11_690_210.3512,
272.892693485,
11_690_180.6138,
272.889527291,
7.10081729578e-5,
345.615999811,
2.3706370264e-6,
0.0230685613948,
0.185176612295,
),
)
def test_pressure_drop_inversion_cache_keys_all_property_inputs(self) -> None: for p_1, T_1, p_2, T_2, mass_flow, re, cm, velocity, ff in samples:
with self.subTest(pressure=p_1):
pipe.port_1.p = p_1
pipe.port_2.p = p_2
pipe.update_stream_outflows(
{
"port_1": medium.specific_enthalpy_at_pressure(p_1, T_1),
"port_2": medium.specific_enthalpy_at_pressure(p_2, T_2),
}
)
actual_mass_flow = pipe.mass_flow(p_1, p_2)
results = pipe.component_result_values()
self.assertAlmostEqual(actual_mass_flow / mass_flow, 1.0, delta=0.005)
self.assertAlmostEqual(results["re"] / re, 1.0, delta=0.01)
self.assertAlmostEqual(results["cm"] / cm, 1.0, delta=1.0e-4)
self.assertAlmostEqual(results["v"] / velocity, 1.0, delta=0.005)
self.assertAlmostEqual(results["ff"] / ff, 1.0, delta=0.01)
def test_matches_amesim_pn2pipefr_transition_baselines(self) -> None:
medium = AmesimHeliumPengRobinsonMedium()
pipe = AmesimPnl00r(
"pnl_1",
medium,
diam=0.014,
le=1.0,
rr=0.045 / 14.0,
)
# Unchanged test_mql_1.ame, pneumatic_105, ascending transition at
# t=0.2862..0.3047 s. AMESim dm1 is positive into port 1 while this
# component's port-1-to-port-2 coordinate is negative for these rows.
samples = (
(
10_631_986.90667049,
262.71180639527233,
10_632_026.310978588,
262.7092388107747,
-2.8192909824026485e-4,
1408.0846374820405,
5.030700878807581e-6,
-0.09668817162408228,
0.04545209089983021,
),
(
10_594_171.934888396,
262.32910587014925,
10_594_213.825687861,
262.3266782251236,
-3.598950492898518e-4,
1799.2599253847663,
5.723572909300966e-6,
-0.1236749841566422,
0.03590026408283139,
),
(
10_554_721.69137201,
261.9257072357455,
10_554_766.280823693,
261.92350790162175,
-4.3973197060195834e-4,
2200.69088342098,
6.529418698258151e-6,
-0.1514267390526478,
0.03111112083131272,
),
(
10_544_953.846352266,
261.82512637406904,
10_544_999.25593169,
261.82299814935993,
-4.592053720488228e-4,
2298.7460389094877,
6.783006948981891e-6,
-0.15821443006863944,
0.03074226487938153,
),
(
10_514_807.25092133,
261.5128728347395,
10_514_855.822238008,
261.5109955483938,
-5.190048780471613e-4,
2600.199197308607,
7.798343300840328e-6,
-0.17910295287093314,
0.03166638237616583,
),
(
10_432_760.829010766,
260.6482173603332,
10_432_818.817992153,
260.6472539768074,
-6.759582533840764e-4,
3394.136683451109,
1.1253055264735043e-5,
-0.23427502517225277,
0.03839469933871734,
),
)
for p_1, T_1, p_2, T_2, mass_flow, re, cm, velocity, ff in samples:
with self.subTest(reynolds=re):
pipe.port_1.p = p_1
pipe.port_2.p = p_2
pipe.update_stream_outflows(
{
"port_1": medium.specific_enthalpy_at_pressure(p_1, T_1),
"port_2": medium.specific_enthalpy_at_pressure(p_2, T_2),
}
)
actual_mass_flow = pipe.mass_flow(p_1, p_2)
results = pipe.component_result_values()
self.assertAlmostEqual(actual_mass_flow / mass_flow, 1.0, delta=0.006)
self.assertAlmostEqual(results["re"] / re, 1.0, delta=0.012)
self.assertAlmostEqual(results["cm"] / cm, 1.0, delta=1.0e-4)
self.assertAlmostEqual(results["v"] / velocity, 1.0, delta=0.006)
self.assertAlmostEqual(results["ff"] / ff, 1.0, delta=0.012)
def test_upstream_temperature_uses_same_side_connected_stream(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium)
h_1 = self.medium.specific_enthalpy(250.0)
h_2 = self.medium.specific_enthalpy(400.0)
pipe.update_stream_outflows({"port_1": h_1, "port_2": h_2})
self.assertAlmostEqual(pipe._port_temperature("port_1"), 250.0)
self.assertAlmostEqual(pipe._port_temperature("port_2"), 400.0)
self.assertEqual(pipe.port_1.h_outflow, h_2)
self.assertEqual(pipe.port_2.h_outflow, h_1)
def test_pn2pipefr_cache_keys_all_property_inputs(self) -> None:
pipe = AmesimPnl00r( pipe = AmesimPnl00r(
"pnl_1", "pnl_1",
self.medium, self.medium,
@@ -113,12 +294,12 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
"port_2": self.medium.specific_enthalpy(300.0), "port_2": self.medium.specific_enthalpy(300.0),
} }
) )
pipe._mass_flow_for_pressure_drop.cache_clear() AmesimPnl0001._one_way_pn2pipefr_mass_flow.cache_clear()
first = pipe.mass_flow(501000.0, 500000.0) first = pipe.mass_flow(501000.0, 500000.0)
after_first = pipe._mass_flow_for_pressure_drop.cache_info() after_first = AmesimPnl0001._one_way_pn2pipefr_mass_flow.cache_info()
second = pipe.mass_flow(501000.0, 500000.0) second = pipe.mass_flow(501000.0, 500000.0)
after_second = pipe._mass_flow_for_pressure_drop.cache_info() after_second = AmesimPnl0001._one_way_pn2pipefr_mass_flow.cache_info()
pipe.update_flow_temperature_references( pipe.update_flow_temperature_references(
{ {
"port_1": self.medium.specific_enthalpy(400.0), "port_1": self.medium.specific_enthalpy(400.0),
@@ -126,7 +307,7 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
} }
) )
third = pipe.mass_flow(501000.0, 500000.0) third = pipe.mass_flow(501000.0, 500000.0)
after_temperature_change = pipe._mass_flow_for_pressure_drop.cache_info() after_temperature_change = AmesimPnl0001._one_way_pn2pipefr_mass_flow.cache_info()
self.assertEqual(first, second) self.assertEqual(first, second)
self.assertEqual(after_first.misses, 1) self.assertEqual(after_first.misses, 1)
@@ -345,6 +526,10 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
actual_flow, actual_flow,
upstream_temperature, upstream_temperature,
) )
friction = pipe.friction_factor(expected_reynolds)
flow_coefficient = (
pipe.diam / (pipe.le * friction)
) ** 0.5
self.assertAlmostEqual(results["re"], expected_reynolds) self.assertAlmostEqual(results["re"], expected_reynolds)
self.assertAlmostEqual( self.assertAlmostEqual(
results["v"], results["v"],
@@ -354,11 +539,11 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
results["cm"], results["cm"],
abs(actual_flow) abs(actual_flow)
* upstream_temperature**0.5 * upstream_temperature**0.5
/ (pipe.area * upstream_pressure), / (flow_coefficient * pipe.area * upstream_pressure),
) )
self.assertAlmostEqual( self.assertAlmostEqual(
results["ff"], results["ff"],
min(pipe.friction_factor(expected_reynolds), 64_000_000.0), min(friction, 64_000_000.0),
) )
def test_friction_factor_transitions_from_laminar_to_turbulent(self) -> None: def test_friction_factor_transitions_from_laminar_to_turbulent(self) -> None:
@@ -405,6 +590,69 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
delta=8.0e-5, delta=8.0e-5,
) )
def test_friction_factor_matches_amesim_transition_sweep(self) -> None:
pipe = AmesimPnl00r(
"pnl_14mm",
self.medium,
diam=0.014,
rr=0.045 / 14.0,
)
samples = (
(1408.0846374820405, 0.04545209089983021),
(1594.9147224775163, 0.04022919421391556),
(1799.2599253847663, 0.03590026408283139),
(2001.1290261016431, 0.03286496043823008),
(2200.69088342098, 0.03111112083131272),
(2298.7460389094877, 0.03074226487938153),
(2396.165041443924, 0.030707477592933765),
(2600.199197308607, 0.03166638237616583),
(2795.6649128003255, 0.03349648426341731),
(2996.892872668051, 0.035593529418766444),
(3204.685791989844, 0.037354280559382634),
(3394.136683451109, 0.03839469933871734),
)
for reynolds, expected in samples:
with self.subTest(reynolds=reynolds):
self.assertAlmostEqual(
pipe.friction_factor(reynolds) / expected,
1.0,
delta=0.0015,
)
def test_mass_flow_is_monotone_through_transition(self) -> None:
medium = AmesimHeliumPengRobinsonMedium()
pipe = AmesimPnl00r(
"pnl_14mm",
medium,
diam=0.014,
le=1.0,
rr=0.045 / 14.0,
)
mean_pressure = 10.6e6
temperature = 262.0
enthalpy = medium.specific_enthalpy_at_pressure(mean_pressure, temperature)
pipe.update_flow_temperature_references(
{"port_1": enthalpy, "port_2": enthalpy}
)
forward: list[float] = []
reverse: list[float] = []
for index in range(221):
pressure_drop = 25.0 + index * 0.25
high = mean_pressure + 0.5 * pressure_drop
low = mean_pressure - 0.5 * pressure_drop
forward.append(pipe.mass_flow(high, low))
reverse.append(-pipe.mass_flow(low, high))
self.assertLess(pipe.reynolds_number(forward[0], temperature), 1400.0)
self.assertGreater(
pipe.reynolds_number(forward[-1], temperature), 3400.0
)
self.assertTrue(all(left < right for left, right in zip(forward, forward[1:])))
for forward_flow, reverse_flow in zip(forward, reverse):
self.assertAlmostEqual(forward_flow, reverse_flow, delta=1.0e-12)
def test_friction_factor_matches_amesim_transition_regime(self) -> None: def test_friction_factor_matches_amesim_transition_regime(self) -> None:
pipe = AmesimPnl00r( pipe = AmesimPnl00r(
"pnl_20mm", "pnl_20mm",
+10 -6
View File
@@ -125,12 +125,12 @@ class RegressionManifestTests(unittest.TestCase):
self.assertTrue(manifest["source"]["xmlIsAuthoritative"]) self.assertTrue(manifest["source"]["xmlIsAuthoritative"])
self.assertEqual( self.assertEqual(
manifest["source"]["sha256"], manifest["source"]["sha256"],
"170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3", "6eb2753a15af66fd8e91c169c4587e66b0ace2164c08c2cb23a67aab5fd09164",
) )
self.assertEqual(Path(manifest["_sourcePath"]).name, "test-mql-8.xml") self.assertEqual(Path(manifest["_sourcePath"]).name, "test-mql-8.xml")
self.assertEqual( self.assertEqual(
manifest["source"]["companionProject"]["sha256"], manifest["source"]["companionProject"]["sha256"],
"258c50ee4850baa72fb7c2cc24536d0a631fc6a7f1fa6cedb7b6eea7c857cbaa", "8d7a263691a9f09f39c92aba19ae1350911dc66b4904e98bae5f20592c5a3fcd",
) )
self.assertFalse( self.assertFalse(
manifest["source"]["companionProject"]["executionInput"] manifest["source"]["companionProject"]["executionInput"]
@@ -142,6 +142,10 @@ class RegressionManifestTests(unittest.TestCase):
self.assertEqual(manifest["lanes"]["production"]["samplingMode"], "source") self.assertEqual(manifest["lanes"]["production"]["samplingMode"], "source")
self.assertEqual(manifest["lanes"]["solver-only"]["sampleStep"], 0.02) self.assertEqual(manifest["lanes"]["solver-only"]["sampleStep"], 0.02)
self.assertEqual(manifest["lanes"]["solver-only"]["maxStep"], 0.05) self.assertEqual(manifest["lanes"]["solver-only"]["maxStep"], 0.05)
self.assertEqual(manifest["historicalReports"][2]["status"], "historicalOnly")
self.assertFalse(
manifest["historicalReports"][2]["compatibleWithCurrentSource"]
)
def test_in_memory_derivative_does_not_change_authoritative_xml(self) -> None: def test_in_memory_derivative_does_not_change_authoritative_xml(self) -> None:
manifest = load_regression_manifest(DEFAULT_MANIFEST_PATH) manifest = load_regression_manifest(DEFAULT_MANIFEST_PATH)
@@ -155,9 +159,9 @@ class RegressionManifestTests(unittest.TestCase):
max_step=0.005, max_step=0.005,
) )
self.assertEqual(source_simulation_config(before)["tStop"], 0.2) self.assertEqual(source_simulation_config(before)["tStop"], 5.0)
self.assertEqual(source_simulation_config(before)["sampleStep"], 0.01) self.assertEqual(source_simulation_config(before)["sampleStep"], 0.005)
self.assertEqual(source_simulation_config(before)["maxStep"], 0.01) self.assertEqual(source_simulation_config(before)["maxStep"], 0.005)
self.assertEqual(source_simulation_config(derived)["tStop"], 1.0) self.assertEqual(source_simulation_config(derived)["tStop"], 1.0)
self.assertEqual(source_simulation_config(derived)["sampleStep"], 0.02) self.assertEqual(source_simulation_config(derived)["sampleStep"], 0.02)
self.assertEqual(source_simulation_config(derived)["maxStep"], 0.005) self.assertEqual(source_simulation_config(derived)["maxStep"], 0.005)
@@ -581,7 +585,7 @@ class ProgressiveSuiteTests(unittest.TestCase):
self.assertEqual( self.assertEqual(
seen, seen,
[("solver-only", 0.02, 0.05), ("production", 0.01, 0.01)], [("solver-only", 0.02, 0.05), ("production", 0.005, 0.005)],
) )
+10 -10
View File
@@ -222,15 +222,15 @@ class TargetCausalNumericIRStructureTests(unittest.TestCase):
assert self.compilation.ir is not None assert self.compilation.ir is not None
program = self.compilation.ir.program program = self.compilation.ir.program
self.assertEqual(len(program.compatibility_slots), 760) self.assertEqual(len(program.compatibility_slots), 776)
self.assertEqual(len(program.canonical_slots), 440) self.assertEqual(len(program.canonical_slots), 452)
self.assertEqual(program.effort_group_count, 112) self.assertEqual(program.effort_group_count, 116)
self.assertEqual(program.flow_assignment_count, 328) self.assertEqual(program.flow_assignment_count, 336)
self.assertEqual(program.effort_scatter_count, 432) self.assertEqual(program.effort_scatter_count, 440)
self.assertEqual(program.eliminated_effort_replica_count, 320) self.assertEqual(program.eliminated_effort_replica_count, 324)
self.assertEqual( self.assertEqual(
tuple(slot.slot for slot in program.canonical_slots), tuple(slot.slot for slot in program.canonical_slots),
tuple(range(440)), tuple(range(452)),
) )
def test_target_batches_efforts_and_preserves_flow_stage_layout(self) -> None: def test_target_batches_efforts_and_preserves_flow_stage_layout(self) -> None:
@@ -240,8 +240,8 @@ class TargetCausalNumericIRStructureTests(unittest.TestCase):
stage for stage in program.effort_stages if stage.variable == "p" stage for stage in program.effort_stages if stage.variable == "p"
) )
self.assertEqual(len(pressure_stage.operations), 72) self.assertEqual(len(pressure_stage.operations), 76)
self.assertEqual(len(pressure_stage.evaluations), 36) self.assertEqual(len(pressure_stage.evaluations), 40)
self.assertTrue( self.assertTrue(
all( all(
evaluation.opcode evaluation.opcode
@@ -251,7 +251,7 @@ class TargetCausalNumericIRStructureTests(unittest.TestCase):
) )
self.assertEqual( self.assertEqual(
[len(stage.target_slots) for stage in program.flow_stages], [len(stage.target_slots) for stage in program.flow_stages],
[110, 130, 49, 33, 5, 1], [114, 134, 49, 33, 5, 1],
) )
def test_target_reference_execution_matches_all_compatibility_slots(self) -> None: def test_target_reference_execution_matches_all_compatibility_slots(self) -> None:
@@ -7,6 +7,7 @@ from unittest.mock import patch
from app.simulation.components.amesim.boundary.sources import AmesimPnpl01 from app.simulation.components.amesim.boundary.sources import AmesimPnpl01
from app.simulation.components.amesim.flow.orifices import ( from app.simulation.components.amesim.flow.orifices import (
AmesimPnor001, AmesimPnor001,
AmesimPnvo001FixedOpening,
AmesimPnvo001SignalOpening, AmesimPnvo001SignalOpening,
) )
from app.simulation.components.amesim.flow.pipes import AmesimPnl00r from app.simulation.components.amesim.flow.pipes import AmesimPnl00r
@@ -110,6 +111,62 @@ class PressureFlowSolverInitializationTests(unittest.TestCase):
self.assertAlmostEqual(valve.port_2.p, pipe.port_1.p) self.assertAlmostEqual(valve.port_2.p, pipe.port_1.p)
self.assertAlmostEqual(-valve.port_2.m_flow, pipe.port_1.m_flow) self.assertAlmostEqual(-valve.port_2.m_flow, pipe.port_1.m_flow)
def test_pnvo_pnl00r_series_pressure_is_seeded_by_flow_balance(self) -> None:
medium = IdealGasMedium()
source = Cylinder(
"source",
medium,
V=0.01,
p0=150_000.0,
T0=300.0,
)
valve = AmesimPnvo001FixedOpening(
"valve",
medium,
opening=0.5,
)
pipe = AmesimPnl00r(
"pipe",
medium,
diam=0.02,
le=1.0,
rr=1.0e-5,
)
sink = Tank(
"sink",
medium,
V=0.1,
p0=100_000.0,
T0=300.0,
)
network = SimulationNetwork("pnvo-pnl00r-series")
for component in (source, valve, pipe, sink):
network.add_component(component)
network.connect("source", "port_b", "valve", "port_2")
network.connect("valve", "port_3", "pipe", "port_1")
network.connect("pipe", "port_2", "sink", "port_a")
source.refresh_thermodynamic_ports()
sink.refresh_thermodynamic_ports()
solver = PressureFlowSolver(network)
result = solver.solve()
self.assertTrue(result.success)
self.assertEqual(result.evaluations, 0)
self.assertGreater(valve.port_3.p, pipe.port_2.p)
self.assertLess(valve.port_3.p, source.port_b.p)
self.assertAlmostEqual(valve.port_3.p, pipe.port_1.p)
self.assertAlmostEqual(
-valve.port_3.m_flow,
pipe.port_1.m_flow,
delta=1.0e-10,
)
self.assertFalse(solver.causal_fast_path_eligible)
self.assertEqual(
solver.causal_execution_diagnostics()["fallbackReason"],
"specialSeriesPressureSeed",
)
def test_dead_ended_pnl00r_is_seeded_at_zero_flow_pressure(self) -> None: def test_dead_ended_pnl00r_is_seeded_at_zero_flow_pressure(self) -> None:
medium = IdealGasMedium() medium = IdealGasMedium()
pipe = AmesimPnl00r("resistance", medium) pipe = AmesimPnl00r("resistance", medium)
+5 -5
View File
@@ -165,9 +165,9 @@ class SupportedPistonTangentCompilerTests(unittest.TestCase):
order=0, order=0,
) )
self.assertEqual(pattern.nnz, 3296) self.assertEqual(pattern.nnz, 3280)
self.assertEqual(columns_only.nnz, 2464) self.assertEqual(columns_only.nnz, 2448)
self.assertEqual(len(active_columns), 108) self.assertEqual(len(active_columns), 116)
self.assertEqual(int(groups.max(initial=-1)) + 1, 36) self.assertEqual(int(groups.max(initial=-1)) + 1, 36)
jacobian = SparseSecantJacobian( jacobian = SparseSecantJacobian(
@@ -182,10 +182,10 @@ class SupportedPistonTangentCompilerTests(unittest.TestCase):
self.assertEqual(diagnostics["originalColorGroupCount"], 52) self.assertEqual(diagnostics["originalColorGroupCount"], 52)
self.assertEqual(diagnostics["remainingColorGroupCount"], 36) self.assertEqual(diagnostics["remainingColorGroupCount"], 36)
self.assertEqual(diagnostics["exactColumnCount"], 16) self.assertEqual(diagnostics["exactColumnCount"], 16)
self.assertEqual(diagnostics["finiteDifferenceColumnCount"], 108) self.assertEqual(diagnostics["finiteDifferenceColumnCount"], 116)
self.assertEqual( self.assertEqual(
jacobian._remaining_finite_difference_sparsity.nnz, jacobian._remaining_finite_difference_sparsity.nnz,
2176, 2160,
) )
def test_partial_unsupported_branch_fails_with_stable_reason(self) -> None: def test_partial_unsupported_branch_fails_with_stable_reason(self) -> None:
+19 -7
View File
@@ -127,12 +127,15 @@ class TestMql8StaticRegressionTests(unittest.TestCase):
case_id, case_id,
) )
def test_approved_production_golden_replays_its_source_report(self) -> None: def test_historical_v2_golden_replays_its_source_report(self) -> None:
repository_root = Path(self.manifest["_repositoryRoot"]) repository_root = Path(self.manifest["_repositoryRoot"])
reference = self.manifest["variants"]["0.2s"]["goldens"]["production"] golden_path = (
repository_root
/ "tests/baselines/simulation/test_mql_8/goldens/production-0.2s-v1.json"
)
golden = load_regression_golden( golden = load_regression_golden(
repository_root / reference["path"], golden_path,
expected_sha256=reference["sha256"], expected_sha256="99fa7b3631a89f59f86551847175f3701a1567d8d0bd10d87545734f17515d72",
repository_root=repository_root, repository_root=repository_root,
) )
source_report = golden["provenance"]["sourceReport"] source_report = golden["provenance"]["sourceReport"]
@@ -142,12 +145,16 @@ class TestMql8StaticRegressionTests(unittest.TestCase):
case = next(item for item in report["cases"] if item["caseId"] == "0.2s") case = next(item for item in report["cases"] if item["caseId"] == "0.2s")
audit = evaluate_regression_golden(case["worker"]["summary"], golden) audit = evaluate_regression_golden(case["worker"]["summary"], golden)
self.assertNotEqual(
golden["sourceXmlSha256"],
self.manifest["source"]["sha256"],
)
self.assertTrue(audit["passed"], audit) self.assertTrue(audit["passed"], audit)
self.assertEqual(audit["comparedValueCount"], 402) self.assertEqual(audit["comparedValueCount"], 402)
self.assertEqual(audit["maxAbsoluteError"], 0.0) self.assertEqual(audit["maxAbsoluteError"], 0.0)
self.assertEqual(audit["maxToleranceRatio"], 0.0) self.assertEqual(audit["maxToleranceRatio"], 0.0)
def test_extension_decision_is_bound_to_the_current_report_and_budget(self) -> None: def test_historical_v2_extension_decision_remains_auditable(self) -> None:
repository_root = Path(self.manifest["_repositoryRoot"]) repository_root = Path(self.manifest["_repositoryRoot"])
runs = repository_root / "tests/baselines/simulation/test_mql_8/runs" runs = repository_root / "tests/baselines/simulation/test_mql_8/runs"
decision = json.loads( decision = json.loads(
@@ -164,6 +171,10 @@ class TestMql8StaticRegressionTests(unittest.TestCase):
first_decision = decision["decisions"][0] first_decision = decision["decisions"][0]
self.assertEqual( self.assertEqual(
decision["sourceXmlSha256"],
"170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
)
self.assertNotEqual(
decision["sourceXmlSha256"], self.manifest["source"]["sha256"] decision["sourceXmlSha256"], self.manifest["source"]["sha256"]
) )
self.assertEqual( self.assertEqual(
@@ -191,7 +202,7 @@ class TestMql8StaticRegressionTests(unittest.TestCase):
self.assertEqual(first_decision["outcome"], "deferred") self.assertEqual(first_decision["outcome"], "deferred")
self.assertTrue(decision["simulationWasNotStartedForDeferredCases"]) self.assertTrue(decision["simulationWasNotStartedForDeferredCases"])
def test_periodic_main_lane_exercises_the_approved_production_golden(self) -> None: def test_periodic_main_lane_uses_the_current_production_source(self) -> None:
workflow = ( workflow = (
Path(self.manifest["_repositoryRoot"]) Path(self.manifest["_repositoryRoot"])
/ ".github/workflows/solver-regression.yml" / ".github/workflows/solver-regression.yml"
@@ -199,11 +210,12 @@ class TestMql8StaticRegressionTests(unittest.TestCase):
self.assertIn("default: production", workflow) self.assertIn("default: production", workflow)
self.assertIn("inputs.lane || 'production'", workflow) self.assertIn("inputs.lane || 'production'", workflow)
self.assertNotIn("goldens", self.manifest["variants"]["0.2s"])
@unittest.skipUnless( @unittest.skipUnless(
os.getenv(LONG_RUN_ENVIRONMENT, "").strip().lower() in {"1", "true", "yes"}, os.getenv(LONG_RUN_ENVIRONMENT, "").strip().lower() in {"1", "true", "yes"},
f"Set {LONG_RUN_ENVIRONMENT}=1 to run bounded 0.01/0.2/1/5/10 s integration.", f"Set {LONG_RUN_ENVIRONMENT}=1 to run bounded 0.005/0.2/1/5/10 s integration.",
) )
class TestMql8ProgressiveLongRegressionTests(unittest.TestCase): class TestMql8ProgressiveLongRegressionTests(unittest.TestCase):
@classmethod @classmethod