优化仿真求解性能并修复流量闭合问题(初版)
This commit is contained in:
1 parent
57b459bc72
commit
5332a788f3
55 files changed
+8973
-549
No files matched your search
@@ -5,10 +5,13 @@ from dataclasses import dataclass, replace
|
||||
from math import floor, isfinite
|
||||
from typing import Literal
|
||||
|
||||
from app.simulation.core.base import DynamicComponent
|
||||
from app.simulation.core.base import Component, DynamicComponent
|
||||
from app.simulation.core.metadata import ResultVariableMetadata
|
||||
from app.simulation.core.ports import PortState
|
||||
from app.simulation.performance import performance_span, profile_phase
|
||||
from app.simulation.property_cache import with_property_cache
|
||||
from app.simulation.solvers.algebraic import PressureFlowSolver
|
||||
from app.simulation.solvers.algebraic_blocks import StreamPressureBlockSolver
|
||||
from app.simulation.solvers.mechanical import (
|
||||
MechanicalConstraintGroup,
|
||||
MechanicalStateReducer,
|
||||
@@ -29,6 +32,25 @@ SimulationCancellationCheck = Callable[[], bool]
|
||||
SimulationRunStatus = Literal["completed", "cancelled", "failed"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ThermofluidClosurePlan:
|
||||
"""Static execution data for one compiled network.
|
||||
|
||||
The first pressure-flow solve remains global. Later fixed-point passes only
|
||||
need the physical islands whose constitutive equations read stream-derived
|
||||
enthalpy. An unclassified custom stream component deliberately falls back
|
||||
to the original global solve.
|
||||
"""
|
||||
|
||||
physical_ports: tuple[PortState, ...]
|
||||
global_component_group: tuple[str, ...]
|
||||
secondary_pressure_solvers: tuple[PressureFlowSolver, ...]
|
||||
secondary_component_groups: tuple[tuple[str, ...], ...]
|
||||
uses_conservative_global_solver: bool
|
||||
conservative_fallback_reason: str | None
|
||||
secondary_block_solvers: tuple[StreamPressureBlockSolver, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SimulationPreparationIssue:
|
||||
code: str
|
||||
@@ -357,15 +379,271 @@ class GenericFluidSystem:
|
||||
self.pneumatic_volume_resolver = PneumaticVolumeResolver(network)
|
||||
self.signal_resolver = SignalResolver(network)
|
||||
self.stream_resolver = StreamResolver(network)
|
||||
self._thermofluid_closure_plan = self._build_thermofluid_closure_plan()
|
||||
self.algebraic_solve_count = 0
|
||||
self.algebraic_seeded_solve_count = 0
|
||||
self.algebraic_nonlinear_solve_count = 0
|
||||
self.algebraic_optimizer_evaluation_count = 0
|
||||
self.algebraic_residual_evaluation_count = 0
|
||||
self.algebraic_block_fallback_count = 0
|
||||
self.algebraic_dense_fallback_count = 0
|
||||
self.thermofluid_pressure_pass_count = 0
|
||||
self.max_algebraic_residual = 0.0
|
||||
self.max_algebraic_evaluations = 0
|
||||
self.max_algebraic_residual_evaluations = 0
|
||||
self._last_algebraic_diagnostics = None
|
||||
self._last_algebraic_scope: tuple[str, ...] = ()
|
||||
self.max_stream_iterations = 0
|
||||
self.max_thermofluid_iterations = 0
|
||||
self.signal_propagation_count = 0
|
||||
self.pneumatic_volume_propagation_count = 0
|
||||
self._jacobian_sparsity = None
|
||||
|
||||
def _request_causal_residual_audit(self) -> None:
|
||||
"""Make topology or mode boundaries verify the next causal closure."""
|
||||
|
||||
self.pressure_flow_solver.request_causal_audit()
|
||||
for block_solver in (
|
||||
self._thermofluid_closure_plan.secondary_block_solvers
|
||||
):
|
||||
block_solver.request_causal_audit()
|
||||
|
||||
@staticmethod
|
||||
def _overrides_stream_update(component: Component) -> bool:
|
||||
component_type = type(component)
|
||||
return (
|
||||
component_type.update_stream_outflows
|
||||
is not Component.update_stream_outflows
|
||||
or component_type.update_flow_temperature_references
|
||||
is not Component.update_flow_temperature_references
|
||||
)
|
||||
|
||||
def _physical_component_groups(self) -> tuple[tuple[str, ...], ...]:
|
||||
"""Return physical islands in component insertion order."""
|
||||
|
||||
physical_names = tuple(
|
||||
component.name
|
||||
for component in self.network.components.values()
|
||||
if any(
|
||||
definition.kind == "physical"
|
||||
for definition in component.active_port_definitions
|
||||
)
|
||||
)
|
||||
adjacency = {name: set() for name in physical_names}
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
first, second = connection.endpoints
|
||||
adjacency[first.component].add(second.component)
|
||||
adjacency[second.component].add(first.component)
|
||||
|
||||
groups: list[tuple[str, ...]] = []
|
||||
visited: set[str] = set()
|
||||
for root in physical_names:
|
||||
if root in visited:
|
||||
continue
|
||||
members = {root}
|
||||
pending = [root]
|
||||
visited.add(root)
|
||||
while pending:
|
||||
current = pending.pop()
|
||||
for neighbor in adjacency[current]:
|
||||
if neighbor in visited:
|
||||
continue
|
||||
visited.add(neighbor)
|
||||
members.add(neighbor)
|
||||
pending.append(neighbor)
|
||||
groups.append(tuple(name for name in physical_names if name in members))
|
||||
return tuple(groups)
|
||||
|
||||
def _network_for_component_group(
|
||||
self,
|
||||
component_names: tuple[str, ...],
|
||||
all_physical_names: frozenset[str],
|
||||
) -> SimulationNetwork:
|
||||
if frozenset(component_names) == all_physical_names:
|
||||
return self.network
|
||||
|
||||
selected = frozenset(component_names)
|
||||
subnetwork = SimulationNetwork(
|
||||
name=f"{self.network.name}:thermofluid:{len(component_names)}"
|
||||
)
|
||||
for component in self.network.components.values():
|
||||
if component.name in selected:
|
||||
subnetwork.add_component(component)
|
||||
subnetwork.connections.extend(
|
||||
connection
|
||||
for connection in self.network.connections
|
||||
if connection.kind == "physical"
|
||||
and connection.endpoint_a.component in selected
|
||||
and connection.endpoint_b.component in selected
|
||||
)
|
||||
return subnetwork
|
||||
|
||||
def _pressure_solver_for_component_group(
|
||||
self,
|
||||
component_names: tuple[str, ...],
|
||||
all_physical_names: frozenset[str],
|
||||
) -> PressureFlowSolver:
|
||||
subnetwork = self._network_for_component_group(
|
||||
component_names,
|
||||
all_physical_names,
|
||||
)
|
||||
if subnetwork is self.network:
|
||||
return self.pressure_flow_solver
|
||||
return PressureFlowSolver(
|
||||
subnetwork,
|
||||
residual_tolerance=self.pressure_flow_solver.residual_tolerance,
|
||||
max_evaluations=self.pressure_flow_solver.max_evaluations,
|
||||
scope_kind="physicalIsland",
|
||||
)
|
||||
|
||||
def _build_thermofluid_closure_plan(self) -> _ThermofluidClosurePlan:
|
||||
physical_ports = tuple(
|
||||
component.get_port(definition.name)
|
||||
for component in self.network.components.values()
|
||||
for definition in component.active_port_definitions
|
||||
if definition.kind == "physical"
|
||||
)
|
||||
physical_groups = self._physical_component_groups()
|
||||
all_physical_names = frozenset(
|
||||
name for group in physical_groups for name in group
|
||||
)
|
||||
all_physical_order = tuple(
|
||||
name for group in physical_groups for name in group
|
||||
)
|
||||
|
||||
sensitive_names: set[str] = set()
|
||||
has_unclassified_stream_component = False
|
||||
has_invalid_dependency_declaration = False
|
||||
for name in all_physical_names:
|
||||
component = self.network.components[name]
|
||||
# Only an exact-class declaration opts into pruning. A custom
|
||||
# subclass cannot accidentally inherit a purity promise after
|
||||
# changing its stream hook or constitutive equations.
|
||||
declared = type(component).__dict__.get(
|
||||
"PRESSURE_FLOW_DEPENDS_ON_STREAM"
|
||||
)
|
||||
if declared is True:
|
||||
sensitive_names.add(name)
|
||||
elif declared is False:
|
||||
continue
|
||||
elif declared is None and self._overrides_stream_update(component):
|
||||
# Preserve the exact legacy behavior for custom components that
|
||||
# receive stream values but have not declared equation purity.
|
||||
has_unclassified_stream_component = True
|
||||
elif declared is not None:
|
||||
has_invalid_dependency_declaration = True
|
||||
|
||||
if has_invalid_dependency_declaration:
|
||||
return _ThermofluidClosurePlan(
|
||||
physical_ports=physical_ports,
|
||||
global_component_group=all_physical_order,
|
||||
secondary_pressure_solvers=(self.pressure_flow_solver,),
|
||||
secondary_component_groups=(all_physical_order,),
|
||||
uses_conservative_global_solver=True,
|
||||
conservative_fallback_reason="invalidDependencyDeclaration",
|
||||
)
|
||||
|
||||
if has_unclassified_stream_component:
|
||||
return _ThermofluidClosurePlan(
|
||||
physical_ports=physical_ports,
|
||||
global_component_group=all_physical_order,
|
||||
secondary_pressure_solvers=(self.pressure_flow_solver,),
|
||||
secondary_component_groups=(all_physical_order,),
|
||||
uses_conservative_global_solver=True,
|
||||
conservative_fallback_reason="unclassifiedStreamComponent",
|
||||
)
|
||||
|
||||
component_names = set(self.network.components)
|
||||
compiled_equations = self.pressure_flow_solver.equation_templates
|
||||
for equation in compiled_equations:
|
||||
if equation.owner != "component":
|
||||
continue
|
||||
referenced_components = {
|
||||
parts[0]
|
||||
for variable in equation.variables
|
||||
if len(parts := variable.rsplit(".", 2)) == 3
|
||||
and parts[0] in component_names
|
||||
}
|
||||
if referenced_components - {equation.owner_id}:
|
||||
# Catalog equations are component-local and connectors carry
|
||||
# cross-component constraints. A custom residual may violate
|
||||
# that convention, so retain the unsplit global problem.
|
||||
return _ThermofluidClosurePlan(
|
||||
physical_ports=physical_ports,
|
||||
global_component_group=all_physical_order,
|
||||
secondary_pressure_solvers=(self.pressure_flow_solver,),
|
||||
secondary_component_groups=(all_physical_order,),
|
||||
uses_conservative_global_solver=True,
|
||||
conservative_fallback_reason="crossComponentEquation",
|
||||
)
|
||||
|
||||
for group in physical_groups:
|
||||
selected = frozenset(group)
|
||||
connection_ids = {
|
||||
connection.id
|
||||
for connection in self.network.connections
|
||||
if connection.kind == "physical"
|
||||
and connection.endpoint_a.component in selected
|
||||
and connection.endpoint_b.component in selected
|
||||
}
|
||||
unknown_count = sum(
|
||||
unknown.component in selected
|
||||
for unknown in self.pressure_flow_solver.unknowns
|
||||
)
|
||||
equation_count = sum(
|
||||
(
|
||||
equation.owner == "component"
|
||||
and equation.owner_id in selected
|
||||
)
|
||||
or (
|
||||
equation.owner == "connection"
|
||||
and equation.owner_id in connection_ids
|
||||
)
|
||||
for equation in compiled_equations
|
||||
)
|
||||
if unknown_count != equation_count:
|
||||
# The full network can be square even when two disconnected
|
||||
# rectangular islands happen to cancel each other's equation
|
||||
# count. Preserve the original global least-squares problem in
|
||||
# that unusual case rather than changing its solution space.
|
||||
return _ThermofluidClosurePlan(
|
||||
physical_ports=physical_ports,
|
||||
global_component_group=all_physical_order,
|
||||
secondary_pressure_solvers=(self.pressure_flow_solver,),
|
||||
secondary_component_groups=(all_physical_order,),
|
||||
uses_conservative_global_solver=True,
|
||||
conservative_fallback_reason="nonSquarePhysicalIsland",
|
||||
)
|
||||
|
||||
coupled_groups = tuple(
|
||||
group for group in physical_groups if sensitive_names.intersection(group)
|
||||
)
|
||||
secondary_pressure_solvers = tuple(
|
||||
self._pressure_solver_for_component_group(group, all_physical_names)
|
||||
for group in coupled_groups
|
||||
)
|
||||
secondary_block_solvers = tuple(
|
||||
StreamPressureBlockSolver(
|
||||
pressure_solver,
|
||||
tuple(name for name in group if name in sensitive_names),
|
||||
)
|
||||
for pressure_solver, group in zip(
|
||||
secondary_pressure_solvers,
|
||||
coupled_groups,
|
||||
)
|
||||
)
|
||||
return _ThermofluidClosurePlan(
|
||||
physical_ports=physical_ports,
|
||||
global_component_group=all_physical_order,
|
||||
secondary_pressure_solvers=secondary_pressure_solvers,
|
||||
secondary_component_groups=coupled_groups,
|
||||
uses_conservative_global_solver=False,
|
||||
conservative_fallback_reason=None,
|
||||
secondary_block_solvers=secondary_block_solvers,
|
||||
)
|
||||
|
||||
def initial_state_vector(self) -> list[float]:
|
||||
return self.pneumatic_storage_reducer.synchronize_state_vector(
|
||||
self.mechanical_state_reducer.initial_state_vector(),
|
||||
@@ -377,6 +655,129 @@ class GenericFluidSystem:
|
||||
self.pneumatic_storage_reducer.synchronize_state_vector(values)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _entry_has_pneumatic_state(entry: object) -> bool:
|
||||
"""Return whether one reduced ODE entry owns pneumatic state.
|
||||
|
||||
Mechanical constraint groups are synthetic state owners. Every other
|
||||
entry is a dynamic component, so its active port metadata is the
|
||||
topology-level way to classify it without depending on model names.
|
||||
"""
|
||||
|
||||
if isinstance(entry, MechanicalConstraintGroup):
|
||||
return False
|
||||
return any(
|
||||
definition.kind == "physical" and definition.domain == "pneumatic"
|
||||
for definition in entry.active_port_definitions
|
||||
)
|
||||
|
||||
def _add_pneumatic_volume_state_dependencies(
|
||||
self,
|
||||
dependencies: list[set[int]],
|
||||
entries: tuple[object, ...],
|
||||
owner_by_component: dict[str, int],
|
||||
) -> None:
|
||||
"""Close the cross-domain dependency hidden by external volume ports.
|
||||
|
||||
A pneumatic-volume source such as a piston writes swept volume from
|
||||
mechanical coordinates into a connected storage component before the
|
||||
pressure-flow closure. The ordinary physical-path walk intentionally
|
||||
stops at a storage state. Consequently, a second storage connected to
|
||||
that chamber can depend on the piston even though the path crosses the
|
||||
chamber state, and that derivative was previously omitted from the BDF
|
||||
sparsity pattern.
|
||||
|
||||
Reuse the resolver's compiled output/connection plan to locate each
|
||||
receiving storage. Mechanical states already found from that receiver,
|
||||
the volume source's own ODE state (when it has one), and pneumatic states
|
||||
whose local closure reaches the receiver form one conservative
|
||||
cross-domain dependency set. Add it in both directions. If executable
|
||||
custom/source metadata cannot bound those drivers, use a dense pattern.
|
||||
"""
|
||||
|
||||
resolver = self.pneumatic_volume_resolver
|
||||
pneumatic_entries = tuple(
|
||||
self._entry_has_pneumatic_state(entry) for entry in entries
|
||||
)
|
||||
all_entry_indexes = set(range(len(entries)))
|
||||
|
||||
def use_conservative_dense_pattern() -> None:
|
||||
for entry_dependencies in dependencies:
|
||||
entry_dependencies.update(all_entry_indexes)
|
||||
|
||||
for component in resolver._output_components:
|
||||
# ``pneumatic_volume_outputs`` is executable code rather than an
|
||||
# equation-level dependency declaration. Catalog components with
|
||||
# no directed signal input can be bounded by their own ODE state
|
||||
# and the mechanical states already connected through topology.
|
||||
# Custom/output components with an external signal driver keep the
|
||||
# implicit integrator safe by disabling sparsity for this system.
|
||||
if (
|
||||
not type(component).__module__.startswith(
|
||||
"app.simulation.components."
|
||||
)
|
||||
or any(
|
||||
(
|
||||
definition.kind == "signal"
|
||||
and definition.nominal_role == "input"
|
||||
)
|
||||
or (
|
||||
definition.kind == "physical"
|
||||
and definition.domain
|
||||
not in {"pneumatic", "mechanical"}
|
||||
)
|
||||
for definition in component.active_port_definitions
|
||||
)
|
||||
):
|
||||
use_conservative_dense_pattern()
|
||||
return
|
||||
|
||||
source_index = owner_by_component.get(component.name)
|
||||
receiver_indexes: set[int] = set()
|
||||
for definition in component.active_port_definitions:
|
||||
if (
|
||||
definition.kind != "physical"
|
||||
or definition.domain != "pneumatic"
|
||||
):
|
||||
continue
|
||||
binding = resolver._connected_endpoint.get(
|
||||
Endpoint(component.name, definition.name)
|
||||
)
|
||||
if binding is None:
|
||||
continue
|
||||
receiver_index = owner_by_component.get(
|
||||
binding.connected_endpoint.component
|
||||
)
|
||||
if receiver_index is not None and pneumatic_entries[receiver_index]:
|
||||
receiver_indexes.add(receiver_index)
|
||||
|
||||
for receiver_index in receiver_indexes:
|
||||
driver_indexes = {
|
||||
entry_index
|
||||
for entry_index in dependencies[receiver_index]
|
||||
if isinstance(entries[entry_index], MechanicalConstraintGroup)
|
||||
}
|
||||
if source_index is not None:
|
||||
driver_indexes.add(source_index)
|
||||
if not driver_indexes:
|
||||
use_conservative_dense_pattern()
|
||||
return
|
||||
coupled_pneumatic_indexes = {
|
||||
entry_index
|
||||
for entry_index, is_pneumatic in enumerate(pneumatic_entries)
|
||||
if is_pneumatic
|
||||
and (
|
||||
entry_index == receiver_index
|
||||
or receiver_index in dependencies[entry_index]
|
||||
)
|
||||
}
|
||||
for pneumatic_index in coupled_pneumatic_indexes:
|
||||
dependencies[pneumatic_index].update(driver_indexes)
|
||||
for driver_index in driver_indexes:
|
||||
dependencies[driver_index].update(
|
||||
coupled_pneumatic_indexes
|
||||
)
|
||||
|
||||
def _build_jacobian_sparsity(self):
|
||||
"""Build a conservative state dependency graph for implicit solvers.
|
||||
|
||||
@@ -431,6 +832,12 @@ class GenericFluidSystem:
|
||||
pending.append(neighbour)
|
||||
dependencies.append(found)
|
||||
|
||||
self._add_pneumatic_volume_state_dependencies(
|
||||
dependencies,
|
||||
entries,
|
||||
owner_by_component,
|
||||
)
|
||||
|
||||
offsets = [0]
|
||||
for state_size in entry_sizes:
|
||||
offsets.append(offsets[-1] + state_size)
|
||||
@@ -479,10 +886,12 @@ class GenericFluidSystem:
|
||||
pneumatic_volume = self.pneumatic_volume_resolver.solve()
|
||||
self.pneumatic_volume_propagation_count += pneumatic_volume.propagated
|
||||
self._refresh_dynamic_components()
|
||||
algebraic = self.pressure_flow_solver.solve(
|
||||
initial_algebraic = self.pressure_flow_solver.solve(
|
||||
effort_variables=("p",),
|
||||
)
|
||||
algebraic_diagnostics = [initial_algebraic]
|
||||
pressure_flow_solve_count = 1
|
||||
self.thermofluid_pressure_pass_count += 1
|
||||
|
||||
# Some constitutive flow laws recover their upstream temperature from
|
||||
# connected stream enthalpy, while junction stream mixing itself depends
|
||||
@@ -490,19 +899,25 @@ class GenericFluidSystem:
|
||||
# leaves that two-way coupling to the next RHS call, making the ODE RHS
|
||||
# depend on evaluation history and corrupting finite-difference
|
||||
# Jacobians. Close both layers to one fixed point inside this call.
|
||||
physical_ports = tuple(
|
||||
port
|
||||
for component in self.network.components.values()
|
||||
for definition in component.active_port_definitions
|
||||
if definition.kind == "physical"
|
||||
for port in (component.get_port(definition.name),)
|
||||
)
|
||||
# The compiled closure plan keeps custom stream-aware components on the
|
||||
# legacy global path. For catalog models, only stream-sensitive physical
|
||||
# islands are revisited; independent islands keep the first solve.
|
||||
closure_plan = self._thermofluid_closure_plan
|
||||
self._last_algebraic_diagnostics = initial_algebraic
|
||||
self._last_algebraic_scope = closure_plan.global_component_group
|
||||
physical_ports = closure_plan.physical_ports
|
||||
secondary_pressure_solvers = closure_plan.secondary_pressure_solvers
|
||||
secondary_block_solvers = closure_plan.secondary_block_solvers
|
||||
connected_h: dict[str, dict[str, float]] = {}
|
||||
stream_diagnostics = []
|
||||
max_coupling_iterations = 25
|
||||
flow_relative_tolerance = 1.0e-12
|
||||
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()
|
||||
stream, connected_h = self.stream_resolver.solve(
|
||||
dynamic_ports_are_current=True,
|
||||
)
|
||||
stream_diagnostics.append(stream)
|
||||
temperature_reference_h = (
|
||||
self.stream_resolver.connected_temperature_reference_enthalpies()
|
||||
)
|
||||
@@ -511,12 +926,57 @@ class GenericFluidSystem:
|
||||
component.update_flow_temperature_references(
|
||||
temperature_reference_h[component.name]
|
||||
)
|
||||
algebraic = self.pressure_flow_solver.solve(
|
||||
effort_variables=(
|
||||
("p",) if pressure_flow_solve_count == 0 else ()
|
||||
),
|
||||
if secondary_pressure_solvers:
|
||||
self.thermofluid_pressure_pass_count += 1
|
||||
block_scale_context = (
|
||||
self.pressure_flow_solver.scale_context()
|
||||
if any(
|
||||
solver is not self.pressure_flow_solver
|
||||
for solver in secondary_pressure_solvers
|
||||
)
|
||||
else None
|
||||
)
|
||||
pressure_flow_solve_count += 1
|
||||
if (
|
||||
secondary_block_solvers
|
||||
and not closure_plan.uses_conservative_global_solver
|
||||
):
|
||||
# Stream propagation only invalidates equations that explicitly
|
||||
# consume the new enthalpy/temperature references. Re-solve the
|
||||
# exact equation/unknown blocks containing those equations; the
|
||||
# first global pass above remains the causalization boundary for
|
||||
# mechanics, contact, and all stream-independent pneumatic blocks.
|
||||
for block_solver in secondary_block_solvers:
|
||||
block_result = block_solver.solve(
|
||||
scale_context=block_scale_context,
|
||||
)
|
||||
# One public secondary closure is one logical solve. The
|
||||
# block solver folds every local attempt and a possible
|
||||
# accepted global fallback into this single diagnostic, so
|
||||
# evaluations and blockFallbackUsed are counted exactly
|
||||
# once here rather than once per internal equation block.
|
||||
(algebraic,) = block_result.diagnostics
|
||||
(scope,) = block_result.scopes
|
||||
algebraic_diagnostics.append(algebraic)
|
||||
self._last_algebraic_diagnostics = algebraic
|
||||
self._last_algebraic_scope = scope
|
||||
pressure_flow_solve_count += 1
|
||||
else:
|
||||
for pressure_solver, component_group in zip(
|
||||
secondary_pressure_solvers,
|
||||
closure_plan.secondary_component_groups,
|
||||
):
|
||||
algebraic = pressure_solver.solve(
|
||||
effort_variables=(),
|
||||
scale_context=(
|
||||
block_scale_context
|
||||
if pressure_solver is not self.pressure_flow_solver
|
||||
else None
|
||||
),
|
||||
)
|
||||
algebraic_diagnostics.append(algebraic)
|
||||
self._last_algebraic_diagnostics = algebraic
|
||||
self._last_algebraic_scope = component_group
|
||||
pressure_flow_solve_count += 1
|
||||
current_flows = tuple(port.m_flow for port in physical_ports)
|
||||
flow_scale = max(
|
||||
[abs(value) for value in (*previous_flows, *current_flows)] + [1.0]
|
||||
@@ -528,7 +988,10 @@ class GenericFluidSystem:
|
||||
),
|
||||
default=0.0,
|
||||
)
|
||||
if max_flow_delta <= flow_relative_tolerance * flow_scale:
|
||||
if (
|
||||
not secondary_pressure_solvers
|
||||
or max_flow_delta <= flow_relative_tolerance * flow_scale
|
||||
):
|
||||
break
|
||||
else:
|
||||
raise ThermofluidClosureError(
|
||||
@@ -541,17 +1004,40 @@ class GenericFluidSystem:
|
||||
)
|
||||
self.mechanical_state_reducer.update_constraint_accelerations()
|
||||
self.algebraic_solve_count += pressure_flow_solve_count
|
||||
seeded_count = sum(
|
||||
item.jacobian_mode == "seeded" for item in algebraic_diagnostics
|
||||
)
|
||||
self.algebraic_seeded_solve_count += seeded_count
|
||||
self.algebraic_nonlinear_solve_count += (
|
||||
len(algebraic_diagnostics) - seeded_count
|
||||
)
|
||||
self.algebraic_optimizer_evaluation_count += sum(
|
||||
item.evaluations for item in algebraic_diagnostics
|
||||
)
|
||||
self.algebraic_residual_evaluation_count += sum(
|
||||
item.residual_evaluations for item in algebraic_diagnostics
|
||||
)
|
||||
self.algebraic_block_fallback_count += sum(
|
||||
item.block_fallback_used for item in algebraic_diagnostics
|
||||
)
|
||||
self.algebraic_dense_fallback_count += sum(
|
||||
item.dense_fallback_used for item in algebraic_diagnostics
|
||||
)
|
||||
self.max_algebraic_residual = max(
|
||||
self.max_algebraic_residual,
|
||||
algebraic.max_scaled_residual,
|
||||
*(item.max_scaled_residual for item in algebraic_diagnostics),
|
||||
)
|
||||
self.max_algebraic_evaluations = max(
|
||||
self.max_algebraic_evaluations,
|
||||
algebraic.evaluations,
|
||||
*(item.evaluations for item in algebraic_diagnostics),
|
||||
)
|
||||
self.max_algebraic_residual_evaluations = max(
|
||||
self.max_algebraic_residual_evaluations,
|
||||
*(item.residual_evaluations for item in algebraic_diagnostics),
|
||||
)
|
||||
self.max_stream_iterations = max(
|
||||
self.max_stream_iterations,
|
||||
stream.iterations,
|
||||
*(item.iterations for item in stream_diagnostics),
|
||||
)
|
||||
return connected_h
|
||||
|
||||
@@ -588,6 +1074,7 @@ class GenericFluidSystem:
|
||||
f"{component.name}.{relative_key}", []
|
||||
).append(value)
|
||||
|
||||
@with_property_cache
|
||||
def simulate(
|
||||
self,
|
||||
config: SolveIVPConfig,
|
||||
@@ -645,6 +1132,7 @@ class GenericFluidSystem:
|
||||
report_progress(0.0, "integrating", force=True)
|
||||
duration = config.t_stop - config.t_start
|
||||
furthest_solver_time = config.t_start
|
||||
next_signal_audit_index = 0
|
||||
|
||||
def report_solver_time(time: float) -> None:
|
||||
nonlocal furthest_solver_time
|
||||
@@ -657,10 +1145,23 @@ class GenericFluidSystem:
|
||||
report_progress(time_fraction, "integrating")
|
||||
|
||||
def monitored_rhs(time: float, state_vector: list[float]) -> list[float]:
|
||||
nonlocal next_signal_audit_index
|
||||
while (
|
||||
next_signal_audit_index < len(signal_event_times)
|
||||
and float(time) >= signal_event_times[next_signal_audit_index]
|
||||
):
|
||||
self._request_causal_residual_audit()
|
||||
next_signal_audit_index += 1
|
||||
if cancel_check is None:
|
||||
report_solver_time(time)
|
||||
return self.rhs(time, state_vector)
|
||||
|
||||
def handle_state_transition(*args):
|
||||
transition = self.mechanical_state_reducer.state_transition(*args)
|
||||
if transition is not None:
|
||||
self._request_causal_residual_audit()
|
||||
return transition
|
||||
|
||||
solution = integrate_ode(
|
||||
rhs=monitored_rhs,
|
||||
initial_state=initial_state,
|
||||
@@ -672,7 +1173,7 @@ class GenericFluidSystem:
|
||||
),
|
||||
breakpoints=signal_event_times,
|
||||
state_transition_handler=(
|
||||
self.mechanical_state_reducer.state_transition
|
||||
handle_state_transition
|
||||
if self.mechanical_state_reducer.has_state_events
|
||||
else None
|
||||
),
|
||||
@@ -791,13 +1292,66 @@ class GenericFluidSystem:
|
||||
},
|
||||
"pressureFlow": {
|
||||
"solveCount": self.algebraic_solve_count,
|
||||
"seededSolveCount": self.algebraic_seeded_solve_count,
|
||||
"nonlinearSolveCount": self.algebraic_nonlinear_solve_count,
|
||||
"fastPathHitRate": (
|
||||
self.algebraic_seeded_solve_count
|
||||
/ self.algebraic_solve_count
|
||||
if self.algebraic_solve_count
|
||||
else 0.0
|
||||
),
|
||||
"optimizerEvaluationCount": (
|
||||
self.algebraic_optimizer_evaluation_count
|
||||
),
|
||||
"residualEvaluationCount": (
|
||||
self.algebraic_residual_evaluation_count
|
||||
),
|
||||
"blockFallbackCount": self.algebraic_block_fallback_count,
|
||||
"denseFallbackCount": self.algebraic_dense_fallback_count,
|
||||
"closurePassCount": self.thermofluid_pressure_pass_count,
|
||||
"secondaryPhysicalIslandCount": len(
|
||||
self._thermofluid_closure_plan.secondary_pressure_solvers
|
||||
),
|
||||
"secondaryBlockCount": sum(
|
||||
len(solver.blocks)
|
||||
for solver in self._thermofluid_closure_plan.secondary_block_solvers
|
||||
if solver.available
|
||||
),
|
||||
"secondaryUnknownCount": sum(
|
||||
len(block.unknowns)
|
||||
for solver in self._thermofluid_closure_plan.secondary_block_solvers
|
||||
if solver.available
|
||||
for block in solver.blocks
|
||||
),
|
||||
"equationBlockFallbackReasons": [
|
||||
solver.fallback_reason
|
||||
for solver in self._thermofluid_closure_plan.secondary_block_solvers
|
||||
if solver.fallback_reason is not None
|
||||
],
|
||||
"usesConservativeGlobalCoupling": (
|
||||
self._thermofluid_closure_plan.uses_conservative_global_solver
|
||||
),
|
||||
"couplingPlanFallbackReason": (
|
||||
self._thermofluid_closure_plan.conservative_fallback_reason
|
||||
),
|
||||
"maxScaledResidual": self.max_algebraic_residual,
|
||||
"maxEvaluationsPerSolve": self.max_algebraic_evaluations,
|
||||
"maxResidualEvaluationsPerSolve": (
|
||||
self.max_algebraic_residual_evaluations
|
||||
),
|
||||
"lastScope": list(self._last_algebraic_scope),
|
||||
"last": (
|
||||
self.pressure_flow_solver.last_diagnostics.as_dict()
|
||||
if self.pressure_flow_solver.last_diagnostics is not None
|
||||
self._last_algebraic_diagnostics.as_dict()
|
||||
if self._last_algebraic_diagnostics is not None
|
||||
else None
|
||||
),
|
||||
"causalExecution": (
|
||||
self.pressure_flow_solver.causal_execution_diagnostics()
|
||||
),
|
||||
"secondaryCausalExecution": [
|
||||
solver.causal_execution_diagnostics()
|
||||
for solver in self._thermofluid_closure_plan.secondary_block_solvers
|
||||
],
|
||||
},
|
||||
"stream": {
|
||||
"maxIterationsPerSolve": self.max_stream_iterations,
|
||||
|
||||
Reference in new issue
Block a user