Files
Entropyk/crates/solver/src/snapshot.rs

227 lines
7.6 KiB
Rust

//! 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<String, ComponentParams>,
/// Fluid state (edge pressures and enthalpies)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fluid_state: Option<SystemState>,
/// Fluid backend information
pub fluid_backend: FluidBackendInfo,
/// Solver configuration
#[serde(default, skip_serializing_if = "Option::is_none")]
pub solver_config: Option<SolverConfigSnapshot>,
/// Component name → type mapping for stable reconstruction
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub component_names: HashMap<String, String>,
/// Component name → circuit ID mapping
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub circuit_assignments: HashMap<String, u16>,
/// Constraints for inverse control
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<ConstraintSnapshot>,
/// Bounded control variables for inverse control
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bounded_variables: Vec<BoundedVariableSnapshot>,
/// Optional metadata
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
/// 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<EdgeSnapshot>,
/// Thermal couplings between circuits
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub thermal_couplings: Vec<ThermalCoupling>,
}
/// 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<String>,
}
/// 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());
}
}