227 lines
7.6 KiB
Rust
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());
|
|
}
|
|
}
|