//! System snapshot structures for JSON serialization/deserialization //! //! This module provides types for capturing complete system state including //! topology, component parameters, fluid state, and backend information. use crate::coupling::ThermalCoupling; use entropyk_components::ComponentParams; use entropyk_core::SystemState; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Snapshot of the complete system for serialization /// /// Contains all information needed to reconstruct an identical system: /// - Topology (components and their connections) /// - Component parameters /// - Fluid state (pressures and enthalpies) /// - Fluid backend information /// - Solver configuration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SystemSnapshot { /// Schema version for forward/backward compatibility pub version: String, /// System topology (components, edges, thermal couplings) pub topology: TopologySnapshot, /// Component-specific parameters indexed by component name #[serde(default)] pub parameters: HashMap, /// Fluid state (edge pressures and enthalpies) #[serde(default, skip_serializing_if = "Option::is_none")] pub fluid_state: Option, /// Fluid backend information pub fluid_backend: FluidBackendInfo, /// Solver configuration #[serde(default, skip_serializing_if = "Option::is_none")] pub solver_config: Option, /// Component name → type mapping for stable reconstruction #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub component_names: HashMap, /// Component name → circuit ID mapping #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub circuit_assignments: HashMap, /// Constraints for inverse control #[serde(default, skip_serializing_if = "Vec::is_empty")] pub constraints: Vec, /// Bounded control variables for inverse control #[serde(default, skip_serializing_if = "Vec::is_empty")] pub bounded_variables: Vec, /// Optional metadata #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub metadata: HashMap, } /// Snapshot of system topology #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct TopologySnapshot { /// Flow edges between components #[serde(default)] pub edges: Vec, /// Thermal couplings between circuits #[serde(default, skip_serializing_if = "Vec::is_empty")] pub thermal_couplings: Vec, } /// Snapshot of a flow edge #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct EdgeSnapshot { /// Source component name pub source: String, /// Source port name #[serde(default)] pub source_port: String, /// Target component name pub target: String, /// Target port name #[serde(default)] pub target_port: String, /// Circuit ID pub circuit_id: u16, } /// Information about the fluid backend #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FluidBackendInfo { /// Backend name (e.g., "CoolPropBackend", "TabularBackend") pub name: String, /// Backend version pub version: String, /// Backend hash for verification #[serde(skip_serializing_if = "Option::is_none")] pub hash: Option, } /// Snapshot of solver configuration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SolverConfigSnapshot { /// Solver type ("NewtonRaphson", "SequentialSubstitution", etc.) pub solver_type: String, /// Maximum iterations pub max_iterations: usize, /// Convergence tolerance pub tolerance: f64, /// Divergence threshold pub divergence_threshold: f64, } impl Default for SolverConfigSnapshot { fn default() -> Self { Self { solver_type: "NewtonRaphson".to_string(), max_iterations: 100, tolerance: 1e-6, divergence_threshold: 1e10, } } } /// Snapshot of a constraint for inverse control #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ConstraintSnapshot { /// Constraint identifier pub id: String, /// Component name the constraint targets pub component: String, /// Output type being constrained (e.g., "capacity", "superheat") pub output_type: String, /// Target value for the constraint pub target: f64, } /// Snapshot of a bounded control variable #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BoundedVariableSnapshot { /// Variable identifier pub id: String, /// Component name the variable belongs to pub component: String, /// Variable name (e.g., "f_m", "f_power", "opening") pub variable_name: String, /// Lower bound pub lower_bound: f64, /// Upper bound pub upper_bound: f64, /// Initial value pub initial_value: f64, } #[cfg(test)] mod tests { use super::*; #[test] fn test_system_snapshot_serialization() { let snapshot = SystemSnapshot { version: "1.0".to_string(), topology: TopologySnapshot { edges: vec![], thermal_couplings: vec![], }, parameters: HashMap::new(), fluid_state: None, fluid_backend: FluidBackendInfo { name: "TestBackend".to_string(), version: "1.0.0".to_string(), hash: Some("abc123".to_string()), }, solver_config: Some(SolverConfigSnapshot::default()), component_names: HashMap::new(), circuit_assignments: HashMap::new(), constraints: vec![], bounded_variables: vec![], metadata: HashMap::new(), }; let json = serde_json::to_string_pretty(&snapshot).unwrap(); let deserialized: SystemSnapshot = serde_json::from_str(&json).unwrap(); assert_eq!(snapshot, deserialized); } #[test] fn test_solver_config_default() { let config = SolverConfigSnapshot::default(); assert_eq!(config.solver_type, "NewtonRaphson"); assert_eq!(config.max_iterations, 100); assert_eq!(config.tolerance, 1e-6); } #[test] fn test_camel_case_output() { let edge = EdgeSnapshot { source: "comp_a".to_string(), source_port: "outlet".to_string(), target: "comp_b".to_string(), target_port: "inlet".to_string(), circuit_id: 0, }; let json = serde_json::to_string(&edge).unwrap(); assert!(json.contains("\"sourcePort\"")); assert!(json.contains("\"targetPort\"")); assert!(json.contains("\"circuitId\"")); } #[test] fn test_backward_compat_missing_fields() { // Old snapshot without new fields should deserialize with defaults let old_json = r#"{ "version": "1.0", "topology": { "edges": [] }, "parameters": {}, "fluidBackend": { "name": "Test", "version": "1.0" } }"#; let snapshot: SystemSnapshot = serde_json::from_str(old_json).unwrap(); assert!(snapshot.component_names.is_empty()); assert!(snapshot.circuit_assignments.is_empty()); assert!(snapshot.constraints.is_empty()); assert!(snapshot.bounded_variables.is_empty()); } }