优化压力流量求解并达到四路性能门槛
This commit is contained in:
1 parent
6572defaa4
commit
6a064892e2
20 files changed
+710
-174
No files matched your search
@@ -46,6 +46,9 @@ class AmesimPnpl01(AlgebraicComponent):
|
|||||||
) -> AmesimPnpl01:
|
) -> AmesimPnpl01:
|
||||||
return cls(name=name)
|
return cls(name=name)
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (self.port_1.m_flow,)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
EquationResidual(
|
EquationResidual(
|
||||||
|
|||||||
@@ -410,6 +410,12 @@ class AmesimPnor001(AlgebraicComponent):
|
|||||||
"gasvel": flow_direction * gas_velocity,
|
"gasvel": flow_direction * gas_velocity,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (
|
||||||
|
self.port_1.m_flow + self.port_2.m_flow,
|
||||||
|
self.port_1.m_flow - self.mass_flow(self.port_1.p, self.port_2.p),
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
EquationResidual(
|
EquationResidual(
|
||||||
@@ -832,6 +838,12 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
|||||||
"gasvel": flow_direction * gas_velocity,
|
"gasvel": flow_direction * gas_velocity,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (
|
||||||
|
self.port_2.m_flow + self.port_3.m_flow,
|
||||||
|
self.port_2.m_flow - self.mass_flow(self.port_2.p, self.port_3.p),
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
EquationResidual(
|
EquationResidual(
|
||||||
|
|||||||
@@ -317,6 +317,12 @@ class AmesimPnl00r(AlgebraicComponent):
|
|||||||
"ff": self.friction_factor(reynolds),
|
"ff": self.friction_factor(reynolds),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (
|
||||||
|
self.port_1.m_flow + self.port_2.m_flow,
|
||||||
|
self.port_1.m_flow - self.mass_flow(self.port_1.p, self.port_2.p),
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
EquationResidual(
|
EquationResidual(
|
||||||
@@ -838,6 +844,13 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
|
|||||||
"ff": self.friction_factor(reynolds),
|
"ff": self.friction_factor(reynolds),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||||
|
return (
|
||||||
|
self.port_2.p - props.p,
|
||||||
|
self.port_1.m_flow - self.mass_flow(self.port_1.p, props.p, props.T),
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||||
return (
|
return (
|
||||||
@@ -1080,6 +1093,25 @@ class AmesimPnl0002(AmesimPnl0001):
|
|||||||
"ff": friction,
|
"ff": friction,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||||
|
return (
|
||||||
|
self.port_1.m_flow
|
||||||
|
- self.port_mass_flow(
|
||||||
|
self.port_1.p,
|
||||||
|
props.p,
|
||||||
|
props.T,
|
||||||
|
port_name="port_1",
|
||||||
|
),
|
||||||
|
self.port_2.m_flow
|
||||||
|
- self.port_mass_flow(
|
||||||
|
self.port_2.p,
|
||||||
|
props.p,
|
||||||
|
props.T,
|
||||||
|
port_name="port_2",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||||
return (
|
return (
|
||||||
@@ -1411,6 +1443,14 @@ class AmesimPnl0003(DynamicComponent):
|
|||||||
"ff": self.friction_factor(reynolds),
|
"ff": self.friction_factor(reynolds),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
port_1 = self._properties(self.state_1)
|
||||||
|
port_2 = self._properties(self.state_2)
|
||||||
|
return (
|
||||||
|
self.port_1.p - port_1.p,
|
||||||
|
self.port_2.p - port_2.p,
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
port_1 = self._properties(self.state_1)
|
port_1 = self._properties(self.state_1)
|
||||||
port_2 = self._properties(self.state_2)
|
port_2 = self._properties(self.state_2)
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ class _AmesimPneumaticNode(AlgebraicComponent):
|
|||||||
for definition in self.PORTS:
|
for definition in self.PORTS:
|
||||||
setattr(self, definition.name, self.register_declared_port(definition.name))
|
setattr(self, definition.name, self.register_declared_port(definition.name))
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
reference = self.get_port(self.REFERENCE_PORT)
|
||||||
|
return tuple(
|
||||||
|
self.get_port(definition.name).p - reference.p
|
||||||
|
for definition in self.PORTS
|
||||||
|
if definition.name != self.REFERENCE_PORT
|
||||||
|
) + (
|
||||||
|
sum(
|
||||||
|
self.get_port(definition.name).m_flow
|
||||||
|
for definition in self.PORTS
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
reference = self.get_port(self.REFERENCE_PORT)
|
reference = self.get_port(self.REFERENCE_PORT)
|
||||||
residuals: list[EquationResidual] = []
|
residuals: list[EquationResidual] = []
|
||||||
|
|||||||
@@ -154,6 +154,22 @@ class AmesimPnrp17(AlgebraicComponent):
|
|||||||
def pressure_force(self) -> float:
|
def pressure_force(self) -> float:
|
||||||
return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area
|
return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
values = [self.port_1.m_flow]
|
||||||
|
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||||
|
for first_name, second_name in effort_pairs:
|
||||||
|
first = self.get_port(first_name)
|
||||||
|
second = self.get_port(second_name)
|
||||||
|
values.extend((first.x - second.x, first.v - second.v))
|
||||||
|
force = self.pressure_force
|
||||||
|
values.extend(
|
||||||
|
(
|
||||||
|
self.port_2.f + self.port_5.f + force,
|
||||||
|
self.port_3.f + self.port_4.f - force,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||||
residuals: list[EquationResidual] = [
|
residuals: list[EquationResidual] = [
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class AmesimF000(AlgebraicComponent):
|
|||||||
) -> "AmesimF000":
|
) -> "AmesimF000":
|
||||||
return cls(name=name)
|
return cls(name=name)
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (self.port_1.f,)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
EquationResidual(
|
EquationResidual(
|
||||||
@@ -140,6 +143,11 @@ class AmesimForc(AlgebraicComponent):
|
|||||||
def output_force(self) -> float:
|
def output_force(self) -> float:
|
||||||
return self.direction * float(self.res.signal)
|
return self.direction * float(self.res.signal)
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (
|
||||||
|
self.port_2.f + self.output_force,
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
EquationResidual(
|
EquationResidual(
|
||||||
@@ -591,6 +599,14 @@ class AmesimMecmas21(DynamicComponent):
|
|||||||
port.x = self.x
|
port.x = self.x
|
||||||
port.v = self.v
|
port.v = self.v
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
return (
|
||||||
|
self.port_1.x - self.x,
|
||||||
|
self.port_1.v - self.v,
|
||||||
|
self.port_2.x - self.x,
|
||||||
|
self.port_2.v - self.v,
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
return (
|
return (
|
||||||
self._state_residual("port_1", "x", self.port_1.x - self.x),
|
self._state_residual("port_1", "x", self.port_1.x - self.x),
|
||||||
@@ -992,6 +1008,13 @@ class AmesimLstp00a(AlgebraicComponent):
|
|||||||
self._causal_port_1_v = float(self.port_1.v)
|
self._causal_port_1_v = float(self.port_1.v)
|
||||||
self._causal_port_2_v = float(self.port_2.v)
|
self._causal_port_2_v = float(self.port_2.v)
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
force = self.contact_force
|
||||||
|
return (
|
||||||
|
self.port_1.f - force,
|
||||||
|
self.port_2.f + force,
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
force = self.contact_force
|
force = self.contact_force
|
||||||
return (
|
return (
|
||||||
@@ -1110,6 +1133,10 @@ class AmesimLmechn1(AlgebraicComponent):
|
|||||||
def active_ports(self) -> tuple[str, ...]:
|
def active_ports(self) -> tuple[str, ...]:
|
||||||
return tuple(f"port_{index}" for index in range(1, self.v1 + 2))
|
return tuple(f"port_{index}" for index in range(1, self.v1 + 2))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
||||||
|
return self.PORTS[: self.v1 + 1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reference_port_name(self) -> str:
|
def reference_port_name(self) -> str:
|
||||||
return f"port_{self.v1 + 1}"
|
return f"port_{self.v1 + 1}"
|
||||||
@@ -1129,6 +1156,15 @@ class AmesimLmechn1(AlgebraicComponent):
|
|||||||
def force_balance(self) -> float:
|
def force_balance(self) -> float:
|
||||||
return self.total_force + self.get_port(self.reference_port_name).f
|
return self.total_force + self.get_port(self.reference_port_name).f
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
reference = self.get_port(self.reference_port_name)
|
||||||
|
values: list[float] = []
|
||||||
|
for port_name in self.active_ports[:-1]:
|
||||||
|
port = self.get_port(port_name)
|
||||||
|
values.extend((port.x - reference.x, port.v - reference.v))
|
||||||
|
values.append(self.force_balance)
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
reference_name = self.reference_port_name
|
reference_name = self.reference_port_name
|
||||||
reference = self.get_port(reference_name)
|
reference = self.get_port(reference_name)
|
||||||
@@ -1168,39 +1204,6 @@ class AmesimLmechn1(AlgebraicComponent):
|
|||||||
value=self.force_balance,
|
value=self.force_balance,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for definition in self.PORTS[self.v1 + 1 :]:
|
|
||||||
port = self.get_port(definition.name)
|
|
||||||
residuals.extend(
|
|
||||||
(
|
|
||||||
EquationResidual(
|
|
||||||
id=f"{self.name}:{definition.name}_inactive_x",
|
|
||||||
owner="component",
|
|
||||||
owner_id=self.name,
|
|
||||||
relation="constitutive",
|
|
||||||
variables=(f"{self.name}.{definition.name}.x",),
|
|
||||||
role="effort",
|
|
||||||
value=port.x,
|
|
||||||
),
|
|
||||||
EquationResidual(
|
|
||||||
id=f"{self.name}:{definition.name}_inactive_v",
|
|
||||||
owner="component",
|
|
||||||
owner_id=self.name,
|
|
||||||
relation="constitutive",
|
|
||||||
variables=(f"{self.name}.{definition.name}.v",),
|
|
||||||
role="effort",
|
|
||||||
value=port.v,
|
|
||||||
),
|
|
||||||
EquationResidual(
|
|
||||||
id=f"{self.name}:{definition.name}_inactive_force",
|
|
||||||
owner="component",
|
|
||||||
owner_id=self.name,
|
|
||||||
relation="constitutive",
|
|
||||||
variables=(f"{self.name}.{definition.name}.f",),
|
|
||||||
role="flow",
|
|
||||||
value=port.f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return tuple(residuals)
|
return tuple(residuals)
|
||||||
|
|
||||||
def component_result_values(self) -> Mapping[str, float]:
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
|
|||||||
@@ -217,6 +217,17 @@ class AmesimPnch023(ThermodynamicVolumeComponent):
|
|||||||
)
|
)
|
||||||
return derivative.as_vector()
|
return derivative.as_vector()
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
pressure = self.medium.properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.cvol,
|
||||||
|
).p
|
||||||
|
return (
|
||||||
|
self.port_1.p - pressure,
|
||||||
|
self.port_2.p - pressure,
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
pressure = self.medium.properties_from_mU(
|
pressure = self.medium.properties_from_mU(
|
||||||
self.state.m,
|
self.state.m,
|
||||||
@@ -507,6 +518,17 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
|||||||
energy_derivative -= props.p * self.total_volume_rate()
|
energy_derivative -= props.p * self.total_volume_rate()
|
||||||
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
pressure = self.medium.properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.total_volume(),
|
||||||
|
).p
|
||||||
|
return tuple(
|
||||||
|
self.get_port(port_name).p - pressure
|
||||||
|
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
||||||
|
)
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
pressure = self.medium.properties_from_mU(
|
pressure = self.medium.properties_from_mU(
|
||||||
self.state.m,
|
self.state.m,
|
||||||
|
|||||||
@@ -44,13 +44,19 @@ class Component(ABC):
|
|||||||
if port.definition is not None
|
if port.definition is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
||||||
|
"""Instance ports that participate in execution and result reporting."""
|
||||||
|
|
||||||
|
return self.port_definitions
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def required_connection_ports(self) -> tuple[str, ...]:
|
def required_connection_ports(self) -> tuple[str, ...]:
|
||||||
"""Physical ports that must have an external connection before simulation."""
|
"""Physical ports that must have an external connection before simulation."""
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
definition.name
|
definition.name
|
||||||
for definition in self.port_definitions
|
for definition in self.active_port_definitions
|
||||||
if definition.kind == "physical"
|
if definition.kind == "physical"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,7 +140,7 @@ class Component(ABC):
|
|||||||
)
|
)
|
||||||
values[name] = float(component_values[name])
|
values[name] = float(component_values[name])
|
||||||
|
|
||||||
for port_definition in self.port_definitions:
|
for port_definition in self.active_port_definitions:
|
||||||
port = self.get_port(port_definition.name)
|
port = self.get_port(port_definition.name)
|
||||||
for variable in port_definition.variables:
|
for variable in port_definition.variables:
|
||||||
if not variable.result_visible:
|
if not variable.result_visible:
|
||||||
@@ -161,7 +167,7 @@ class Component(ABC):
|
|||||||
for definition in self.RESULT_VARIABLES
|
for definition in self.RESULT_VARIABLES
|
||||||
if definition.visible
|
if definition.visible
|
||||||
]
|
]
|
||||||
for port_definition in self.port_definitions:
|
for port_definition in self.active_port_definitions:
|
||||||
for variable in port_definition.variables:
|
for variable in port_definition.variables:
|
||||||
if not variable.result_visible:
|
if not variable.result_visible:
|
||||||
continue
|
continue
|
||||||
@@ -209,6 +215,20 @@ class Component(ABC):
|
|||||||
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
"""Return live residual values in the declared equation order.
|
||||||
|
|
||||||
|
Components with frequently evaluated equations can override this
|
||||||
|
method to avoid rebuilding immutable equation metadata during closure.
|
||||||
|
The default keeps third-party components compatible with the public
|
||||||
|
residual API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
float(equation.value)
|
||||||
|
for equation in self.pressure_flow_equation_residuals()
|
||||||
|
)
|
||||||
|
|
||||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
"""Update connector outflow properties from current flow directions."""
|
"""Update connector outflow properties from current flow directions."""
|
||||||
|
|
||||||
|
|||||||
+270
-124
@@ -54,6 +54,23 @@ class ExplicitFlowAssignment:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ExplicitFlowStage:
|
class ExplicitFlowStage:
|
||||||
assignments: tuple[ExplicitFlowAssignment, ...]
|
assignments: tuple[ExplicitFlowAssignment, ...]
|
||||||
|
direct_evaluations: tuple[tuple[int, Callable[[], float]], ...]
|
||||||
|
component_evaluations: tuple["ExplicitFlowComponentEvaluation", ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExplicitFlowComponentEvaluation:
|
||||||
|
component: object
|
||||||
|
evaluate: Callable[[], tuple[float, ...]]
|
||||||
|
assignment_indices: tuple[int, ...]
|
||||||
|
equation_indices: tuple[int, ...]
|
||||||
|
equation_ids: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EquationScalePlan:
|
||||||
|
variable_names: tuple[str, ...]
|
||||||
|
force_unknowns: tuple[AlgebraicUnknown, ...]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -68,6 +85,13 @@ class ConnectionEquationEvaluation:
|
|||||||
evaluate: Callable[[], float]
|
evaluate: Callable[[], float]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ComponentEquationEvaluation:
|
||||||
|
component: object
|
||||||
|
evaluate: Callable[[], tuple[float, ...]]
|
||||||
|
templates: tuple[EquationResidual, ...]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PnorPnl0001SeriesBinding:
|
class PnorPnl0001SeriesBinding:
|
||||||
orifice: AmesimPnor001
|
orifice: AmesimPnor001
|
||||||
@@ -147,6 +171,18 @@ class PressureFlowSolver:
|
|||||||
for variable in ("p", "m_flow", "x", "v", "f")
|
for variable in ("p", "m_flow", "x", "v", "f")
|
||||||
}
|
}
|
||||||
self._component_equation_owners = tuple(network.components.values())
|
self._component_equation_owners = tuple(network.components.values())
|
||||||
|
self._component_equation_plan = tuple(
|
||||||
|
ComponentEquationEvaluation(
|
||||||
|
component=component,
|
||||||
|
evaluate=component.pressure_flow_equation_values,
|
||||||
|
templates=component.pressure_flow_equation_residuals(),
|
||||||
|
)
|
||||||
|
for component in self._component_equation_owners
|
||||||
|
)
|
||||||
|
self._component_equation_plans_by_id = {
|
||||||
|
item.component.name: item
|
||||||
|
for item in self._component_equation_plan
|
||||||
|
}
|
||||||
self._estimated_flow_components = tuple(
|
self._estimated_flow_components = tuple(
|
||||||
component
|
component
|
||||||
for component in self._component_equation_owners
|
for component in self._component_equation_owners
|
||||||
@@ -164,6 +200,11 @@ class PressureFlowSolver:
|
|||||||
)
|
)
|
||||||
for equation in network.connection_equation_residuals()
|
for equation in network.connection_equation_residuals()
|
||||||
)
|
)
|
||||||
|
self._equation_templates = tuple(
|
||||||
|
equation
|
||||||
|
for item in self._component_equation_plan
|
||||||
|
for equation in item.templates
|
||||||
|
) + tuple(item.template for item in self._connection_equation_plan)
|
||||||
self._effort_groups = {
|
self._effort_groups = {
|
||||||
variable: self._build_effort_equality_groups(variable)
|
variable: self._build_effort_equality_groups(variable)
|
||||||
for variable in ("p", "x", "v")
|
for variable in ("p", "x", "v")
|
||||||
@@ -176,12 +217,19 @@ class PressureFlowSolver:
|
|||||||
)
|
)
|
||||||
self._unilateral_contact_plan = self._build_unilateral_contact_plan()
|
self._unilateral_contact_plan = self._build_unilateral_contact_plan()
|
||||||
self._explicit_flow_plan = self._build_explicit_flow_plan()
|
self._explicit_flow_plan = self._build_explicit_flow_plan()
|
||||||
|
self._explicit_flow_plans_by_variables = {
|
||||||
|
selected: self._filter_explicit_flow_plan(selected)
|
||||||
|
for selected in (frozenset(("f", "m_flow")), frozenset(("f",)))
|
||||||
|
}
|
||||||
|
self._equation_scale_plans = self._build_equation_scale_plans(
|
||||||
|
self._equation_templates
|
||||||
|
)
|
||||||
self.last_diagnostics: AlgebraicSolveDiagnostics | None = None
|
self.last_diagnostics: AlgebraicSolveDiagnostics | None = None
|
||||||
|
|
||||||
def _build_unknowns(self) -> tuple[AlgebraicUnknown, ...]:
|
def _build_unknowns(self) -> tuple[AlgebraicUnknown, ...]:
|
||||||
unknowns: list[AlgebraicUnknown] = []
|
unknowns: list[AlgebraicUnknown] = []
|
||||||
for component in self.network.components.values():
|
for component in self.network.components.values():
|
||||||
for definition in component.port_definitions:
|
for definition in component.active_port_definitions:
|
||||||
if definition.kind != "physical":
|
if definition.kind != "physical":
|
||||||
continue
|
continue
|
||||||
state = component.get_port(definition.name)
|
state = component.get_port(definition.name)
|
||||||
@@ -277,8 +325,8 @@ class PressureFlowSolver:
|
|||||||
union(first, second)
|
union(first, second)
|
||||||
|
|
||||||
component_equations = {
|
component_equations = {
|
||||||
component.name: component.pressure_flow_equation_residuals()
|
item.component.name: item.templates
|
||||||
for component in self.network.components.values()
|
for item in self._component_equation_plan
|
||||||
}
|
}
|
||||||
for equations in component_equations.values():
|
for equations in component_equations.values():
|
||||||
for equation in equations:
|
for equation in equations:
|
||||||
@@ -720,12 +768,10 @@ class PressureFlowSolver:
|
|||||||
f"Unsupported connection equation relation: {equation.relation}."
|
f"Unsupported connection equation relation: {equation.relation}."
|
||||||
)
|
)
|
||||||
|
|
||||||
component = self.network.components[equation.owner_id]
|
evaluation = self._component_equation_plans_by_id[equation.owner_id]
|
||||||
|
component = evaluation.component
|
||||||
equation_id = equation.id
|
equation_id = equation.id
|
||||||
equation_ids = tuple(
|
equation_ids = tuple(current.id for current in evaluation.templates)
|
||||||
current.id
|
|
||||||
for current in component.pressure_flow_equation_residuals()
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
equation_index = equation_ids.index(equation_id)
|
equation_index = equation_ids.index(equation_id)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -734,38 +780,49 @@ class PressureFlowSolver:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
def read_component_equation() -> float:
|
def read_component_equation() -> float:
|
||||||
current_equations = component.pressure_flow_equation_residuals()
|
current_values = evaluation.evaluate()
|
||||||
if (
|
if equation_index >= len(current_values):
|
||||||
equation_index >= len(current_equations)
|
|
||||||
or current_equations[equation_index].id != equation_id
|
|
||||||
):
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Compiled algebraic equation disappeared at runtime: {equation_id}."
|
"Compiled algebraic equation disappeared at runtime: "
|
||||||
|
f"{equation_id}."
|
||||||
)
|
)
|
||||||
return float(current_equations[equation_index].value)
|
return float(current_values[equation_index])
|
||||||
|
|
||||||
return read_component_equation
|
return read_component_equation
|
||||||
def _pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
||||||
"""Evaluate live values through a precompiled connector topology."""
|
|
||||||
component_residuals = tuple(
|
|
||||||
residual
|
|
||||||
for component in self._component_equation_owners
|
|
||||||
for residual in component.pressure_flow_equation_residuals()
|
|
||||||
)
|
|
||||||
connection_residuals = tuple(
|
|
||||||
EquationResidual(
|
|
||||||
id=item.template.id,
|
|
||||||
owner=item.template.owner,
|
|
||||||
owner_id=item.template.owner_id,
|
|
||||||
relation=item.template.relation,
|
|
||||||
variables=item.template.variables,
|
|
||||||
value=item.evaluate(),
|
|
||||||
role=item.template.role,
|
|
||||||
)
|
|
||||||
for item in self._connection_equation_plan
|
|
||||||
)
|
|
||||||
return component_residuals + connection_residuals
|
|
||||||
|
|
||||||
|
def _pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
"""Evaluate live equation values through the compiled topology."""
|
||||||
|
|
||||||
|
values: list[float] = []
|
||||||
|
for item in self._component_equation_plan:
|
||||||
|
current_values = item.evaluate()
|
||||||
|
if len(current_values) != len(item.templates):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Compiled algebraic equation count changed at runtime for "
|
||||||
|
f"{item.component.name}."
|
||||||
|
)
|
||||||
|
values.extend(current_values)
|
||||||
|
values.extend(item.evaluate() for item in self._connection_equation_plan)
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
|
def _pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
|
"""Materialize public residual objects only when explicitly requested."""
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
EquationResidual(
|
||||||
|
id=template.id,
|
||||||
|
owner=template.owner,
|
||||||
|
owner_id=template.owner_id,
|
||||||
|
relation=template.relation,
|
||||||
|
variables=template.variables,
|
||||||
|
value=value,
|
||||||
|
role=template.role,
|
||||||
|
)
|
||||||
|
for template, value in zip(
|
||||||
|
self._equation_templates,
|
||||||
|
self._pressure_flow_equation_values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _explicit_flow_assignment(
|
def _explicit_flow_assignment(
|
||||||
self,
|
self,
|
||||||
@@ -779,8 +836,9 @@ class PressureFlowSolver:
|
|||||||
evaluate=self._equation_value_reader(equation),
|
evaluate=self._equation_value_reader(equation),
|
||||||
)
|
)
|
||||||
|
|
||||||
component = self.network.components[equation.owner_id]
|
evaluation = self._component_equation_plans_by_id[equation.owner_id]
|
||||||
equations = component.pressure_flow_equation_residuals()
|
component = evaluation.component
|
||||||
|
equations = evaluation.templates
|
||||||
equation_ids = tuple(current.id for current in equations)
|
equation_ids = tuple(current.id for current in equations)
|
||||||
try:
|
try:
|
||||||
equation_index = equation_ids.index(equation.id)
|
equation_index = equation_ids.index(equation.id)
|
||||||
@@ -803,8 +861,8 @@ class PressureFlowSolver:
|
|||||||
seeded_ids: set[str] = set()
|
seeded_ids: set[str] = set()
|
||||||
|
|
||||||
initial_assignments: list[ExplicitFlowAssignment] = []
|
initial_assignments: list[ExplicitFlowAssignment] = []
|
||||||
for component in self.network.components.values():
|
for evaluation in self._component_equation_plan:
|
||||||
for equation in component.pressure_flow_equation_residuals():
|
for equation in evaluation.templates:
|
||||||
if equation.relation != "constitutive" or equation.role != "flow":
|
if equation.relation != "constitutive" or equation.role != "flow":
|
||||||
continue
|
continue
|
||||||
flow_unknowns = self._flow_unknowns_for_equation(equation)
|
flow_unknowns = self._flow_unknowns_for_equation(equation)
|
||||||
@@ -818,9 +876,9 @@ class PressureFlowSolver:
|
|||||||
)
|
)
|
||||||
seeded_ids.add(unknown.id)
|
seeded_ids.add(unknown.id)
|
||||||
if initial_assignments:
|
if initial_assignments:
|
||||||
stages.append(ExplicitFlowStage(tuple(initial_assignments)))
|
stages.append(self._compile_explicit_flow_stage(tuple(initial_assignments)))
|
||||||
|
|
||||||
equations = self._pressure_flow_equation_residuals()
|
equations = self._equation_templates
|
||||||
while True:
|
while True:
|
||||||
stage_assignments: list[ExplicitFlowAssignment] = []
|
stage_assignments: list[ExplicitFlowAssignment] = []
|
||||||
stage_unknown_ids: set[str] = set()
|
stage_unknown_ids: set[str] = set()
|
||||||
@@ -850,7 +908,9 @@ class PressureFlowSolver:
|
|||||||
)
|
)
|
||||||
stage_unknown_ids.add(unknown.id)
|
stage_unknown_ids.add(unknown.id)
|
||||||
if stage_assignments:
|
if stage_assignments:
|
||||||
stages.append(ExplicitFlowStage(tuple(stage_assignments)))
|
stages.append(
|
||||||
|
self._compile_explicit_flow_stage(tuple(stage_assignments))
|
||||||
|
)
|
||||||
seeded_ids.update(stage_unknown_ids)
|
seeded_ids.update(stage_unknown_ids)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -880,43 +940,105 @@ class PressureFlowSolver:
|
|||||||
break
|
break
|
||||||
if fallback_assignment is None:
|
if fallback_assignment is None:
|
||||||
break
|
break
|
||||||
stages.append(ExplicitFlowStage((fallback_assignment,)))
|
stages.append(self._compile_explicit_flow_stage((fallback_assignment,)))
|
||||||
|
|
||||||
return tuple(stages)
|
return tuple(stages)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _compile_explicit_flow_stage(
|
||||||
|
assignments: tuple[ExplicitFlowAssignment, ...],
|
||||||
|
) -> ExplicitFlowStage:
|
||||||
|
direct_evaluations: list[tuple[int, Callable[[], float]]] = []
|
||||||
|
assignments_by_component: dict[
|
||||||
|
object,
|
||||||
|
list[tuple[int, ExplicitFlowAssignment]],
|
||||||
|
] = {}
|
||||||
|
for assignment_index, assignment in enumerate(assignments):
|
||||||
|
if assignment.component is None:
|
||||||
|
if assignment.evaluate is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Explicit flow assignment has no compiled evaluator: "
|
||||||
|
f"{assignment.equation_id}."
|
||||||
|
)
|
||||||
|
direct_evaluations.append((assignment_index, assignment.evaluate))
|
||||||
|
continue
|
||||||
|
assignments_by_component.setdefault(assignment.component, []).append(
|
||||||
|
(assignment_index, assignment)
|
||||||
|
)
|
||||||
|
|
||||||
|
component_evaluations: list[ExplicitFlowComponentEvaluation] = []
|
||||||
|
for component, component_assignments in assignments_by_component.items():
|
||||||
|
if any(
|
||||||
|
assignment.equation_index is None
|
||||||
|
for _assignment_index, assignment in component_assignments
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Explicit component flow assignment has no equation index."
|
||||||
|
)
|
||||||
|
component_evaluations.append(
|
||||||
|
ExplicitFlowComponentEvaluation(
|
||||||
|
component=component,
|
||||||
|
evaluate=component.pressure_flow_equation_values,
|
||||||
|
assignment_indices=tuple(
|
||||||
|
assignment_index
|
||||||
|
for assignment_index, _assignment in component_assignments
|
||||||
|
),
|
||||||
|
equation_indices=tuple(
|
||||||
|
int(assignment.equation_index)
|
||||||
|
for _assignment_index, assignment in component_assignments
|
||||||
|
),
|
||||||
|
equation_ids=tuple(
|
||||||
|
assignment.equation_id
|
||||||
|
for _assignment_index, assignment in component_assignments
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ExplicitFlowStage(
|
||||||
|
assignments=assignments,
|
||||||
|
direct_evaluations=tuple(direct_evaluations),
|
||||||
|
component_evaluations=tuple(component_evaluations),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _filter_explicit_flow_plan(
|
||||||
|
self,
|
||||||
|
selected: frozenset[str],
|
||||||
|
) -> tuple[ExplicitFlowStage, ...]:
|
||||||
|
return tuple(
|
||||||
|
self._compile_explicit_flow_stage(
|
||||||
|
tuple(
|
||||||
|
assignment
|
||||||
|
for assignment in stage.assignments
|
||||||
|
if assignment.unknown.variable in selected
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for stage in self._explicit_flow_plan
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _evaluate_explicit_flow_stage(
|
def _evaluate_explicit_flow_stage(
|
||||||
assignments: tuple[ExplicitFlowAssignment, ...],
|
stage: ExplicitFlowStage,
|
||||||
) -> dict[str, float]:
|
) -> tuple[float, ...]:
|
||||||
values: dict[str, float] = {}
|
assignments = stage.assignments
|
||||||
assignments_by_component: dict[object, list[ExplicitFlowAssignment]] = {}
|
values: list[float | None] = [None] * len(assignments)
|
||||||
for assignment in assignments:
|
for assignment_index, evaluate in stage.direct_evaluations:
|
||||||
if assignment.component is None:
|
values[assignment_index] = evaluate()
|
||||||
assert assignment.evaluate is not None
|
|
||||||
values[assignment.equation_id] = assignment.evaluate()
|
|
||||||
continue
|
|
||||||
assignments_by_component.setdefault(assignment.component, []).append(
|
|
||||||
assignment
|
|
||||||
)
|
|
||||||
|
|
||||||
for component, component_assignments in assignments_by_component.items():
|
for evaluation in stage.component_evaluations:
|
||||||
equations = component.pressure_flow_equation_residuals()
|
equation_values = evaluation.evaluate()
|
||||||
for assignment in component_assignments:
|
for assignment_index, equation_index, equation_id in zip(
|
||||||
assert assignment.equation_index is not None
|
evaluation.assignment_indices,
|
||||||
equation_index = assignment.equation_index
|
evaluation.equation_indices,
|
||||||
if (
|
evaluation.equation_ids,
|
||||||
equation_index >= len(equations)
|
|
||||||
or equations[equation_index].id != assignment.equation_id
|
|
||||||
):
|
):
|
||||||
|
if equation_index >= len(equation_values):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Compiled algebraic equation disappeared at runtime: "
|
"Compiled algebraic equation disappeared at runtime: "
|
||||||
f"{assignment.equation_id}."
|
f"{equation_id}."
|
||||||
)
|
)
|
||||||
values[assignment.equation_id] = float(
|
values[assignment_index] = float(equation_values[equation_index])
|
||||||
equations[equation_index].value
|
if any(value is None for value in values):
|
||||||
)
|
raise RuntimeError("Explicit flow evaluation plan returned no value.")
|
||||||
return values
|
return tuple(float(value) for value in values)
|
||||||
|
|
||||||
def _solve_explicit_flow_unknowns(
|
def _solve_explicit_flow_unknowns(
|
||||||
self,
|
self,
|
||||||
@@ -930,19 +1052,18 @@ class PressureFlowSolver:
|
|||||||
unknown.write(0.0)
|
unknown.write(0.0)
|
||||||
|
|
||||||
seeded_ids: set[str] = set()
|
seeded_ids: set[str] = set()
|
||||||
for stage in self._explicit_flow_plan:
|
plan = self._explicit_flow_plans_by_variables.get(selected)
|
||||||
assignments = tuple(
|
if plan is None:
|
||||||
assignment
|
plan = self._filter_explicit_flow_plan(selected)
|
||||||
for assignment in stage.assignments
|
for stage in plan:
|
||||||
if assignment.unknown.variable in selected
|
assignments = stage.assignments
|
||||||
)
|
values = self._evaluate_explicit_flow_stage(stage)
|
||||||
values = self._evaluate_explicit_flow_stage(assignments)
|
|
||||||
targets = tuple(
|
targets = tuple(
|
||||||
(
|
(
|
||||||
assignment,
|
assignment,
|
||||||
assignment.unknown.read() - values[assignment.equation_id],
|
assignment.unknown.read() - value,
|
||||||
)
|
)
|
||||||
for assignment in assignments
|
for assignment, value in zip(assignments, values)
|
||||||
)
|
)
|
||||||
for assignment, target_value in targets:
|
for assignment, target_value in targets:
|
||||||
if not isfinite(target_value):
|
if not isfinite(target_value):
|
||||||
@@ -1101,12 +1222,38 @@ class PressureFlowSolver:
|
|||||||
orifice.get_port(orifice_port).p = pressure
|
orifice.get_port(orifice_port).p = pressure
|
||||||
pipe.get_port(pipe_port).p = pressure
|
pipe.get_port(pipe_port).p = pressure
|
||||||
|
|
||||||
|
def _equation_scale_plan(self, equation) -> EquationScalePlan:
|
||||||
|
return EquationScalePlan(
|
||||||
|
variable_names=tuple(
|
||||||
|
variable.rsplit(".", 1)[-1]
|
||||||
|
for variable in equation.variables
|
||||||
|
),
|
||||||
|
force_unknowns=tuple(
|
||||||
|
unknown
|
||||||
|
for variable in equation.variables
|
||||||
|
if (unknown := self._unknowns_by_id.get(variable)) is not None
|
||||||
|
and unknown.variable == "f"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_equation_scale_plans(
|
||||||
|
self,
|
||||||
|
equations: tuple[EquationResidual, ...],
|
||||||
|
) -> tuple[EquationScalePlan, ...]:
|
||||||
|
return tuple(
|
||||||
|
self._equation_scale_plan(equation)
|
||||||
|
for equation in equations
|
||||||
|
)
|
||||||
|
|
||||||
def _scales(self) -> dict[str, float]:
|
def _scales(self) -> dict[str, float]:
|
||||||
|
pressure_values = [
|
||||||
|
unknown.read() for unknown in self._unknowns_by_variable["p"]
|
||||||
|
]
|
||||||
pressure_scale = max(
|
pressure_scale = max(
|
||||||
[
|
[
|
||||||
abs(unknown.read())
|
abs(value)
|
||||||
for unknown in self._unknowns_by_variable["p"]
|
for value in pressure_values
|
||||||
if unknown.read() > 0.0
|
if value > 0.0
|
||||||
]
|
]
|
||||||
+ [1e5]
|
+ [1e5]
|
||||||
)
|
)
|
||||||
@@ -1166,36 +1313,24 @@ class PressureFlowSolver:
|
|||||||
scales = self._scales()
|
scales = self._scales()
|
||||||
pressure_scale = scales["p"]
|
pressure_scale = scales["p"]
|
||||||
flow_scale = scales["m_flow"]
|
flow_scale = scales["m_flow"]
|
||||||
unknown_scales = {
|
seeded_values = self._pressure_flow_equation_values()
|
||||||
unknown.id: (
|
|
||||||
max(abs(unknown.read()), 1.0)
|
|
||||||
if unknown.variable == "f"
|
|
||||||
else scales.get(unknown.variable, max(abs(unknown.read()), 1.0))
|
|
||||||
)
|
|
||||||
for unknown in self.unknowns
|
|
||||||
}
|
|
||||||
def variable_scale(unknown: AlgebraicUnknown) -> float:
|
|
||||||
return unknown_scales[unknown.id]
|
|
||||||
|
|
||||||
seeded_equations = self._pressure_flow_equation_residuals()
|
def initial_equation_scale(
|
||||||
|
equation: EquationResidual,
|
||||||
def initial_equation_scale(equation) -> float:
|
value: float,
|
||||||
variable_names = [
|
scale_plan: EquationScalePlan,
|
||||||
variable.rsplit(".", 1)[-1]
|
) -> float:
|
||||||
for variable in equation.variables
|
variable_names = scale_plan.variable_names
|
||||||
]
|
|
||||||
if equation.role == "flow":
|
if equation.role == "flow":
|
||||||
force_scales = [
|
force_scales = [
|
||||||
unknown_scales[variable]
|
max(abs(unknown.read()), 1.0)
|
||||||
for variable in equation.variables
|
for unknown in scale_plan.force_unknowns
|
||||||
if variable in self._unknowns_by_id
|
|
||||||
and self._unknowns_by_id[variable].variable == "f"
|
|
||||||
]
|
]
|
||||||
if force_scales:
|
if force_scales:
|
||||||
# Freeze force scaling per equation. A 1e17 N source must
|
# Freeze force scaling per equation. A 1e17 N source must
|
||||||
# not hide an unrelated 40 N piston/contact imbalance in a
|
# not hide an unrelated 40 N piston/contact imbalance in a
|
||||||
# different mechanical branch.
|
# different mechanical branch.
|
||||||
return max(force_scales + [abs(float(equation.value)), 1.0])
|
return max(force_scales + [abs(value), 1.0])
|
||||||
return flow_scale
|
return flow_scale
|
||||||
if equation.role == "effort":
|
if equation.role == "effort":
|
||||||
if "x" in variable_names:
|
if "x" in variable_names:
|
||||||
@@ -1205,20 +1340,18 @@ class PressureFlowSolver:
|
|||||||
return pressure_scale
|
return pressure_scale
|
||||||
return max([scales.get(name, 1.0) for name in variable_names] + [1.0])
|
return max([scales.get(name, 1.0) for name in variable_names] + [1.0])
|
||||||
|
|
||||||
equation_scales = {
|
equation_scales = tuple(
|
||||||
equation.id: initial_equation_scale(equation)
|
initial_equation_scale(equation, value, scale_plan)
|
||||||
for equation in seeded_equations
|
for equation, value, scale_plan in zip(
|
||||||
}
|
self._equation_templates,
|
||||||
|
seeded_values,
|
||||||
def equation_scale(equation) -> float:
|
self._equation_scale_plans,
|
||||||
cached = equation_scales.get(equation.id)
|
)
|
||||||
if cached is not None:
|
)
|
||||||
return cached
|
|
||||||
return initial_equation_scale(equation)
|
|
||||||
|
|
||||||
seeded_scaled = [
|
seeded_scaled = [
|
||||||
abs(equation.value / equation_scale(equation))
|
abs(value / scale)
|
||||||
for equation in seeded_equations
|
for value, scale in zip(seeded_values, equation_scales)
|
||||||
]
|
]
|
||||||
seeded_max_scaled_residual = max(seeded_scaled, default=0.0)
|
seeded_max_scaled_residual = max(seeded_scaled, default=0.0)
|
||||||
seeded_unknown_values = [
|
seeded_unknown_values = [
|
||||||
@@ -1242,13 +1375,25 @@ class PressureFlowSolver:
|
|||||||
flow_scale=flow_scale,
|
flow_scale=flow_scale,
|
||||||
max_scaled_residual=seeded_max_scaled_residual,
|
max_scaled_residual=seeded_max_scaled_residual,
|
||||||
max_raw_residual=max(
|
max_raw_residual=max(
|
||||||
(abs(item.value) for item in seeded_equations),
|
(abs(value) for value in seeded_values),
|
||||||
default=0.0,
|
default=0.0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.last_diagnostics = diagnostics
|
self.last_diagnostics = diagnostics
|
||||||
return diagnostics
|
return diagnostics
|
||||||
|
|
||||||
|
unknown_scales = {
|
||||||
|
unknown.id: (
|
||||||
|
max(abs(unknown.read()), 1.0)
|
||||||
|
if unknown.variable == "f"
|
||||||
|
else scales[unknown.variable]
|
||||||
|
)
|
||||||
|
for unknown in self.unknowns
|
||||||
|
}
|
||||||
|
|
||||||
|
def variable_scale(unknown: AlgebraicUnknown) -> float:
|
||||||
|
return unknown_scales[unknown.id]
|
||||||
|
|
||||||
positive_pressures = [
|
positive_pressures = [
|
||||||
unknown.read()
|
unknown.read()
|
||||||
for unknown in self._unknowns_by_variable["p"]
|
for unknown in self._unknowns_by_variable["p"]
|
||||||
@@ -1293,11 +1438,11 @@ class PressureFlowSolver:
|
|||||||
def scaled_residuals(values):
|
def scaled_residuals(values):
|
||||||
assign(values)
|
assign(values)
|
||||||
self._refresh_unilateral_contacts(contact_bindings)
|
self._refresh_unilateral_contacts(contact_bindings)
|
||||||
equations = self._pressure_flow_equation_residuals()
|
equation_values = self._pressure_flow_equation_values()
|
||||||
return np.asarray(
|
return np.asarray(
|
||||||
[
|
[
|
||||||
equation.value / equation_scale(equation)
|
value / scale
|
||||||
for equation in equations
|
for value, scale in zip(equation_values, equation_scales)
|
||||||
],
|
],
|
||||||
dtype=float,
|
dtype=float,
|
||||||
)
|
)
|
||||||
@@ -1314,12 +1459,10 @@ class PressureFlowSolver:
|
|||||||
)
|
)
|
||||||
assign(result.x)
|
assign(result.x)
|
||||||
self._refresh_unilateral_contacts(contact_bindings)
|
self._refresh_unilateral_contacts(contact_bindings)
|
||||||
equations = self._pressure_flow_equation_residuals()
|
equation_values = self._pressure_flow_equation_values()
|
||||||
scaled = [
|
scaled = [
|
||||||
abs(
|
abs(value / scale)
|
||||||
equation.value / equation_scale(equation)
|
for value, scale in zip(equation_values, equation_scales)
|
||||||
)
|
|
||||||
for equation in equations
|
|
||||||
]
|
]
|
||||||
max_scaled_residual = max(scaled, default=0.0)
|
max_scaled_residual = max(scaled, default=0.0)
|
||||||
residuals_converged = (
|
residuals_converged = (
|
||||||
@@ -1335,7 +1478,10 @@ class PressureFlowSolver:
|
|||||||
pressure_scale=pressure_scale,
|
pressure_scale=pressure_scale,
|
||||||
flow_scale=flow_scale,
|
flow_scale=flow_scale,
|
||||||
max_scaled_residual=max_scaled_residual,
|
max_scaled_residual=max_scaled_residual,
|
||||||
max_raw_residual=max((abs(item.value) for item in equations), default=0.0),
|
max_raw_residual=max(
|
||||||
|
(abs(value) for value in equation_values),
|
||||||
|
default=0.0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.last_diagnostics = diagnostics
|
self.last_diagnostics = diagnostics
|
||||||
if not success:
|
if not success:
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ class MechanicalStateReducer:
|
|||||||
mechanical_ports = {
|
mechanical_ports = {
|
||||||
(component.name, definition.name)
|
(component.name, definition.name)
|
||||||
for component in self.network.components.values()
|
for component in self.network.components.values()
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if definition.kind == "physical" and definition.domain == "mechanical"
|
if definition.kind == "physical" and definition.domain == "mechanical"
|
||||||
}
|
}
|
||||||
parents = {
|
parents = {
|
||||||
@@ -339,7 +339,7 @@ class MechanicalStateReducer:
|
|||||||
for component in masses:
|
for component in masses:
|
||||||
ports = [
|
ports = [
|
||||||
(component.name, definition.name)
|
(component.name, definition.name)
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if definition.kind == "physical"
|
if definition.kind == "physical"
|
||||||
and definition.domain == "mechanical"
|
and definition.domain == "mechanical"
|
||||||
]
|
]
|
||||||
@@ -354,7 +354,7 @@ class MechanicalStateReducer:
|
|||||||
for component in masses:
|
for component in masses:
|
||||||
first_port = next(
|
first_port = next(
|
||||||
(component.name, definition.name)
|
(component.name, definition.name)
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if definition.kind == "physical"
|
if definition.kind == "physical"
|
||||||
and definition.domain == "mechanical"
|
and definition.domain == "mechanical"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ def _pressure_storage_endpoint_groups(
|
|||||||
pneumatic_endpoints = {
|
pneumatic_endpoints = {
|
||||||
Endpoint(component.name, definition.name)
|
Endpoint(component.name, definition.name)
|
||||||
for component in network.components.values()
|
for component in network.components.values()
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if definition.kind == "physical" and definition.domain == "pneumatic"
|
if definition.kind == "physical" and definition.domain == "pneumatic"
|
||||||
}
|
}
|
||||||
parent = {endpoint: endpoint for endpoint in pneumatic_endpoints}
|
parent = {endpoint: endpoint for endpoint in pneumatic_endpoints}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class PneumaticVolumeResolver:
|
|||||||
|
|
||||||
def solve(self) -> PneumaticVolumeDiagnostics:
|
def solve(self) -> PneumaticVolumeDiagnostics:
|
||||||
for component in self.network.components.values():
|
for component in self.network.components.values():
|
||||||
for definition in component.port_definitions:
|
for definition in component.active_port_definitions:
|
||||||
if definition.kind == "physical" and definition.domain == "pneumatic":
|
if definition.kind == "physical" and definition.domain == "pneumatic":
|
||||||
port = component.get_port(definition.name)
|
port = component.get_port(definition.name)
|
||||||
port.volume = 0.0
|
port.volume = 0.0
|
||||||
|
|||||||
@@ -44,6 +44,36 @@ class SolveIVPConfig:
|
|||||||
first_step: float | None = None
|
first_step: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SolverSegmentDiagnostics:
|
||||||
|
"""Work performed by implicit solver instances inside one event segment."""
|
||||||
|
|
||||||
|
start_time: float
|
||||||
|
requested_stop_time: float
|
||||||
|
simulated_until: float
|
||||||
|
nfev: int = 0
|
||||||
|
njev: int = 0
|
||||||
|
nlu: int = 0
|
||||||
|
accepted_step_count: int = 0
|
||||||
|
solver_start_count: int = 0
|
||||||
|
state_transition_count: int = 0
|
||||||
|
recoverable_retry_count: int = 0
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, float | int]:
|
||||||
|
return {
|
||||||
|
"startTime": self.start_time,
|
||||||
|
"requestedStopTime": self.requested_stop_time,
|
||||||
|
"simulatedUntil": self.simulated_until,
|
||||||
|
"nfev": self.nfev,
|
||||||
|
"njev": self.njev,
|
||||||
|
"nlu": self.nlu,
|
||||||
|
"acceptedStepCount": self.accepted_step_count,
|
||||||
|
"solverStartCount": self.solver_start_count,
|
||||||
|
"stateTransitionCount": self.state_transition_count,
|
||||||
|
"recoverableRetryCount": self.recoverable_retry_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ODESolution:
|
class ODESolution:
|
||||||
t: list[float]
|
t: list[float]
|
||||||
@@ -52,6 +82,7 @@ class ODESolution:
|
|||||||
message: str
|
message: str
|
||||||
status: IntegrationStatus = "completed"
|
status: IntegrationStatus = "completed"
|
||||||
error: Exception | None = None
|
error: Exception | None = None
|
||||||
|
solver_segments: tuple[SolverSegmentDiagnostics, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
def _vector_add(a: list[float], b: list[float], scale: float = 1.0) -> list[float]:
|
def _vector_add(a: list[float], b: list[float], scale: float = 1.0) -> list[float]:
|
||||||
@@ -610,6 +641,7 @@ def _integrate_scipy_stepwise(
|
|||||||
same_time_transition_count = 0
|
same_time_transition_count = 0
|
||||||
integration_progressed = False
|
integration_progressed = False
|
||||||
last_reported_step: float | None = None
|
last_reported_step: float | None = None
|
||||||
|
solver_segments: list[SolverSegmentDiagnostics] = []
|
||||||
|
|
||||||
def cancellation_message() -> str:
|
def cancellation_message() -> str:
|
||||||
return (
|
return (
|
||||||
@@ -639,9 +671,17 @@ def _integrate_scipy_stepwise(
|
|||||||
math.nextafter(segment_end, -math.inf) if is_breakpoint else segment_end
|
math.nextafter(segment_end, -math.inf) if is_breakpoint else segment_end
|
||||||
)
|
)
|
||||||
has_integration_interval = integration_end > last_accepted_time
|
has_integration_interval = integration_end > last_accepted_time
|
||||||
|
segment_start_time = last_accepted_time
|
||||||
segment_max_step = float(config.max_step)
|
segment_max_step = float(config.max_step)
|
||||||
recoverable_retry_count = 0
|
recoverable_retry_count = 0
|
||||||
last_recoverable_error: RecoverableTrialStateError | None = None
|
last_recoverable_error: RecoverableTrialStateError | None = None
|
||||||
|
segment_nfev = 0
|
||||||
|
segment_njev = 0
|
||||||
|
segment_nlu = 0
|
||||||
|
segment_accepted_steps = 0
|
||||||
|
segment_solver_starts = 0
|
||||||
|
segment_state_transitions = 0
|
||||||
|
segment_recoverable_retries = 0
|
||||||
|
|
||||||
while has_integration_interval and last_accepted_time < integration_end:
|
while has_integration_interval and last_accepted_time < integration_end:
|
||||||
if cancel_check():
|
if cancel_check():
|
||||||
@@ -681,6 +721,7 @@ def _integrate_scipy_stepwise(
|
|||||||
break
|
break
|
||||||
except RecoverableTrialStateError as exc:
|
except RecoverableTrialStateError as exc:
|
||||||
recoverable_retry_count += 1
|
recoverable_retry_count += 1
|
||||||
|
segment_recoverable_retries += 1
|
||||||
last_recoverable_error = exc
|
last_recoverable_error = exc
|
||||||
next_step = 0.5 * segment_max_step
|
next_step = 0.5 * segment_max_step
|
||||||
minimum_step = 64.0 * math.ulp(max(abs(last_accepted_time), 1.0))
|
minimum_step = 64.0 * math.ulp(max(abs(last_accepted_time), 1.0))
|
||||||
@@ -697,6 +738,7 @@ def _integrate_scipy_stepwise(
|
|||||||
error = exc
|
error = exc
|
||||||
break
|
break
|
||||||
|
|
||||||
|
segment_solver_starts += 1
|
||||||
|
|
||||||
restart_at_transition = False
|
restart_at_transition = False
|
||||||
restart_after_recoverable = False
|
restart_after_recoverable = False
|
||||||
@@ -720,6 +762,7 @@ def _integrate_scipy_stepwise(
|
|||||||
break
|
break
|
||||||
except RecoverableTrialStateError as exc:
|
except RecoverableTrialStateError as exc:
|
||||||
recoverable_retry_count += 1
|
recoverable_retry_count += 1
|
||||||
|
segment_recoverable_retries += 1
|
||||||
last_recoverable_error = exc
|
last_recoverable_error = exc
|
||||||
attempted_step = segment_max_step
|
attempted_step = segment_max_step
|
||||||
next_step = 0.5 * attempted_step
|
next_step = 0.5 * attempted_step
|
||||||
@@ -744,6 +787,7 @@ def _integrate_scipy_stepwise(
|
|||||||
if solver.status == "failed":
|
if solver.status == "failed":
|
||||||
if last_recoverable_error is not None:
|
if last_recoverable_error is not None:
|
||||||
recoverable_retry_count += 1
|
recoverable_retry_count += 1
|
||||||
|
segment_recoverable_retries += 1
|
||||||
next_step = 0.5 * segment_max_step
|
next_step = 0.5 * segment_max_step
|
||||||
minimum_step = 64.0 * math.ulp(
|
minimum_step = 64.0 * math.ulp(
|
||||||
max(abs(last_accepted_time), 1.0)
|
max(abs(last_accepted_time), 1.0)
|
||||||
@@ -759,6 +803,7 @@ def _integrate_scipy_stepwise(
|
|||||||
message = str(step_message or "Integration step failed.")
|
message = str(step_message or "Integration step failed.")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
segment_accepted_steps += 1
|
||||||
step_end_time = float(solver.t)
|
step_end_time = float(solver.t)
|
||||||
step_end_state = [float(value) for value in solver.y]
|
step_end_state = [float(value) for value in solver.y]
|
||||||
dense_output = (
|
dense_output = (
|
||||||
@@ -807,6 +852,7 @@ def _integrate_scipy_stepwise(
|
|||||||
break
|
break
|
||||||
|
|
||||||
if transition is not None:
|
if transition is not None:
|
||||||
|
segment_state_transitions += 1
|
||||||
try:
|
try:
|
||||||
same_time_transition_count = (
|
same_time_transition_count = (
|
||||||
_next_same_time_transition_count(
|
_next_same_time_transition_count(
|
||||||
@@ -896,6 +942,9 @@ def _integrate_scipy_stepwise(
|
|||||||
)
|
)
|
||||||
report_step(reported_time)
|
report_step(reported_time)
|
||||||
|
|
||||||
|
segment_nfev += int(getattr(solver, "nfev", 0))
|
||||||
|
segment_njev += int(getattr(solver, "njev", 0))
|
||||||
|
segment_nlu += int(getattr(solver, "nlu", 0))
|
||||||
if status != "completed":
|
if status != "completed":
|
||||||
break
|
break
|
||||||
if restart_after_recoverable:
|
if restart_after_recoverable:
|
||||||
@@ -903,6 +952,22 @@ def _integrate_scipy_stepwise(
|
|||||||
if not restart_at_transition:
|
if not restart_at_transition:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
solver_segments.append(
|
||||||
|
SolverSegmentDiagnostics(
|
||||||
|
start_time=float(segment_start_time),
|
||||||
|
requested_stop_time=float(segment_end),
|
||||||
|
simulated_until=float(
|
||||||
|
segment_end if status == "completed" else last_accepted_time
|
||||||
|
),
|
||||||
|
nfev=segment_nfev,
|
||||||
|
njev=segment_njev,
|
||||||
|
nlu=segment_nlu,
|
||||||
|
accepted_step_count=segment_accepted_steps,
|
||||||
|
solver_start_count=segment_solver_starts,
|
||||||
|
state_transition_count=segment_state_transitions,
|
||||||
|
recoverable_retry_count=segment_recoverable_retries,
|
||||||
|
)
|
||||||
|
)
|
||||||
if status != "completed":
|
if status != "completed":
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -949,6 +1014,7 @@ def _integrate_scipy_stepwise(
|
|||||||
message=message,
|
message=message,
|
||||||
status=status,
|
status=status,
|
||||||
error=error,
|
error=error,
|
||||||
|
solver_segments=tuple(solver_segments),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ def simulation_preparation_issues(
|
|||||||
for component in network.components.values()
|
for component in network.components.values()
|
||||||
if any(
|
if any(
|
||||||
definition.kind == "physical"
|
definition.kind == "physical"
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
adjacency = {name: set() for name in physical_component_names}
|
adjacency = {name: set() for name in physical_component_names}
|
||||||
@@ -449,6 +449,22 @@ class GenericFluidSystem:
|
|||||||
self._jacobian_sparsity = self._build_jacobian_sparsity()
|
self._jacobian_sparsity = self._build_jacobian_sparsity()
|
||||||
return self._jacobian_sparsity
|
return self._jacobian_sparsity
|
||||||
|
|
||||||
|
def jacobian_sparsity_diagnostics(self) -> dict[str, float | int]:
|
||||||
|
from scipy.optimize._numdiff import group_columns
|
||||||
|
|
||||||
|
sparsity = self.jacobian_sparsity()
|
||||||
|
group_count = int(group_columns(sparsity).max(initial=-1)) + 1
|
||||||
|
state_count = int(sparsity.shape[0])
|
||||||
|
return {
|
||||||
|
"nonzeroCount": int(sparsity.nnz),
|
||||||
|
"density": (
|
||||||
|
float(sparsity.nnz) / float(state_count * state_count)
|
||||||
|
if state_count
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"colorGroupCount": group_count,
|
||||||
|
}
|
||||||
|
|
||||||
def _close_current_state(self, time: float) -> dict[str, dict[str, float]]:
|
def _close_current_state(self, time: float) -> dict[str, dict[str, float]]:
|
||||||
signal = self.signal_resolver.solve(time)
|
signal = self.signal_resolver.solve(time)
|
||||||
self.signal_propagation_count += signal.propagated
|
self.signal_propagation_count += signal.propagated
|
||||||
@@ -471,7 +487,7 @@ class GenericFluidSystem:
|
|||||||
physical_ports = tuple(
|
physical_ports = tuple(
|
||||||
port
|
port
|
||||||
for component in self.network.components.values()
|
for component in self.network.components.values()
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if definition.kind == "physical"
|
if definition.kind == "physical"
|
||||||
for port in (component.get_port(definition.name),)
|
for port in (component.get_port(definition.name),)
|
||||||
)
|
)
|
||||||
@@ -655,6 +671,54 @@ class GenericFluidSystem:
|
|||||||
)
|
)
|
||||||
report_progress(postprocess_progress, "postprocessing", force=True)
|
report_progress(postprocess_progress, "postprocessing", force=True)
|
||||||
times = [float(value) for value in solution.t]
|
times = [float(value) for value in solution.t]
|
||||||
|
if isinstance(solution, ODESolution):
|
||||||
|
solver_segment_diagnostics = [
|
||||||
|
segment.as_dict() for segment in solution.solver_segments
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
solver_segment_diagnostics = [
|
||||||
|
{
|
||||||
|
"startTime": float(config.t_start),
|
||||||
|
"requestedStopTime": float(config.t_stop),
|
||||||
|
"simulatedUntil": times[-1] if times else float(config.t_start),
|
||||||
|
"nfev": int(getattr(solution, "nfev", 0)),
|
||||||
|
"njev": int(getattr(solution, "njev", 0)),
|
||||||
|
"nlu": int(getattr(solution, "nlu", 0)),
|
||||||
|
"acceptedStepCount": 0,
|
||||||
|
"solverStartCount": 1,
|
||||||
|
"stateTransitionCount": 0,
|
||||||
|
"recoverableRetryCount": 0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
solver_total_keys = (
|
||||||
|
"nfev",
|
||||||
|
"njev",
|
||||||
|
"nlu",
|
||||||
|
"acceptedStepCount",
|
||||||
|
"solverStartCount",
|
||||||
|
"stateTransitionCount",
|
||||||
|
"recoverableRetryCount",
|
||||||
|
)
|
||||||
|
solver_totals = {
|
||||||
|
key: sum(int(segment[key]) for segment in solver_segment_diagnostics)
|
||||||
|
for key in solver_total_keys
|
||||||
|
}
|
||||||
|
jacobian_diagnostics = (
|
||||||
|
self.jacobian_sparsity_diagnostics()
|
||||||
|
if integration_config.method in {"BDF", "Radau"}
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if jacobian_diagnostics is not None:
|
||||||
|
color_group_count = int(jacobian_diagnostics["colorGroupCount"])
|
||||||
|
for segment in solver_segment_diagnostics:
|
||||||
|
segment["finiteDifferenceRhsEstimate"] = (
|
||||||
|
int(segment["njev"]) * color_group_count
|
||||||
|
)
|
||||||
|
solver_totals["finiteDifferenceRhsEstimate"] = sum(
|
||||||
|
int(segment["finiteDifferenceRhsEstimate"])
|
||||||
|
for segment in solver_segment_diagnostics
|
||||||
|
)
|
||||||
|
|
||||||
series: dict[str, list[float]] = {"time": []}
|
series: dict[str, list[float]] = {"time": []}
|
||||||
postprocessing_error: Exception | None = None
|
postprocessing_error: Exception | None = None
|
||||||
self.mechanical_state_reducer.reset_constraint_modes()
|
self.mechanical_state_reducer.reset_constraint_modes()
|
||||||
@@ -693,6 +757,13 @@ class GenericFluidSystem:
|
|||||||
if key != "time" and values
|
if key != "time" and values
|
||||||
}
|
}
|
||||||
diagnostics = {
|
diagnostics = {
|
||||||
|
"integration": {
|
||||||
|
"method": integration_config.method,
|
||||||
|
"jacobianSparsity": jacobian_diagnostics,
|
||||||
|
"segmentCount": len(solver_segment_diagnostics),
|
||||||
|
"segments": solver_segment_diagnostics,
|
||||||
|
"totals": solver_totals,
|
||||||
|
},
|
||||||
"pressureFlow": {
|
"pressureFlow": {
|
||||||
"solveCount": self.algebraic_solve_count,
|
"solveCount": self.algebraic_solve_count,
|
||||||
"maxScaledResidual": self.max_algebraic_residual,
|
"maxScaledResidual": self.max_algebraic_residual,
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ class SimulationNetwork:
|
|||||||
return tuple(
|
return tuple(
|
||||||
f"{component.name}.{definition.name}.{variable.name}"
|
f"{component.name}.{definition.name}.{variable.name}"
|
||||||
for component in self.components.values()
|
for component in self.components.values()
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if definition.kind == "physical"
|
if definition.kind == "physical"
|
||||||
for variable in definition.variables
|
for variable in definition.variables
|
||||||
if variable.role in {"effort", "flow"}
|
if variable.role in {"effort", "flow"}
|
||||||
@@ -304,7 +304,7 @@ class SimulationNetwork:
|
|||||||
"parameters": component.parameter_interface_dicts(),
|
"parameters": component.parameter_interface_dicts(),
|
||||||
"ports": [
|
"ports": [
|
||||||
definition.as_interface_dict()
|
definition.as_interface_dict()
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
],
|
],
|
||||||
"resultVariables": [
|
"resultVariables": [
|
||||||
variable.as_dict()
|
variable.as_dict()
|
||||||
@@ -320,7 +320,7 @@ class SimulationNetwork:
|
|||||||
"unconnectedPorts": [
|
"unconnectedPorts": [
|
||||||
{"component": component.name, "port": definition.name}
|
{"component": component.name, "port": definition.name}
|
||||||
for component in self.components.values()
|
for component in self.components.values()
|
||||||
for definition in component.port_definitions
|
for definition in component.active_port_definitions
|
||||||
if (component.name, definition.name) not in connected_endpoints
|
if (component.name, definition.name) not in connected_endpoints
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
|||||||
residuals = converter.pressure_flow_equation_residuals()
|
residuals = converter.pressure_flow_equation_residuals()
|
||||||
|
|
||||||
self.assertAlmostEqual(converter.output_force, 20.0)
|
self.assertAlmostEqual(converter.output_force, 20.0)
|
||||||
|
self.assertEqual(
|
||||||
|
converter.pressure_flow_equation_values(),
|
||||||
|
tuple(residual.value for residual in residuals),
|
||||||
|
)
|
||||||
self.assertAlmostEqual(residuals[0].value, 0.0)
|
self.assertAlmostEqual(residuals[0].value, 0.0)
|
||||||
self.assertEqual(converter.component_result_values(), {"force": 20.0})
|
self.assertEqual(converter.component_result_values(), {"force": 20.0})
|
||||||
self.assertEqual(converter.parameter_values, {"direction": 1.0})
|
self.assertEqual(converter.parameter_values, {"direction": 1.0})
|
||||||
@@ -54,10 +58,13 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
|||||||
converter.res.signal = 20.0
|
converter.res.signal = 20.0
|
||||||
converter.port_2.f = 20.0
|
converter.port_2.f = 20.0
|
||||||
|
|
||||||
self.assertAlmostEqual(
|
residuals = converter.pressure_flow_equation_residuals()
|
||||||
converter.pressure_flow_equation_residuals()[0].value,
|
|
||||||
0.0,
|
self.assertEqual(
|
||||||
|
converter.pressure_flow_equation_values(),
|
||||||
|
tuple(residual.value for residual in residuals),
|
||||||
)
|
)
|
||||||
|
self.assertAlmostEqual(residuals[0].value, 0.0)
|
||||||
self.assertEqual(converter.parameter_values, {"direction": -1.0})
|
self.assertEqual(converter.parameter_values, {"direction": -1.0})
|
||||||
self.assertEqual(converter.component_result_values(), {"force": -20.0})
|
self.assertEqual(converter.component_result_values(), {"force": -20.0})
|
||||||
|
|
||||||
@@ -231,9 +238,17 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
|||||||
residuals = node.pressure_flow_equation_residuals()
|
residuals = node.pressure_flow_equation_residuals()
|
||||||
|
|
||||||
self.assertEqual(node.active_ports, ("port_1", "port_2", "port_3"))
|
self.assertEqual(node.active_ports, ("port_1", "port_2", "port_3"))
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(definition.name for definition in node.active_port_definitions),
|
||||||
|
node.active_ports,
|
||||||
|
)
|
||||||
self.assertAlmostEqual(node.total_force, 10.0)
|
self.assertAlmostEqual(node.total_force, 10.0)
|
||||||
self.assertAlmostEqual(node.force_balance, 0.0)
|
self.assertAlmostEqual(node.force_balance, 0.0)
|
||||||
self.assertEqual(len(residuals), 5 + 18 * 3)
|
self.assertEqual(len(residuals), 5)
|
||||||
|
self.assertEqual(
|
||||||
|
node.pressure_flow_equation_values(),
|
||||||
|
tuple(residual.value for residual in residuals),
|
||||||
|
)
|
||||||
self.assertTrue(all(abs(residual.value) <= 1.0e-12 for residual in residuals))
|
self.assertTrue(all(abs(residual.value) <= 1.0e-12 for residual in residuals))
|
||||||
self.assertEqual(node.component_result_values(), {"tforce": 10.0})
|
self.assertEqual(node.component_result_values(), {"tforce": 10.0})
|
||||||
|
|
||||||
|
|||||||
@@ -423,6 +423,8 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
|
|||||||
result = run_system_xml_simulation(xml)
|
result = run_system_xml_simulation(xml)
|
||||||
|
|
||||||
self.assertTrue(result["success"], result["message"])
|
self.assertTrue(result["success"], result["message"])
|
||||||
|
self.assertNotIn("node_1.port_4.x", result["series"])
|
||||||
|
self.assertNotIn("node_1.port_21.f", result["series"])
|
||||||
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
||||||
|
|
||||||
def test_elastic_contact_project_compiles_and_simulates(self) -> None:
|
def test_elastic_contact_project_compiles_and_simulates(self) -> None:
|
||||||
|
|||||||
@@ -241,6 +241,77 @@ class IntegrateOdeTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_segmented_solver_reports_implicit_work_by_event_segment(self) -> None:
|
||||||
|
import numpy as np
|
||||||
|
import scipy.integrate
|
||||||
|
|
||||||
|
class CountingBDF:
|
||||||
|
def __init__(self, _fun, t0, y0, t_bound, **_kwargs):
|
||||||
|
self.t = float(t0)
|
||||||
|
self.y = np.asarray(y0, dtype=float)
|
||||||
|
self.t_bound = float(t_bound)
|
||||||
|
self.status = "running"
|
||||||
|
self.nfev = 2
|
||||||
|
self.njev = 1
|
||||||
|
self.nlu = 0
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
self.t = self.t_bound
|
||||||
|
self.nfev += 3
|
||||||
|
self.nlu += 2
|
||||||
|
self.status = "finished"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def dense_output(self):
|
||||||
|
state = self.y.copy()
|
||||||
|
return lambda _time: state.copy()
|
||||||
|
|
||||||
|
with patch.object(scipy.integrate, "BDF", CountingBDF):
|
||||||
|
result = integrate_ode(
|
||||||
|
rhs=lambda _time, _state: [0.0],
|
||||||
|
initial_state=[1.0],
|
||||||
|
config=SolveIVPConfig(
|
||||||
|
t_start=0.0,
|
||||||
|
t_stop=1.0,
|
||||||
|
method="BDF",
|
||||||
|
max_step=1.0,
|
||||||
|
),
|
||||||
|
t_eval=[0.0, 0.4, 1.0],
|
||||||
|
breakpoints=[0.4],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(result.success, result.message)
|
||||||
|
self.assertEqual(len(result.solver_segments), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[segment.as_dict() for segment in result.solver_segments],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"startTime": 0.0,
|
||||||
|
"requestedStopTime": 0.4,
|
||||||
|
"simulatedUntil": 0.4,
|
||||||
|
"nfev": 5,
|
||||||
|
"njev": 1,
|
||||||
|
"nlu": 2,
|
||||||
|
"acceptedStepCount": 1,
|
||||||
|
"solverStartCount": 1,
|
||||||
|
"stateTransitionCount": 0,
|
||||||
|
"recoverableRetryCount": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"startTime": 0.4,
|
||||||
|
"requestedStopTime": 1.0,
|
||||||
|
"simulatedUntil": 1.0,
|
||||||
|
"nfev": 5,
|
||||||
|
"njev": 1,
|
||||||
|
"nlu": 2,
|
||||||
|
"acceptedStepCount": 1,
|
||||||
|
"solverStartCount": 1,
|
||||||
|
"stateTransitionCount": 0,
|
||||||
|
"recoverableRetryCount": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_segmented_solver_can_cancel_after_crossing_a_breakpoint(self) -> None:
|
def test_segmented_solver_can_cancel_after_crossing_a_breakpoint(self) -> None:
|
||||||
callback_times: list[float] = []
|
callback_times: list[float] = []
|
||||||
cancellation_requested = False
|
cancellation_requested = False
|
||||||
|
|||||||
@@ -322,6 +322,23 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
|||||||
self.assertFalse(result.success)
|
self.assertFalse(result.success)
|
||||||
self.assertEqual(result.status, "cancelled")
|
self.assertEqual(result.status, "cancelled")
|
||||||
self.assertGreaterEqual(result.diagnostics["sampleCount"], 2)
|
self.assertGreaterEqual(result.diagnostics["sampleCount"], 2)
|
||||||
|
integration = result.diagnostics["integration"]
|
||||||
|
self.assertEqual(integration["method"], "BDF")
|
||||||
|
self.assertEqual(integration["segmentCount"], len(integration["segments"]))
|
||||||
|
self.assertEqual(
|
||||||
|
integration["totals"]["nfev"],
|
||||||
|
sum(segment["nfev"] for segment in integration["segments"]),
|
||||||
|
)
|
||||||
|
sparsity = integration["jacobianSparsity"]
|
||||||
|
self.assertGreater(sparsity["nonzeroCount"], 0)
|
||||||
|
self.assertGreater(sparsity["colorGroupCount"], 0)
|
||||||
|
self.assertEqual(
|
||||||
|
integration["totals"]["finiteDifferenceRhsEstimate"],
|
||||||
|
sum(
|
||||||
|
segment["finiteDifferenceRhsEstimate"]
|
||||||
|
for segment in integration["segments"]
|
||||||
|
),
|
||||||
|
)
|
||||||
self.assertGreater(result.simulated_until, 0.0)
|
self.assertGreater(result.simulated_until, 0.0)
|
||||||
self.assertLess(result.simulated_until, 0.05)
|
self.assertLess(result.simulated_until, 0.05)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -219,6 +219,25 @@ class PressureFlowSolverInitializationTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(high.port_b.m_flow, -valve.port_2.m_flow, places=10)
|
self.assertAlmostEqual(high.port_b.m_flow, -valve.port_2.m_flow, places=10)
|
||||||
self.assertAlmostEqual(low.port_a.m_flow, -valve.port_3.m_flow, places=10)
|
self.assertAlmostEqual(low.port_a.m_flow, -valve.port_3.m_flow, places=10)
|
||||||
|
|
||||||
|
def test_compiled_plan_does_not_rebuild_equation_metadata_during_solve(self) -> None:
|
||||||
|
boundary = AmesimPnpl01("compiled_closed")
|
||||||
|
boundary.port_1.p = 100_000.0
|
||||||
|
boundary.port_1.m_flow = 1.0
|
||||||
|
network = SimulationNetwork("compiled-closed-boundary")
|
||||||
|
network.add_component(boundary)
|
||||||
|
solver = PressureFlowSolver(network)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
boundary,
|
||||||
|
"pressure_flow_equation_residuals",
|
||||||
|
side_effect=AssertionError("equation metadata rebuilt at runtime"),
|
||||||
|
):
|
||||||
|
diagnostics = solver.solve()
|
||||||
|
|
||||||
|
self.assertTrue(diagnostics.success)
|
||||||
|
self.assertEqual(diagnostics.evaluations, 0)
|
||||||
|
self.assertEqual(boundary.port_1.m_flow, 0.0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _closed_boundary_solver() -> PressureFlowSolver:
|
def _closed_boundary_solver() -> PressureFlowSolver:
|
||||||
boundary = AmesimPnpl01("closed")
|
boundary = AmesimPnpl01("closed")
|
||||||
|
|||||||
Reference in new issue
Block a user