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