chore: sync project state and current artifacts
This commit is contained in:
744
crates/cli/src/run.rs
Normal file
744
crates/cli/src/run.rs
Normal file
@@ -0,0 +1,744 @@
|
||||
//! Single simulation execution module.
|
||||
//!
|
||||
//! Handles loading a configuration, running a simulation, and outputting results.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::ScenarioConfig;
|
||||
use crate::error::{CliError, CliResult};
|
||||
|
||||
/// Result of a single simulation run.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SimulationResult {
|
||||
/// Input configuration name or path.
|
||||
pub input: String,
|
||||
/// Simulation status.
|
||||
pub status: SimulationStatus,
|
||||
/// Convergence information.
|
||||
pub convergence: Option<ConvergenceInfo>,
|
||||
/// Solver iterations.
|
||||
pub iterations: Option<usize>,
|
||||
/// Final state vector (P, h per edge).
|
||||
pub state: Option<Vec<StateEntry>>,
|
||||
/// Performance metrics.
|
||||
pub performance: Option<PerformanceMetrics>,
|
||||
/// Error message if failed.
|
||||
pub error: Option<String>,
|
||||
/// Execution time in milliseconds.
|
||||
pub elapsed_ms: u64,
|
||||
}
|
||||
|
||||
/// Performance metrics from simulation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PerformanceMetrics {
|
||||
/// Cooling capacity in kW.
|
||||
pub q_cooling_kw: Option<f64>,
|
||||
/// Heating capacity in kW.
|
||||
pub q_heating_kw: Option<f64>,
|
||||
/// Compressor power in kW.
|
||||
pub compressor_power_kw: Option<f64>,
|
||||
/// Coefficient of performance.
|
||||
pub cop: Option<f64>,
|
||||
}
|
||||
|
||||
/// Simulation status.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SimulationStatus {
|
||||
Converged,
|
||||
Timeout,
|
||||
NonConverged,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Convergence information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConvergenceInfo {
|
||||
/// Final residual norm.
|
||||
pub final_residual: f64,
|
||||
/// Convergence tolerance achieved.
|
||||
pub tolerance: f64,
|
||||
}
|
||||
|
||||
/// State entry for one edge.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateEntry {
|
||||
/// Edge index.
|
||||
pub edge: usize,
|
||||
/// Pressure in bar.
|
||||
pub pressure_bar: f64,
|
||||
/// Enthalpy in kJ/kg.
|
||||
pub enthalpy_kj_kg: f64,
|
||||
}
|
||||
|
||||
/// Run a single simulation from a configuration file.
|
||||
pub fn run_simulation(
|
||||
config_path: &Path,
|
||||
output_path: Option<&Path>,
|
||||
verbose: bool,
|
||||
) -> CliResult<SimulationResult> {
|
||||
let start = std::time::Instant::now();
|
||||
let input_name = config_path.display().to_string();
|
||||
|
||||
if verbose {
|
||||
info!("Loading configuration from: {}", config_path.display());
|
||||
}
|
||||
|
||||
let config = ScenarioConfig::from_file(config_path)?;
|
||||
|
||||
if verbose {
|
||||
info!("Scenario: {:?}", config.name);
|
||||
info!("Primary fluid: {}", config.fluid);
|
||||
info!("Circuits: {}", config.circuits.len());
|
||||
info!("Thermal couplings: {}", config.thermal_couplings.len());
|
||||
info!("Solver: {}", config.solver.strategy);
|
||||
}
|
||||
|
||||
let result = execute_simulation(&config, &input_name, start.elapsed().as_millis() as u64);
|
||||
|
||||
if let Some(ref path) = output_path {
|
||||
let json = serde_json::to_string_pretty(&result)
|
||||
.map_err(|e| CliError::Simulation(format!("Failed to serialize result: {}", e)))?;
|
||||
std::fs::write(path, json)?;
|
||||
if verbose {
|
||||
info!("Results written to: {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Execute the simulation with the given configuration.
|
||||
fn execute_simulation(
|
||||
config: &ScenarioConfig,
|
||||
input_name: &str,
|
||||
elapsed_ms: u64,
|
||||
) -> SimulationResult {
|
||||
use entropyk::{
|
||||
ConvergenceStatus, FallbackSolver, FluidId, NewtonConfig, PicardConfig, Solver,
|
||||
SolverStrategy, System, ThermalConductance,
|
||||
};
|
||||
use entropyk_fluids::TestBackend;
|
||||
use entropyk_solver::{CircuitId, ThermalCoupling};
|
||||
use std::collections::HashMap;
|
||||
|
||||
let fluid_id = FluidId::new(&config.fluid);
|
||||
let backend: Arc<dyn entropyk_fluids::FluidBackend> = Arc::new(TestBackend::new());
|
||||
let mut system = System::new();
|
||||
|
||||
// Track component name -> node index mapping per circuit
|
||||
let mut component_indices: HashMap<String, petgraph::graph::NodeIndex> = HashMap::new();
|
||||
|
||||
for circuit_config in &config.circuits {
|
||||
let circuit_id = CircuitId(circuit_config.id as u8);
|
||||
|
||||
for component_config in &circuit_config.components {
|
||||
match create_component(
|
||||
&component_config.component_type,
|
||||
&component_config.params,
|
||||
&fluid_id,
|
||||
Arc::clone(&backend),
|
||||
) {
|
||||
Ok(component) => match system.add_component_to_circuit(component, circuit_id) {
|
||||
Ok(node_id) => {
|
||||
component_indices.insert(component_config.name.clone(), node_id);
|
||||
}
|
||||
Err(e) => {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"Failed to add component '{}': {:?}",
|
||||
component_config.name, e
|
||||
)),
|
||||
elapsed_ms,
|
||||
};
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"Failed to create component '{}': {}",
|
||||
component_config.name, e
|
||||
)),
|
||||
elapsed_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add edges between components
|
||||
for circuit_config in &config.circuits {
|
||||
for edge in &circuit_config.edges {
|
||||
let from_parts: Vec<&str> = edge.from.split(':').collect();
|
||||
let to_parts: Vec<&str> = edge.to.split(':').collect();
|
||||
|
||||
let from_name = from_parts.get(0).unwrap_or(&"");
|
||||
let to_name = to_parts.get(0).unwrap_or(&"");
|
||||
|
||||
let from_node = component_indices.get(*from_name);
|
||||
let to_node = component_indices.get(*to_name);
|
||||
|
||||
match (from_node, to_node) {
|
||||
(Some(from), Some(to)) => {
|
||||
if let Err(e) = system.add_edge(*from, *to) {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"Failed to add edge '{} -> {}': {:?}",
|
||||
edge.from, edge.to, e
|
||||
)),
|
||||
elapsed_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"Edge references unknown component: '{}' or '{}'",
|
||||
from_name, to_name
|
||||
)),
|
||||
elapsed_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for coupling_config in &config.thermal_couplings {
|
||||
let coupling = ThermalCoupling::new(
|
||||
CircuitId(coupling_config.hot_circuit as u8),
|
||||
CircuitId(coupling_config.cold_circuit as u8),
|
||||
ThermalConductance::from_watts_per_kelvin(coupling_config.ua),
|
||||
)
|
||||
.with_efficiency(coupling_config.efficiency);
|
||||
|
||||
if let Err(e) = system.add_thermal_coupling(coupling) {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!("Failed to add thermal coupling: {:?}", e)),
|
||||
elapsed_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = system.finalize() {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!("System finalization failed: {:?}", e)),
|
||||
elapsed_ms,
|
||||
};
|
||||
}
|
||||
|
||||
let result = match config.solver.strategy.as_str() {
|
||||
"newton" => {
|
||||
let mut strategy = SolverStrategy::NewtonRaphson(NewtonConfig::default());
|
||||
strategy.solve(&mut system)
|
||||
}
|
||||
"picard" => {
|
||||
let mut strategy = SolverStrategy::SequentialSubstitution(PicardConfig::default());
|
||||
strategy.solve(&mut system)
|
||||
}
|
||||
"fallback" | _ => {
|
||||
let mut solver = FallbackSolver::default_solver();
|
||||
solver.solve(&mut system)
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(converged) => {
|
||||
let status = match converged.status {
|
||||
ConvergenceStatus::Converged => SimulationStatus::Converged,
|
||||
ConvergenceStatus::TimedOutWithBestState => SimulationStatus::Timeout,
|
||||
ConvergenceStatus::ControlSaturation => SimulationStatus::NonConverged,
|
||||
};
|
||||
|
||||
let state = extract_state(&converged);
|
||||
|
||||
SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status,
|
||||
convergence: Some(ConvergenceInfo {
|
||||
final_residual: converged.final_residual,
|
||||
tolerance: config.solver.tolerance,
|
||||
}),
|
||||
iterations: Some(converged.iterations),
|
||||
state: Some(state),
|
||||
performance: None,
|
||||
error: None,
|
||||
elapsed_ms,
|
||||
}
|
||||
}
|
||||
Err(e) => SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!("Solver error: {:?}", e)),
|
||||
elapsed_ms,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_param_f64(
|
||||
params: &std::collections::HashMap<String, serde_json::Value>,
|
||||
key: &str,
|
||||
) -> CliResult<f64> {
|
||||
params
|
||||
.get(key)
|
||||
.and_then(|v| v.as_f64())
|
||||
.ok_or_else(|| CliError::Config(format!("Missing required parameter: {}", key)))
|
||||
}
|
||||
|
||||
fn get_param_string(
|
||||
params: &std::collections::HashMap<String, serde_json::Value>,
|
||||
key: &str,
|
||||
) -> CliResult<String> {
|
||||
params
|
||||
.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| CliError::Config(format!("Missing required parameter: {}", key)))
|
||||
}
|
||||
|
||||
fn parse_side_conditions(
|
||||
params: &std::collections::HashMap<String, serde_json::Value>,
|
||||
prefix: &str,
|
||||
) -> CliResult<entropyk::HxSideConditions> {
|
||||
use entropyk::{HxSideConditions, MassFlow, Pressure, Temperature};
|
||||
|
||||
let fluid = get_param_string(params, &format!("{}_fluid", prefix))?;
|
||||
let t_inlet_c = get_param_f64(params, &format!("{}_t_inlet_c", prefix))?;
|
||||
let pressure_bar = params
|
||||
.get(&format!("{}_pressure_bar", prefix))
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(1.0);
|
||||
let mass_flow = params
|
||||
.get(&format!("{}_mass_flow_kg_s", prefix))
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.1);
|
||||
|
||||
Ok(HxSideConditions::new(
|
||||
Temperature::from_celsius(t_inlet_c),
|
||||
Pressure::from_bar(pressure_bar),
|
||||
MassFlow::from_kg_per_s(mass_flow),
|
||||
&fluid,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Create a component from configuration.
|
||||
fn create_component(
|
||||
component_type: &str,
|
||||
params: &std::collections::HashMap<String, serde_json::Value>,
|
||||
_primary_fluid: &entropyk::FluidId,
|
||||
backend: Arc<dyn entropyk_fluids::FluidBackend>,
|
||||
) -> CliResult<Box<dyn entropyk::Component>> {
|
||||
use entropyk::{Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger};
|
||||
use entropyk_components::heat_exchanger::{FlowConfiguration, LmtdModel};
|
||||
|
||||
match component_type {
|
||||
"Condenser" | "CondenserCoil" => {
|
||||
let ua = get_param_f64(params, "ua")?;
|
||||
let t_sat_k = params.get("t_sat_k").and_then(|v| v.as_f64());
|
||||
|
||||
if let Some(t_sat) = t_sat_k {
|
||||
Ok(Box::new(CondenserCoil::with_saturation_temp(ua, t_sat)))
|
||||
} else {
|
||||
Ok(Box::new(Condenser::new(ua)))
|
||||
}
|
||||
}
|
||||
|
||||
"Evaporator" | "EvaporatorCoil" => {
|
||||
let ua = get_param_f64(params, "ua")?;
|
||||
let t_sat_k = params.get("t_sat_k").and_then(|v| v.as_f64());
|
||||
let superheat_k = params.get("superheat_k").and_then(|v| v.as_f64());
|
||||
|
||||
let default_superheat = 5.0;
|
||||
match (t_sat_k, superheat_k) {
|
||||
(Some(t_sat), Some(sh)) => Ok(Box::new(Evaporator::with_superheat(ua, t_sat, sh))),
|
||||
(Some(t_sat), None) => Ok(Box::new(EvaporatorCoil::with_superheat(ua, t_sat, default_superheat))),
|
||||
(None, _) => Ok(Box::new(Evaporator::new(ua))),
|
||||
}
|
||||
}
|
||||
|
||||
"HeatExchanger" => {
|
||||
let ua = get_param_f64(params, "ua")?;
|
||||
let name = params
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("HeatExchanger");
|
||||
|
||||
let model = LmtdModel::new(ua, FlowConfiguration::CounterFlow);
|
||||
let mut hx = HeatExchanger::new(model, name).with_fluid_backend(backend);
|
||||
|
||||
if params.contains_key("hot_fluid") {
|
||||
let hot = parse_side_conditions(params, "hot")?;
|
||||
hx = hx.with_hot_conditions(hot);
|
||||
}
|
||||
|
||||
if params.contains_key("cold_fluid") {
|
||||
let cold = parse_side_conditions(params, "cold")?;
|
||||
hx = hx.with_cold_conditions(cold);
|
||||
}
|
||||
|
||||
Ok(Box::new(hx))
|
||||
}
|
||||
|
||||
"Compressor" => {
|
||||
let speed_rpm = get_param_f64(params, "speed_rpm")?;
|
||||
let displacement_m3 = get_param_f64(params, "displacement_m3")?;
|
||||
let efficiency = params
|
||||
.get("efficiency")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.85);
|
||||
let fluid = get_param_string(params, "fluid")?;
|
||||
|
||||
let m1 = params.get("m1").and_then(|v| v.as_f64()).unwrap_or(0.85);
|
||||
let m2 = params.get("m2").and_then(|v| v.as_f64()).unwrap_or(2.5);
|
||||
let m3 = params.get("m3").and_then(|v| v.as_f64()).unwrap_or(500.0);
|
||||
let m4 = params.get("m4").and_then(|v| v.as_f64()).unwrap_or(1500.0);
|
||||
let m5 = params.get("m5").and_then(|v| v.as_f64()).unwrap_or(-2.5);
|
||||
let m6 = params.get("m6").and_then(|v| v.as_f64()).unwrap_or(1.8);
|
||||
let m7 = params.get("m7").and_then(|v| v.as_f64()).unwrap_or(600.0);
|
||||
let m8 = params.get("m8").and_then(|v| v.as_f64()).unwrap_or(1600.0);
|
||||
let m9 = params.get("m9").and_then(|v| v.as_f64()).unwrap_or(-3.0);
|
||||
let m10 = params.get("m10").and_then(|v| v.as_f64()).unwrap_or(2.0);
|
||||
|
||||
let comp = PyCompressor::new(&fluid, speed_rpm, displacement_m3, efficiency)
|
||||
.with_coefficients(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10);
|
||||
Ok(Box::new(comp))
|
||||
}
|
||||
|
||||
"ExpansionValve" => {
|
||||
let fluid = get_param_string(params, "fluid")?;
|
||||
let opening = params.get("opening").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
let valve = PyExpansionValve::new(&fluid, opening);
|
||||
Ok(Box::new(valve))
|
||||
}
|
||||
|
||||
"Pump" => {
|
||||
let name = params
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Pump");
|
||||
Ok(Box::new(SimpleComponent::new(name, 0)))
|
||||
}
|
||||
|
||||
"Placeholder" => {
|
||||
let n_eqs = params.get("n_equations").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
Ok(Box::new(SimpleComponent::new("", n_eqs)))
|
||||
}
|
||||
|
||||
_ => Err(CliError::Config(format!(
|
||||
"Unknown component type: '{}'. Supported: Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, Pump, Placeholder",
|
||||
component_type
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract state entries from converged state.
|
||||
fn extract_state(converged: &entropyk::ConvergedState) -> Vec<StateEntry> {
|
||||
let state = &converged.state;
|
||||
let edge_count = state.len() / 2;
|
||||
|
||||
(0..edge_count)
|
||||
.map(|i| {
|
||||
let p_pa = state[i * 2];
|
||||
let h_j_kg = state[i * 2 + 1];
|
||||
StateEntry {
|
||||
edge: i,
|
||||
pressure_bar: p_pa / 1e5,
|
||||
enthalpy_kj_kg: h_j_kg / 1000.0,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Python-style components for CLI (no type-state pattern)
|
||||
// =============================================================================
|
||||
|
||||
use entropyk_fluids::FluidId as FluidsFluidId;
|
||||
use std::fmt;
|
||||
|
||||
struct SimpleComponent {
|
||||
name: String,
|
||||
n_eqs: usize,
|
||||
}
|
||||
|
||||
impl SimpleComponent {
|
||||
fn new(name: &str, n_eqs: usize) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
n_eqs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl entropyk::Component for SimpleComponent {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &entropyk::SystemState,
|
||||
residuals: &mut entropyk::ResidualVector,
|
||||
) -> Result<(), entropyk::ComponentError> {
|
||||
for i in 0..self.n_eqs.min(residuals.len()) {
|
||||
residuals[i] = if state.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
state[i % state.len()] * 1e-3
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &entropyk::SystemState,
|
||||
jacobian: &mut entropyk::JacobianBuilder,
|
||||
) -> Result<(), entropyk::ComponentError> {
|
||||
for i in 0..self.n_eqs {
|
||||
jacobian.add_entry(i, i, 1.0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
self.n_eqs
|
||||
}
|
||||
fn get_ports(&self) -> &[entropyk::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SimpleComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("SimpleComponent")
|
||||
.field("name", &self.name)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PyCompressor {
|
||||
fluid: FluidsFluidId,
|
||||
speed_rpm: f64,
|
||||
displacement_m3: f64,
|
||||
efficiency: f64,
|
||||
m1: f64,
|
||||
m2: f64,
|
||||
m3: f64,
|
||||
m4: f64,
|
||||
m5: f64,
|
||||
m6: f64,
|
||||
m7: f64,
|
||||
m8: f64,
|
||||
m9: f64,
|
||||
m10: f64,
|
||||
}
|
||||
|
||||
impl PyCompressor {
|
||||
fn new(fluid: &str, speed_rpm: f64, displacement_m3: f64, efficiency: f64) -> Self {
|
||||
Self {
|
||||
fluid: FluidsFluidId::new(fluid),
|
||||
speed_rpm,
|
||||
displacement_m3,
|
||||
efficiency,
|
||||
m1: 0.85,
|
||||
m2: 2.5,
|
||||
m3: 500.0,
|
||||
m4: 1500.0,
|
||||
m5: -2.5,
|
||||
m6: 1.8,
|
||||
m7: 600.0,
|
||||
m8: 1600.0,
|
||||
m9: -3.0,
|
||||
m10: 2.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_coefficients(
|
||||
mut self,
|
||||
m1: f64,
|
||||
m2: f64,
|
||||
m3: f64,
|
||||
m4: f64,
|
||||
m5: f64,
|
||||
m6: f64,
|
||||
m7: f64,
|
||||
m8: f64,
|
||||
m9: f64,
|
||||
m10: f64,
|
||||
) -> Self {
|
||||
self.m1 = m1;
|
||||
self.m2 = m2;
|
||||
self.m3 = m3;
|
||||
self.m4 = m4;
|
||||
self.m5 = m5;
|
||||
self.m6 = m6;
|
||||
self.m7 = m7;
|
||||
self.m8 = m8;
|
||||
self.m9 = m9;
|
||||
self.m10 = m10;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl entropyk::Component for PyCompressor {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &entropyk::SystemState,
|
||||
residuals: &mut entropyk::ResidualVector,
|
||||
) -> Result<(), entropyk::ComponentError> {
|
||||
for r in residuals.iter_mut() {
|
||||
*r = 0.0;
|
||||
}
|
||||
if state.len() >= 2 {
|
||||
residuals[0] = state[0] * 1e-3;
|
||||
residuals[1] = state[1] * 1e-3;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &entropyk::SystemState,
|
||||
jacobian: &mut entropyk::JacobianBuilder,
|
||||
) -> Result<(), entropyk::ComponentError> {
|
||||
jacobian.add_entry(0, 0, 1.0);
|
||||
jacobian.add_entry(1, 1, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
2
|
||||
}
|
||||
fn get_ports(&self) -> &[entropyk::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PyExpansionValve {
|
||||
fluid: FluidsFluidId,
|
||||
opening: f64,
|
||||
}
|
||||
|
||||
impl PyExpansionValve {
|
||||
fn new(fluid: &str, opening: f64) -> Self {
|
||||
Self {
|
||||
fluid: FluidsFluidId::new(fluid),
|
||||
opening,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl entropyk::Component for PyExpansionValve {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &entropyk::SystemState,
|
||||
residuals: &mut entropyk::ResidualVector,
|
||||
) -> Result<(), entropyk::ComponentError> {
|
||||
for r in residuals.iter_mut() {
|
||||
*r = 0.0;
|
||||
}
|
||||
if !state.is_empty() {
|
||||
residuals[0] = state[0] * 1e-3;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &entropyk::SystemState,
|
||||
jacobian: &mut entropyk::JacobianBuilder,
|
||||
) -> Result<(), entropyk::ComponentError> {
|
||||
jacobian.add_entry(0, 0, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn get_ports(&self) -> &[entropyk::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simulation_status_serialization() {
|
||||
let status = SimulationStatus::Converged;
|
||||
let json = serde_json::to_string(&status).unwrap();
|
||||
assert_eq!(json, "\"converged\"");
|
||||
|
||||
let status = SimulationStatus::NonConverged;
|
||||
let json = serde_json::to_string(&status).unwrap();
|
||||
assert_eq!(json, "\"non_converged\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simulation_result_serialization() {
|
||||
let result = SimulationResult {
|
||||
input: "test.json".to_string(),
|
||||
status: SimulationStatus::Converged,
|
||||
convergence: Some(ConvergenceInfo {
|
||||
final_residual: 1e-8,
|
||||
tolerance: 1e-6,
|
||||
}),
|
||||
iterations: Some(25),
|
||||
state: Some(vec![StateEntry {
|
||||
edge: 0,
|
||||
pressure_bar: 10.0,
|
||||
enthalpy_kj_kg: 400.0,
|
||||
}]),
|
||||
performance: None,
|
||||
error: None,
|
||||
elapsed_ms: 50,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&result).unwrap();
|
||||
assert!(json.contains("\"status\": \"converged\""));
|
||||
assert!(json.contains("\"iterations\": 25"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user