完成求解器雅可比矩阵首轮优化,增加更新目录,整理了文档文件夹,增加了服务启动脚本

This commit is contained in:
lujingze committed 2026-08-17 07:33:31 +00:00
1 parent 6bb0591d32
commit 16a7eb2d6c
48 files changed
+8172 -217

No files matched your search

+219 -3
View File
@@ -1,8 +1,9 @@
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 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 (
AMESIM_GAS_INDEX_PARAMETER,
@@ -22,11 +23,35 @@ from app.simulation.core.metadata import (
ResultVariableDefinition,
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.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
@@ -829,6 +854,105 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
)
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]:
props = self.properties()
flow = self.mass_flow(self.port_1.p, props.p, props.T)
@@ -913,6 +1037,98 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
)
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):
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
@@ -1,7 +1,8 @@
from __future__ import annotations
from collections.abc import Mapping
from math import pi
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from math import isfinite, pi
from app.simulation.components.amesim.gases import (
AMESIM_GAS_INDEX_PARAMETER,
@@ -18,6 +19,18 @@ from app.simulation.core.ports import PortDefinition
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):
"""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]]:
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:
self.port_1.h_outflow = connected_h.get(
"port_1",
@@ -1,7 +1,8 @@
from __future__ import annotations
from collections.abc import Mapping
from math import expm1
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from math import expm1, isfinite
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
from app.simulation.core.catalog import (
@@ -20,6 +21,24 @@ from app.simulation.core.medium import IdealGasMedium
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_NON_RESTITUTION = ParameterCondition("stoptype", (1.0, 2.0, 4.0))
_MECMAS21_LIMITS_ENABLED = ParameterCondition("stoptype", (1.0, 2.0, 3.0))
@@ -723,6 +742,204 @@ class AmesimMecmas21(DynamicComponent):
)
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]:
return {
"a": self.acceleration(),
@@ -979,6 +1196,112 @@ class AmesimLstp00a(AlgebraicComponent):
)
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:
self._causal_penetration = None
self._causal_contact_force = None
@@ -1,7 +1,8 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from math import isfinite
from typing import ClassVar
from app.simulation.core.errors import RecoverableTrialStateError
@@ -9,6 +10,8 @@ from app.simulation.core.medium import (
GasMedium,
IdealGasMedium,
ThermodynamicProperties,
ThermodynamicPropertiesLinearization,
ThermodynamicPropertyTangents,
)
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
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)
class AmesimGasPropertyModelSpec:
@@ -1,6 +1,8 @@
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 (
AMESIM_GAS_INDEX_PARAMETER,
@@ -14,11 +16,24 @@ from app.simulation.core.metadata import (
ResultVariableDefinition,
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.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):
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
@@ -518,6 +533,132 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
energy_derivative -= props.p * self.total_volume_rate()
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, ...]:
pressure = self.medium.properties_from_mU(
self.state.m,