完善AMESim组件界面与仿真求解稳定性
This commit is contained in:
1 parent
e7177ab03e
commit
410ef535e8
34 files changed
+3340
-251
No files matched your search
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import sqrt
|
||||
from math import isfinite, sqrt
|
||||
|
||||
from app.simulation.core.ports import PortState, VariableRole
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
@@ -68,6 +68,7 @@ class PressureFlowSolver:
|
||||
self.residual_tolerance = residual_tolerance
|
||||
self.max_evaluations = max_evaluations
|
||||
self.unknowns = self._build_unknowns()
|
||||
self._unknowns_by_id = {unknown.id: unknown for unknown in self.unknowns}
|
||||
self.last_diagnostics: AlgebraicSolveDiagnostics | None = None
|
||||
|
||||
def _build_unknowns(self) -> tuple[AlgebraicUnknown, ...]:
|
||||
@@ -91,48 +92,220 @@ class PressureFlowSolver:
|
||||
)
|
||||
return tuple(unknowns)
|
||||
|
||||
def _seed_equal_pressures(self) -> None:
|
||||
for _ in range(max(2, len(self.network.connections))):
|
||||
changed = False
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
first = self.network.components[
|
||||
connection.endpoint_a.component
|
||||
].get_port(connection.endpoint_a.port)
|
||||
second = self.network.components[
|
||||
connection.endpoint_b.component
|
||||
].get_port(connection.endpoint_b.port)
|
||||
if first.p > 0.0 and second.p <= 0.0:
|
||||
second.p = first.p
|
||||
changed = True
|
||||
elif second.p > 0.0 and first.p <= 0.0:
|
||||
first.p = second.p
|
||||
changed = True
|
||||
@staticmethod
|
||||
def _port_key(variable: str, expected_variable: str) -> tuple[str, str] | None:
|
||||
try:
|
||||
component_name, port_name, variable_name = variable.rsplit(".", 2)
|
||||
except ValueError:
|
||||
return None
|
||||
if variable_name != expected_variable:
|
||||
return None
|
||||
return component_name, port_name
|
||||
|
||||
for component in self.network.components.values():
|
||||
equal_pressure_equations = [
|
||||
equation
|
||||
for equation in component.pressure_flow_equation_residuals()
|
||||
if equation.relation == "equal" and equation.role == "effort"
|
||||
def _seed_equal_pressures(self) -> None:
|
||||
"""Lift current state pressures across their complete equality groups.
|
||||
|
||||
Dynamic components refresh their own pressure ports before each closure,
|
||||
while connected algebraic ports retain values from the preceding RHS
|
||||
evaluation. Merely filling non-positive pressures therefore leaves a
|
||||
stale, and sometimes badly conditioned, nonlinear initial guess. State
|
||||
equations expose the current pressure as ``port.p - target``; use that
|
||||
target as the authoritative anchor for every connected/equal port.
|
||||
"""
|
||||
|
||||
pressure_unknowns = {
|
||||
(unknown.component, unknown.port): unknown
|
||||
for unknown in self.unknowns
|
||||
if unknown.variable == "p"
|
||||
}
|
||||
if not pressure_unknowns:
|
||||
return
|
||||
|
||||
parent = {key: key for key in pressure_unknowns}
|
||||
|
||||
def find(key: tuple[str, str]) -> tuple[str, str]:
|
||||
root = key
|
||||
while parent[root] != root:
|
||||
root = parent[root]
|
||||
while parent[key] != key:
|
||||
next_key = parent[key]
|
||||
parent[key] = root
|
||||
key = next_key
|
||||
return root
|
||||
|
||||
def union(first: tuple[str, str], second: tuple[str, str]) -> None:
|
||||
first_root = find(first)
|
||||
second_root = find(second)
|
||||
if first_root != second_root:
|
||||
parent[second_root] = first_root
|
||||
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
first = connection.endpoint_a.key
|
||||
second = connection.endpoint_b.key
|
||||
if first in pressure_unknowns and second in pressure_unknowns:
|
||||
union(first, second)
|
||||
|
||||
component_equations = {
|
||||
component.name: component.pressure_flow_equation_residuals()
|
||||
for component in self.network.components.values()
|
||||
}
|
||||
for equations in component_equations.values():
|
||||
for equation in equations:
|
||||
if equation.relation != "equal" or equation.role != "effort":
|
||||
continue
|
||||
endpoints = [
|
||||
endpoint
|
||||
for variable in equation.variables
|
||||
if (
|
||||
(endpoint := self._port_key(variable, "p"))
|
||||
in pressure_unknowns
|
||||
)
|
||||
]
|
||||
for equation in equal_pressure_equations:
|
||||
states = []
|
||||
for variable in equation.variables:
|
||||
_, port_name, variable_name = variable.rsplit(".", 2)
|
||||
if variable_name == "p":
|
||||
states.append(component.get_port(port_name))
|
||||
if len(states) != 2:
|
||||
continue
|
||||
first, second = states
|
||||
if first.p > 0.0 and second.p <= 0.0:
|
||||
second.p = first.p
|
||||
changed = True
|
||||
elif second.p > 0.0 and first.p <= 0.0:
|
||||
first.p = second.p
|
||||
changed = True
|
||||
if not changed:
|
||||
break
|
||||
for endpoint in endpoints[1:]:
|
||||
union(endpoints[0], endpoint)
|
||||
|
||||
members_by_root: dict[tuple[str, str], list[tuple[str, str]]] = {}
|
||||
for endpoint in pressure_unknowns:
|
||||
members_by_root.setdefault(find(endpoint), []).append(endpoint)
|
||||
|
||||
anchors_by_root: dict[tuple[str, str], list[float]] = {}
|
||||
for equations in component_equations.values():
|
||||
for equation in equations:
|
||||
if equation.relation != "state" or equation.role != "effort":
|
||||
continue
|
||||
endpoints = [
|
||||
endpoint
|
||||
for variable in equation.variables
|
||||
if (
|
||||
(endpoint := self._port_key(variable, "p"))
|
||||
in pressure_unknowns
|
||||
)
|
||||
]
|
||||
if len(endpoints) != 1:
|
||||
continue
|
||||
endpoint = endpoints[0]
|
||||
unknown = pressure_unknowns[endpoint]
|
||||
target_pressure = unknown.read() - float(equation.value)
|
||||
if not isfinite(target_pressure):
|
||||
continue
|
||||
# Keep the state-owned port current even when an invalid model
|
||||
# has conflicting storage anchors in one equality group.
|
||||
unknown.write(target_pressure)
|
||||
anchors_by_root.setdefault(find(endpoint), []).append(target_pressure)
|
||||
|
||||
for root, members in members_by_root.items():
|
||||
anchors = anchors_by_root.get(root, [])
|
||||
if anchors:
|
||||
pressure_scale = max([abs(value) for value in anchors] + [1.0])
|
||||
if max(anchors) - min(anchors) > 1.0e-9 * pressure_scale:
|
||||
# A conflicting multi-storage group is structurally invalid;
|
||||
# leave it for the residual solver/preparation diagnostics.
|
||||
continue
|
||||
target_pressure = sum(anchors) / len(anchors)
|
||||
for endpoint in members:
|
||||
pressure_unknowns[endpoint].write(target_pressure)
|
||||
continue
|
||||
|
||||
positive_seed = next(
|
||||
(
|
||||
pressure_unknowns[endpoint].read()
|
||||
for endpoint in members
|
||||
if pressure_unknowns[endpoint].read() > 0.0
|
||||
),
|
||||
None,
|
||||
)
|
||||
if positive_seed is None:
|
||||
continue
|
||||
for endpoint in members:
|
||||
unknown = pressure_unknowns[endpoint]
|
||||
if unknown.read() <= 0.0:
|
||||
unknown.write(positive_seed)
|
||||
|
||||
def _seed_explicit_mass_flows(self) -> None:
|
||||
"""Initialize explicit ``m_flow - f(...)`` constitutive relations.
|
||||
|
||||
AMESim orifices and quasi-steady pneumatic lines expose one mass-flow
|
||||
unknown with unit coefficient. Once pressure anchors are current, a
|
||||
residual correction places that flow directly on its constitutive
|
||||
surface and avoids asking the nonlinear optimizer to discover the
|
||||
square-root branch from a stale preceding-step value.
|
||||
"""
|
||||
|
||||
seeded_ids: set[str] = set()
|
||||
for component in self.network.components.values():
|
||||
for equation in component.pressure_flow_equation_residuals():
|
||||
if equation.relation != "constitutive" or equation.role != "flow":
|
||||
continue
|
||||
mass_flow_unknowns = [
|
||||
self._unknowns_by_id[variable]
|
||||
for variable in equation.variables
|
||||
if variable in self._unknowns_by_id
|
||||
and self._unknowns_by_id[variable].variable == "m_flow"
|
||||
]
|
||||
if len(mass_flow_unknowns) != 1:
|
||||
continue
|
||||
unknown = mass_flow_unknowns[0]
|
||||
target_flow = unknown.read() - float(equation.value)
|
||||
if not isfinite(target_flow):
|
||||
continue
|
||||
unknown.write(target_flow)
|
||||
seeded_ids.add(unknown.id)
|
||||
|
||||
# Complete local two-port balances for explicit elements. Connection
|
||||
# flow equations remain available to align the adjacent component port.
|
||||
for component in self.network.components.values():
|
||||
for equation in component.pressure_flow_equation_residuals():
|
||||
if equation.relation != "sumToZero" or equation.role != "flow":
|
||||
continue
|
||||
mass_flow_unknowns = [
|
||||
self._unknowns_by_id[variable]
|
||||
for variable in equation.variables
|
||||
if variable in self._unknowns_by_id
|
||||
and self._unknowns_by_id[variable].variable == "m_flow"
|
||||
]
|
||||
if len(mass_flow_unknowns) != 2:
|
||||
continue
|
||||
seeded = [
|
||||
unknown for unknown in mass_flow_unknowns if unknown.id in seeded_ids
|
||||
]
|
||||
if len(seeded) != 1:
|
||||
continue
|
||||
other = next(
|
||||
unknown for unknown in mass_flow_unknowns if unknown.id not in seeded_ids
|
||||
)
|
||||
other.write(-seeded[0].read())
|
||||
seeded_ids.add(other.id)
|
||||
|
||||
# A physical connector imposes the same sum-to-zero flow rule as a
|
||||
# two-port component. Once an explicit component flow is known, carry
|
||||
# that guess to the connected storage/boundary port as well. For the
|
||||
# common volume-orifice-volume topology this makes the seeded state an
|
||||
# exact algebraic solution and avoids an unnecessary nonlinear solve on
|
||||
# every ODE/Jacobian evaluation.
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
endpoint_unknowns = []
|
||||
for endpoint in connection.endpoints:
|
||||
unknown = self._unknowns_by_id.get(
|
||||
f"{endpoint.component}.{endpoint.port}.m_flow"
|
||||
)
|
||||
if unknown is not None:
|
||||
endpoint_unknowns.append(unknown)
|
||||
if len(endpoint_unknowns) != 2:
|
||||
continue
|
||||
seeded = [
|
||||
unknown for unknown in endpoint_unknowns if unknown.id in seeded_ids
|
||||
]
|
||||
if len(seeded) != 1:
|
||||
continue
|
||||
other = next(
|
||||
unknown for unknown in endpoint_unknowns if unknown.id not in seeded_ids
|
||||
)
|
||||
other.write(-seeded[0].read())
|
||||
seeded_ids.add(other.id)
|
||||
|
||||
def _scales(self) -> dict[str, float]:
|
||||
pressure_scale = max(
|
||||
@@ -184,6 +357,7 @@ class PressureFlowSolver:
|
||||
) from exc
|
||||
|
||||
self._seed_equal_pressures()
|
||||
self._seed_explicit_mass_flows()
|
||||
scales = self._scales()
|
||||
pressure_scale = scales["p"]
|
||||
flow_scale = scales["m_flow"]
|
||||
@@ -216,6 +390,40 @@ class PressureFlowSolver:
|
||||
return pressure_scale
|
||||
return max([scales.get(name, 1.0) for name in variable_names] + [1.0])
|
||||
|
||||
seeded_equations = self.network.pressure_flow_equation_residuals()
|
||||
seeded_scaled = [
|
||||
abs(equation.value / equation_scale(equation))
|
||||
for equation in seeded_equations
|
||||
]
|
||||
seeded_max_scaled_residual = max(seeded_scaled, default=0.0)
|
||||
seeded_unknown_values = [
|
||||
(unknown, unknown.read()) for unknown in self.unknowns
|
||||
]
|
||||
seeded_unknowns_are_feasible = all(
|
||||
isfinite(value)
|
||||
and (unknown.variable != "p" or value >= 1.0)
|
||||
for unknown, value in seeded_unknown_values
|
||||
)
|
||||
if (
|
||||
seeded_unknowns_are_feasible
|
||||
and all(isfinite(value) for value in seeded_scaled)
|
||||
and seeded_max_scaled_residual <= self.residual_tolerance
|
||||
):
|
||||
diagnostics = AlgebraicSolveDiagnostics(
|
||||
success=True,
|
||||
message="Seeded pressure-flow state satisfies the residual tolerance.",
|
||||
evaluations=0,
|
||||
pressure_scale=pressure_scale,
|
||||
flow_scale=flow_scale,
|
||||
max_scaled_residual=seeded_max_scaled_residual,
|
||||
max_raw_residual=max(
|
||||
(abs(item.value) for item in seeded_equations),
|
||||
default=0.0,
|
||||
),
|
||||
)
|
||||
self.last_diagnostics = diagnostics
|
||||
return diagnostics
|
||||
|
||||
x0 = np.asarray(
|
||||
[
|
||||
(
|
||||
@@ -269,14 +477,20 @@ class PressureFlowSolver:
|
||||
)
|
||||
for equation in equations
|
||||
]
|
||||
success = bool(result.success) and max(scaled, default=0.0) <= self.residual_tolerance
|
||||
max_scaled_residual = max(scaled, default=0.0)
|
||||
residuals_converged = (
|
||||
all(isfinite(value) for value in scaled)
|
||||
and max_scaled_residual <= self.residual_tolerance
|
||||
)
|
||||
optimizer_status_is_acceptable = bool(result.success) or int(result.status) == 0
|
||||
success = residuals_converged and optimizer_status_is_acceptable
|
||||
diagnostics = AlgebraicSolveDiagnostics(
|
||||
success=success,
|
||||
message=str(result.message),
|
||||
evaluations=int(result.nfev),
|
||||
pressure_scale=pressure_scale,
|
||||
flow_scale=flow_scale,
|
||||
max_scaled_residual=max(scaled, default=0.0),
|
||||
max_scaled_residual=max_scaled_residual,
|
||||
max_raw_residual=max((abs(item.value) for item in equations), default=0.0),
|
||||
)
|
||||
self.last_diagnostics = diagnostics
|
||||
|
||||
Reference in new issue
Block a user