完善仿真交互、结果展示与模型元数据

This commit is contained in:
ljz committed 2026-07-22 19:33:38 +08:00
1 parent f1256a121d
commit f7f1078911
26 files changed
+9042 -575

No files matched your search

+133 -28
View File
@@ -1,15 +1,23 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from math import floor, isfinite
from typing import Literal
from PythonModels.core.algebraic import PressureFlowSolver
from PythonModels.core.base import DynamicComponent
from PythonModels.core.metadata import ResultVariableMetadata
from PythonModels.core.network import Endpoint, SimulationNetwork
from PythonModels.core.solver import SolveIVPConfig, integrate_ode
from PythonModels.core.solver import ODESolution, SolveIVPConfig, integrate_ode
from PythonModels.core.stream import StreamResolver
SimulationProgressCallback = Callable[[float, str], None]
SimulationCancellationCheck = Callable[[], bool]
SimulationRunStatus = Literal["completed", "cancelled", "failed"]
@dataclass(frozen=True)
class SimulationPreparationIssue:
code: str
@@ -28,7 +36,11 @@ class SimulationPreparationError(ValueError):
@dataclass(frozen=True)
class GenericSimulationResult:
success: bool
status: SimulationRunStatus
message: str
simulated_until: float
requested_stop_time: float
variables: tuple[ResultVariableMetadata, ...]
series: dict[str, list[float]]
final: dict[str, float]
diagnostics: dict[str, object]
@@ -36,7 +48,12 @@ class GenericSimulationResult:
def as_dict(self) -> dict[str, object]:
return {
"success": self.success,
"status": self.status,
"partial": self.status != "completed",
"message": self.message,
"simulatedUntil": self.simulated_until,
"requestedStopTime": self.requested_stop_time,
"variables": [variable.as_dict() for variable in self.variables],
"series": self.series,
"final": self.final,
"diagnostics": self.diagnostics,
@@ -276,51 +293,121 @@ class GenericFluidSystem:
return derivatives
def _append_current_state(self, series: dict[str, list[float]]) -> None:
for component in self.dynamic_components:
properties = component.refresh_thermodynamic_ports()
state = component.get_state_vector()
if len(state) >= 2:
series.setdefault(f"{component.name}.m", []).append(float(state[0]))
series.setdefault(f"{component.name}.U", []).append(float(state[1]))
for name in ("p", "T", "rho", "u", "h"):
if hasattr(properties, name):
series.setdefault(f"{component.name}.{name}", []).append(
float(getattr(properties, name))
)
for component in self.network.components.values():
for port_name, port in component.ports.items():
prefix = f"{component.name}.{port_name}"
series.setdefault(f"{prefix}.p", []).append(float(port.p))
series.setdefault(f"{prefix}.m_flow", []).append(float(port.m_flow))
series.setdefault(f"{prefix}.h_outflow", []).append(
float(port.h_outflow)
)
for relative_key, value in component.result_values().items():
series.setdefault(
f"{component.name}.{relative_key}", []
).append(value)
def simulate(
self,
config: SolveIVPConfig,
*,
sample_step: float,
progress_callback: SimulationProgressCallback | None = None,
cancel_check: SimulationCancellationCheck | None = None,
) -> GenericSimulationResult:
last_reported_progress = -1.0
last_reported_phase = ""
def report_progress(
progress: float,
phase: str,
*,
force: bool = False,
) -> None:
nonlocal last_reported_phase, last_reported_progress
if progress_callback is None:
return
bounded_progress = min(1.0, max(0.0, progress))
if (
force
or phase != last_reported_phase
or bounded_progress - last_reported_progress >= 0.0025
):
last_reported_phase = phase
last_reported_progress = max(
last_reported_progress,
bounded_progress,
)
progress_callback(last_reported_progress, phase)
report_progress(0.0, "initializing", force=True)
t_eval = simulation_sample_times(config, sample_step)
initial_state = self.consistent_initial_state_vector()
report_progress(0.0, "integrating", force=True)
duration = config.t_stop - config.t_start
furthest_solver_time = config.t_start
def report_solver_time(time: float) -> None:
nonlocal furthest_solver_time
furthest_solver_time = max(furthest_solver_time, float(time))
time_fraction = (
(furthest_solver_time - config.t_start) / duration
if duration > 0.0
else 1.0
)
report_progress(time_fraction, "integrating")
def monitored_rhs(time: float, state_vector: list[float]) -> list[float]:
if cancel_check is None:
report_solver_time(time)
return self.rhs(time, state_vector)
solution = integrate_ode(
rhs=self.rhs,
rhs=monitored_rhs,
initial_state=initial_state,
config=config,
t_eval=t_eval,
cancel_check=cancel_check,
accepted_step_callback=(
report_solver_time if cancel_check is not None else None
),
)
if isinstance(solution, ODESolution):
run_status: SimulationRunStatus = solution.status
integration_error = solution.error
else:
run_status = "completed" if bool(solution.success) else "failed"
integration_error = None
result_message = str(solution.message)
postprocess_progress = (
1.0
if run_status == "completed"
else max(0.0, last_reported_progress)
)
report_progress(postprocess_progress, "postprocessing", force=True)
times = [float(value) for value in solution.t]
series: dict[str, list[float]] = {"time": times}
series: dict[str, list[float]] = {"time": []}
postprocessing_error: Exception | None = None
for time_index in range(len(times)):
if (
run_status == "completed"
and cancel_check is not None
and cancel_check()
):
run_status = "cancelled"
result_message = "Simulation was stopped while preparing partial results."
break
state = [
float(solution.y[state_index][time_index])
for state_index in range(len(solution.y))
]
self.apply_state_vector(state)
self._close_current_state()
self._append_current_state(series)
try:
self.apply_state_vector(state)
self._close_current_state()
self._append_current_state(series)
series["time"].append(times[time_index])
except Exception as exc:
run_status = "failed"
result_message = str(exc)
postprocessing_error = exc
break
if len(series["time"]) < 2:
if postprocessing_error is not None:
raise postprocessing_error
if integration_error is not None:
raise integration_error
final = {
key: values[-1]
@@ -347,11 +434,29 @@ class GenericFluidSystem:
),
},
"stateCount": len(initial_state),
"sampleCount": len(times),
"sampleCount": len(series["time"]),
}
variables = tuple(
variable
for variable in self.network.result_variable_metadata()
if variable.key in series
)
report_progress(
1.0 if run_status == "completed" else max(0.0, last_reported_progress),
"complete" if run_status == "completed" else run_status,
force=True,
)
return GenericSimulationResult(
success=bool(solution.success),
message=str(solution.message),
success=run_status == "completed" and bool(solution.success),
status=run_status,
message=result_message,
simulated_until=(
float(series["time"][-1])
if series["time"]
else float(config.t_start)
),
requested_stop_time=float(config.t_stop),
variables=variables,
series=series,
final=final,
diagnostics=diagnostics,