完成求解器雅可比矩阵首轮优化,增加更新目录,整理了文档文件夹,增加了服务启动脚本
This commit is contained in:
1 parent
6bb0591d32
commit
16a7eb2d6c
48 files changed
+8172
-217
No files matched your search
@@ -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),
|
||||
)
|
||||
Reference in new issue
Block a user