chore: sync project state and current artifacts
This commit is contained in:
345
crates/cli/src/config.rs
Normal file
345
crates/cli/src/config.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user