chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

345
crates/cli/src/config.rs Normal file
View File

@@ -0,0 +1,345 @@
//! Configuration parsing for CLI scenarios.
//!
//! This module defines the JSON schema for scenario configuration files
//! and provides utilities for loading and validating them.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::error::{CliError, CliResult};
/// Root configuration for a simulation scenario.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioConfig {
/// Scenario name.
#[serde(default)]
pub name: Option<String>,
/// Fluid name (e.g., "R134a", "R410A", "R744").
pub fluid: String,
/// Circuit configurations.
#[serde(default)]
pub circuits: Vec<CircuitConfig>,
/// Thermal couplings between circuits.
#[serde(default)]
pub thermal_couplings: Vec<ThermalCouplingConfig>,
/// Solver configuration.
#[serde(default)]
pub solver: SolverConfig,
/// Optional metadata.
#[serde(default)]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
/// Thermal coupling configuration between two circuits.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThermalCouplingConfig {
/// Hot circuit ID.
pub hot_circuit: usize,
/// Cold circuit ID.
pub cold_circuit: usize,
/// Thermal conductance in W/K.
pub ua: f64,
/// Heat exchanger efficiency (0.0 to 1.0).
#[serde(default = "default_efficiency")]
pub efficiency: f64,
}
fn default_efficiency() -> f64 {
0.95
}
/// Configuration for a single circuit.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitConfig {
/// Circuit ID (default: 0).
#[serde(default)]
pub id: usize,
/// Components in this circuit.
pub components: Vec<ComponentConfig>,
/// Edge connections between components.
#[serde(default)]
pub edges: Vec<EdgeConfig>,
/// Initial state for edges.
#[serde(default)]
pub initial_state: Option<InitialStateConfig>,
}
/// Configuration for a component.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentConfig {
/// Component type (e.g., "Compressor", "Condenser", "Evaporator", "ExpansionValve", "HeatExchanger").
#[serde(rename = "type")]
pub component_type: String,
/// Component name for referencing in edges.
pub name: String,
/// Component-specific parameters.
#[serde(flatten)]
pub params: HashMap<String, serde_json::Value>,
}
/// Side conditions for a heat exchanger (hot or cold fluid).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideConditionsConfig {
/// Fluid name (e.g., "R134a", "Water", "Air").
pub fluid: String,
/// Inlet temperature in °C.
pub t_inlet_c: f64,
/// Pressure in bar.
#[serde(default = "default_pressure")]
pub pressure_bar: f64,
/// Mass flow rate in kg/s.
#[serde(default = "default_mass_flow")]
pub mass_flow_kg_s: f64,
}
fn default_pressure() -> f64 {
1.0
}
fn default_mass_flow() -> f64 {
0.1
}
/// Compressor AHRI 540 coefficients configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ahri540Config {
/// Flow coefficient M1.
pub m1: f64,
/// Pressure ratio exponent M2.
pub m2: f64,
/// Power coefficients M3-M6 (cooling) and M7-M10 (heating).
#[serde(default)]
pub m3: Option<f64>,
#[serde(default)]
pub m4: Option<f64>,
#[serde(default)]
pub m5: Option<f64>,
#[serde(default)]
pub m6: Option<f64>,
#[serde(default)]
pub m7: Option<f64>,
#[serde(default)]
pub m8: Option<f64>,
#[serde(default)]
pub m9: Option<f64>,
#[serde(default)]
pub m10: Option<f64>,
}
/// Configuration for an edge between components.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeConfig {
/// Source component and port (e.g., "comp1:outlet").
pub from: String,
/// Target component and port (e.g., "cond1:inlet").
pub to: String,
}
/// Initial state configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitialStateConfig {
/// Initial pressure in bar.
pub pressure_bar: Option<f64>,
/// Initial enthalpy in kJ/kg.
pub enthalpy_kj_kg: Option<f64>,
/// Initial temperature in Kelvin.
pub temperature_k: Option<f64>,
}
/// Solver configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverConfig {
/// Solver strategy: "newton", "picard", or "fallback".
#[serde(default = "default_solver_strategy")]
pub strategy: String,
/// Maximum iterations.
#[serde(default = "default_max_iterations")]
pub max_iterations: usize,
/// Convergence tolerance.
#[serde(default = "default_tolerance")]
pub tolerance: f64,
/// Timeout in milliseconds (0 = no timeout).
#[serde(default)]
pub timeout_ms: u64,
/// Enable verbose output.
#[serde(default)]
pub verbose: bool,
}
fn default_solver_strategy() -> String {
"fallback".to_string()
}
fn default_max_iterations() -> usize {
100
}
fn default_tolerance() -> f64 {
1e-6
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
strategy: default_solver_strategy(),
max_iterations: default_max_iterations(),
tolerance: default_tolerance(),
timeout_ms: 0,
verbose: false,
}
}
}
impl ScenarioConfig {
/// Load a scenario configuration from a file.
pub fn from_file(path: &std::path::Path) -> CliResult<Self> {
let content = std::fs::read_to_string(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
CliError::ConfigNotFound(path.to_path_buf())
} else {
CliError::Io(e)
}
})?;
let config: Self = serde_json::from_str(&content).map_err(CliError::InvalidConfig)?;
config.validate()?;
Ok(config)
}
/// Load a scenario configuration from a JSON string.
pub fn from_json(json: &str) -> CliResult<Self> {
let config: Self = serde_json::from_str(json).map_err(CliError::InvalidConfig)?;
config.validate()?;
Ok(config)
}
/// Validate the configuration.
pub fn validate(&self) -> CliResult<()> {
if self.fluid.is_empty() {
return Err(CliError::Config("fluid field is required".to_string()));
}
for (i, circuit) in self.circuits.iter().enumerate() {
if circuit.components.is_empty() {
return Err(CliError::Config(format!("circuit {} has no components", i)));
}
let component_names: std::collections::HashSet<&str> =
circuit.components.iter().map(|c| c.name.as_str()).collect();
for edge in &circuit.edges {
let from_parts: Vec<&str> = edge.from.split(':').collect();
let to_parts: Vec<&str> = edge.to.split(':').collect();
if from_parts.len() != 2 || to_parts.len() != 2 {
return Err(CliError::Config(format!(
"invalid edge format '{} -> {}'. Expected 'component:port'",
edge.from, edge.to
)));
}
let from_component = from_parts[0];
let to_component = to_parts[0];
if !component_names.contains(from_component) {
return Err(CliError::Config(format!(
"edge references unknown component '{}' (in '{}'). Available: {}",
from_component,
edge.from,
component_names
.iter()
.cloned()
.collect::<Vec<_>>()
.join(", ")
)));
}
if !component_names.contains(to_component) {
return Err(CliError::Config(format!(
"edge references unknown component '{}' (in '{}'). Available: {}",
to_component,
edge.to,
component_names
.iter()
.cloned()
.collect::<Vec<_>>()
.join(", ")
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_config() {
let json = r#"{ "fluid": "R134a" }"#;
let config = ScenarioConfig::from_json(json).unwrap();
assert_eq!(config.fluid, "R134a");
assert!(config.circuits.is_empty());
}
#[test]
fn test_parse_full_config() {
let json = r#"
{
"fluid": "R410A",
"circuits": [
{
"id": 0,
"components": [
{ "type": "Compressor", "name": "comp1", "ua": 5000.0 },
{ "type": "Condenser", "name": "cond1", "ua": 5000.0 }
],
"edges": [
{ "from": "comp1:outlet", "to": "cond1:inlet" }
],
"initial_state": {
"pressure_bar": 10.0,
"enthalpy_kj_kg": 400.0
}
}
],
"solver": {
"strategy": "newton",
"max_iterations": 50,
"tolerance": 1e-8
}
}"#;
let config = ScenarioConfig::from_json(json).unwrap();
assert_eq!(config.fluid, "R410A");
assert_eq!(config.circuits.len(), 1);
assert_eq!(config.circuits[0].components.len(), 2);
assert_eq!(config.solver.strategy, "newton");
}
#[test]
fn test_validate_missing_fluid() {
let json = r#"{ "fluid": "" }"#;
let result = ScenarioConfig::from_json(json);
assert!(result.is_err());
}
#[test]
fn test_validate_invalid_edge_format() {
let json = r#"
{
"fluid": "R134a",
"circuits": [{
"id": 0,
"components": [{ "type": "Compressor", "name": "comp1", "ua": 5000.0 }],
"edges": [{ "from": "invalid", "to": "also_invalid" }]
}]
}"#;
let result = ScenarioConfig::from_json(json);
assert!(result.is_err());
}
}