校准第二支路热流体能量与管路摩擦

This commit is contained in:
huojiarong committed 2026-08-11 12:13:09 +00:00
1 parent 0f73d5b568
commit caca32a513
12 files changed
+336 -42

No files matched your search

+99 -17
View File
@@ -47,7 +47,7 @@ class AmesimPnl00r(AlgebraicComponent):
"""
MODEL_TYPE = "amesim_pnl00r"
MODEL_VERSION = "0.1.0"
MODEL_VERSION = "0.2.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
@@ -203,13 +203,34 @@ class AmesimPnl00r(AlgebraicComponent):
laminar = 64.0 / reynolds_number
if reynolds_number <= 2300.0:
return laminar
turbulent = 1.0 / (
-1.8 * log10((self.rr / 3.7) ** 1.11 + 6.9 / reynolds_number)
# pn2pipefr does not apply the fully rough correction at every
# turbulent Reynolds number. Its saved ff curves first follow the
# hydraulically smooth law and approach the rough asymptote as Re*rr
# grows. Keeping those two limits separate reproduces the AMESim
# curves for both 14 mm and 20 mm test_mql pipes; putting both terms
# directly inside one Haaland logarithm over-predicts PNL0002 friction
# by about 23 percent near Re=57,000.
smooth_turbulent = 1.0 / (
-1.8 * log10(6.9 / reynolds_number)
) ** 2
if self.rr <= 0.0:
turbulent = smooth_turbulent
else:
fully_rough = 1.0 / (
-1.8 * log10((self.rr / 3.7) ** 1.11)
) ** 2
roughness_reynolds = reynolds_number * self.rr
roughness_weight = roughness_reynolds * roughness_reynolds / (
roughness_reynolds * roughness_reynolds + 180.0 * 180.0
)
turbulent = smooth_turbulent + roughness_weight * (
fully_rough - smooth_turbulent
)
if reynolds_number >= 4000.0:
return turbulent
fraction = (reynolds_number - 2300.0) / 1700.0
return laminar + fraction * (turbulent - laminar)
return laminar + fraction**0.58 * (turbulent - laminar)
def darcy_pressure_drop(
self,
@@ -279,7 +300,11 @@ class AmesimPnl00r(AlgebraicComponent):
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
reynolds = self.reynolds_number(m_flow, upstream_temperature)
velocity = m_flow / (density * self.area)
cm = abs(m_flow) / max(self.area * upstream_pressure, 1.0e-18)
cm = (
abs(m_flow)
* sqrt(upstream_temperature)
/ max(self.area * upstream_pressure, 1.0e-18)
)
return {
"re": reynolds,
"cm": cm,
@@ -326,7 +351,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
"""AMESim PNL0001 C-R pneumatic pipe with compressibility and friction."""
MODEL_TYPE = "amesim_pnl0001"
MODEL_VERSION = "0.2.0"
MODEL_VERSION = "0.3.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
@@ -798,7 +823,11 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
"u": props.u,
"h": props.h,
"re": reynolds,
"cm": abs(flow) / max(self.area * upstream_pressure, 1.0e-18),
"cm": (
abs(flow)
* sqrt(props.T)
/ max(self.area * upstream_pressure, 1.0e-18)
),
"v": flow / (density * self.area),
"ff": self.friction_factor(reynolds),
}
@@ -861,7 +890,7 @@ class AmesimPnl0002(AmesimPnl0001):
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
MODEL_TYPE = "amesim_pnl0002"
MODEL_VERSION = "0.3.0"
MODEL_VERSION = "0.5.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
@@ -964,6 +993,20 @@ class AmesimPnl0002(AmesimPnl0001):
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self._connected_h = dict(connected_h)
def update_flow_temperature_references(
self,
connected_h: Mapping[str, float],
) -> None:
self._connected_h = dict(connected_h)
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
# Junctions allocate their energy-balanced outlet enthalpy per port.
# The separate cache is only the temperature input to pn2pipefr.
return super().state_derivative_from_ports(connected_h)
def component_result_values(self) -> Mapping[str, float]:
props = self.properties()
flow_1 = self.port_mass_flow(
@@ -978,10 +1021,45 @@ class AmesimPnl0002(AmesimPnl0001):
props.T,
port_name="port_2",
)
diagnostic_flow = flow_1 if abs(flow_1) >= abs(flow_2) else flow_2
upstream_pressure = max(self.port_1.p, self.port_2.p, props.p, 1.0)
density = max(self.medium.density(upstream_pressure, props.T), 1.0e-12)
reynolds = self.reynolds_number(diagnostic_flow, props.T)
resistance_diagnostics: list[tuple[float, float, float, float]] = []
for port_name, port, flow in (
("port_1", self.port_1, flow_1),
("port_2", self.port_2, flow_2),
):
if flow >= 0.0:
upstream_pressure = max(port.p, 1.0)
upstream_h = self._connected_h.get(port_name, props.h)
upstream_temperature = max(
self.medium.temperature_from_pressure_enthalpy(
upstream_pressure,
upstream_h,
),
1.0,
)
else:
upstream_pressure = max(props.p, 1.0)
upstream_temperature = props.T
density = max(
self.medium.density(upstream_pressure, upstream_temperature),
1.0e-12,
)
reynolds = self.reynolds_number(flow, upstream_temperature)
resistance_diagnostics.append(
(
reynolds,
(
abs(flow)
* sqrt(upstream_temperature)
/ max(self.area * upstream_pressure, 1.0e-18)
),
abs(flow) / (density * self.area),
self.friction_factor(reynolds),
)
)
reynolds, cm, velocity, friction = (
sum(values) / len(resistance_diagnostics)
for values in zip(*resistance_diagnostics)
)
return {
"m": self.state.m,
"U": self.state.U,
@@ -991,9 +1069,9 @@ class AmesimPnl0002(AmesimPnl0001):
"u": props.u,
"h": props.h,
"re": reynolds,
"cm": abs(diagnostic_flow) / max(self.area * upstream_pressure, 1.0e-18),
"v": diagnostic_flow / (density * self.area),
"ff": self.friction_factor(reynolds),
"cm": cm,
"v": velocity,
"ff": friction,
}
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
@@ -1045,7 +1123,7 @@ class AmesimPnl0003(DynamicComponent):
state_size = 4
MODEL_TYPE = "amesim_pnl0003"
MODEL_VERSION = "0.2.0"
MODEL_VERSION = "0.3.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
@@ -1318,7 +1396,11 @@ class AmesimPnl0003(DynamicComponent):
"h2": port_2.h,
"dmctr": center_flow,
"re": reynolds,
"cm": abs(center_flow) / max(self.area * max(port_1.p, port_2.p, 1.0), 1.0e-18),
"cm": (
abs(center_flow)
* sqrt(upstream.T)
/ max(self.area * max(port_1.p, port_2.p, 1.0), 1.0e-18)
),
"v": center_flow / (max(upstream.rho, 1.0e-12) * self.area),
"ff": self.friction_factor(reynolds),
}
@@ -10,13 +10,20 @@ from app.simulation.core.ports import PortDefinition, PortState
class _AmesimPneumaticNode(AlgebraicComponent):
"""Shared implementation for AMESim pneumatic junction submodels."""
"""Shared implementation for AMESim pneumatic junction submodels.
PN3NODE2/P4NODE2 use port 2 as their pressure and temperature reference.
Non-reference outlet ports use that reference temperature. When port 2 is
an outlet, its enthalpy is the residual that closes the junction energy
balance, matching the AMESim dh2 causality.
"""
REFERENCE_PORT = "port_2"
def __init__(self, name: str) -> None:
super().__init__(name=name)
self.set_parameter_values({})
self.temperature_reference_h = 0.0
for definition in self.PORTS:
setattr(self, definition.name, self.register_declared_port(definition.name))
@@ -58,6 +65,10 @@ class _AmesimPneumaticNode(AlgebraicComponent):
return tuple(residuals)
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
self.temperature_reference_h = connected_h.get(
self.REFERENCE_PORT,
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
)
incoming = [
(port.m_flow, connected_h[name])
for name, port in self.ports.items()
@@ -67,19 +78,37 @@ class _AmesimPneumaticNode(AlgebraicComponent):
if total_flow > 1e-12:
mixed_h = sum(m_flow * h for m_flow, h in incoming) / total_flow
else:
mixed_h = connected_h.get(
self.REFERENCE_PORT,
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
mixed_h = self.temperature_reference_h
reference_port = self.get_port(self.REFERENCE_PORT)
for name, port in self.ports.items():
port.h_outflow = (
mixed_h
if name == self.REFERENCE_PORT
else self.temperature_reference_h
)
if reference_port.m_flow < -1e-12:
energy_without_reference = sum(
port.m_flow
* (
connected_h[name]
if port.m_flow > 1e-12
else self.temperature_reference_h
)
for name, port in self.ports.items()
if name != self.REFERENCE_PORT
)
reference_port.h_outflow = (
-energy_without_reference / reference_port.m_flow
)
for port in self.ports.values():
port.h_outflow = mixed_h
class AmesimPn3Node2(_AmesimPneumaticNode):
"""AMESim PN3NODE2 pneumatic three-port junction."""
MODEL_TYPE = "amesim_pn3node2"
MODEL_VERSION = "0.1.0"
MODEL_VERSION = "0.3.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
@@ -115,7 +144,7 @@ class AmesimP4Node2(_AmesimPneumaticNode):
"""AMESim P4NODE2 pneumatic four-port junction."""
MODEL_TYPE = "amesim_p4node2"
MODEL_VERSION = "0.1.0"
MODEL_VERSION = "0.3.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
+13
View File
@@ -204,6 +204,19 @@ class Component(ABC):
return None
def update_flow_temperature_references(
self,
connected_h: Mapping[str, float],
) -> None:
"""Update enthalpy references used only by pressure-flow laws.
Most components use the normal stream enthalpy for both energy
transport and upstream-property evaluation. AMESim node submodels can
expose a distinct temperature reference, so the default is a no-op.
"""
return None
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
"""Return directed ``volume``/``volume_flow`` values by pneumatic port.
+25 -8
View File
@@ -60,6 +60,20 @@ class MechanicalConstraintGroup:
def _boundary_tolerance(bound: float) -> float:
return 1.0e-12 * max(abs(bound), 1.0)
@staticmethod
def _velocity_tolerance(velocity: float) -> float:
"""Treat only floating-point-scale motion as stationary at a stop.
Implicit solvers perturb every state while constructing a numerical
Jacobian. Around an ideal endstop those perturbations must not switch
the unilateral constraint on and off; doing so turns a zero constrained
acceleration into the full outward-force acceleration across a
machine-scale velocity delta. The tolerance is deliberately far below
MECMAS21's physical ``dvel`` threshold so real release motion is kept.
"""
return 1.0e-12 * max(abs(velocity), 1.0)
def reset_mode(self) -> None:
self.mode = "uninitialized"
@@ -131,6 +145,7 @@ class MechanicalConstraintGroup:
def _static_endstop_side(self, total_force: float) -> str | None:
position = self.representative.x
velocity = self.representative.v
velocity_tolerance = self._velocity_tolerance(velocity)
lower = self.lower_bound
upper = self.upper_bound
# MECMAS21's dvel is the friction stick threshold. Its discrete
@@ -138,14 +153,14 @@ class MechanicalConstraintGroup:
if (
lower is not None
and position <= lower + self._boundary_tolerance(lower)
and velocity <= 0.0
and velocity <= velocity_tolerance
and total_force <= 0.0
):
return "lower"
if (
upper is not None
and position >= upper - self._boundary_tolerance(upper)
and velocity >= 0.0
and velocity >= -velocity_tolerance
and total_force >= 0.0
):
return "upper"
@@ -489,6 +504,8 @@ class MechanicalStateReducer:
position_index = velocity_index + 1
previous_velocity = float(previous_state[velocity_index])
current_velocity = float(current_state[velocity_index])
previous_velocity_tolerance = group._velocity_tolerance(previous_velocity)
current_velocity_tolerance = group._velocity_tolerance(current_velocity)
previous_position = float(previous_state[position_index])
current_position = float(current_state[position_index])
lower = group.lower_bound
@@ -496,7 +513,7 @@ class MechanicalStateReducer:
if (
lower is not None
and previous_position <= lower + group._boundary_tolerance(lower)
and previous_velocity < 0.0
and previous_velocity < -previous_velocity_tolerance
):
candidates.append((previous_time, group, "lower", lower))
elif (
@@ -522,8 +539,8 @@ class MechanicalStateReducer:
elif (
lower is not None
and previous_position <= lower
and previous_velocity > 0.0
and current_velocity < 0.0
and previous_velocity > previous_velocity_tolerance
and current_velocity < -current_velocity_tolerance
and current_position <= lower
):
turnaround_time = self._locate_turnaround(
@@ -551,7 +568,7 @@ class MechanicalStateReducer:
if (
upper is not None
and previous_position >= upper - group._boundary_tolerance(upper)
and previous_velocity > 0.0
and previous_velocity > previous_velocity_tolerance
):
candidates.append((previous_time, group, "upper", upper))
elif (
@@ -577,8 +594,8 @@ class MechanicalStateReducer:
elif (
upper is not None
and previous_position >= upper
and previous_velocity < 0.0
and current_velocity > 0.0
and previous_velocity < -previous_velocity_tolerance
and current_velocity > current_velocity_tolerance
and current_position >= upper
):
turnaround_time = self._locate_turnaround(
+20
View File
@@ -63,6 +63,26 @@ class StreamResolver:
values[endpoint.component][endpoint.port] = connected_port.h_outflow
return values
def connected_temperature_reference_enthalpies(
self,
) -> dict[str, dict[str, float]]:
"""Return connector references used for upstream temperature only."""
values: dict[str, dict[str, float]] = {
component.name: {} for component in self.network.components.values()
}
for endpoint, connected in self._connected_endpoint.items():
connected_component = self.network.components[connected.component]
connected_port = connected_component.get_port(connected.port)
values[endpoint.component][endpoint.port] = float(
getattr(
connected_component,
"temperature_reference_h",
connected_port.h_outflow,
)
)
return values
def solve(self) -> tuple[StreamSolveDiagnostics, dict[str, dict[str, float]]]:
dynamic_components = [
component
+6
View File
@@ -313,8 +313,14 @@ class GenericFluidSystem:
for coupling_iteration in range(1, max_coupling_iterations + 1):
previous_flows = tuple(port.m_flow for port in physical_ports)
stream, connected_h = self.stream_resolver.solve()
temperature_reference_h = (
self.stream_resolver.connected_temperature_reference_enthalpies()
)
for component in self.dynamic_components:
component.update_stream_outflows(connected_h[component.name])
component.update_flow_temperature_references(
temperature_reference_h[component.name]
)
algebraic = self.pressure_flow_solver.solve()
pressure_flow_solve_count += 1
current_flows = tuple(port.m_flow for port in physical_ports)