chore: sync project state and current artifacts
This commit is contained in:
@@ -10,6 +10,7 @@ description = "Core types and primitives for Entropyk thermodynamic simulation l
|
||||
[dependencies]
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
seahash = "4.1"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5"
|
||||
|
||||
@@ -77,7 +77,6 @@ pub struct CalibIndices {
|
||||
pub f_etav: Option<usize>,
|
||||
}
|
||||
|
||||
|
||||
/// Error returned when a calibration factor is outside the allowed range [0.5, 2.0].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CalibValidationError {
|
||||
|
||||
@@ -38,13 +38,17 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod calib;
|
||||
pub mod state;
|
||||
pub mod types;
|
||||
|
||||
// Re-export all physical types for convenience
|
||||
pub use types::{
|
||||
Enthalpy, MassFlow, MIN_MASS_FLOW_REGULARIZATION_KG_S, Power, Pressure, Temperature,
|
||||
ThermalConductance,
|
||||
CircuitId, Enthalpy, Entropy, MassFlow, Power, Pressure, Temperature, ThermalConductance,
|
||||
MIN_MASS_FLOW_REGULARIZATION_KG_S,
|
||||
};
|
||||
|
||||
// Re-export calibration types
|
||||
pub use calib::{Calib, CalibIndices, CalibValidationError};
|
||||
|
||||
// Re-export system state
|
||||
pub use state::SystemState;
|
||||
|
||||
655
crates/core/src/state.rs
Normal file
655
crates/core/src/state.rs
Normal file
@@ -0,0 +1,655 @@
|
||||
//! System state container for thermodynamic simulations.
|
||||
//!
|
||||
//! This module provides [`SystemState`], a type-safe container for the thermodynamic
|
||||
//! state variables of a system during simulation. Each edge in the system graph
|
||||
//! has two state variables: pressure and enthalpy.
|
||||
|
||||
use crate::{Enthalpy, Pressure};
|
||||
use std::ops::{Deref, DerefMut, Index, IndexMut};
|
||||
|
||||
/// Represents the thermodynamic state of the entire system.
|
||||
///
|
||||
/// The internal layout is `[P_edge0, h_edge0, P_edge1, h_edge1, ...]` where:
|
||||
/// - `P`: Pressure in Pascals (Pa)
|
||||
/// - `h`: Specific enthalpy in Joules per kilogram (J/kg)
|
||||
///
|
||||
/// Each edge in the system graph corresponds to a connection between two ports
|
||||
/// and carries exactly two state variables.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::{SystemState, Pressure, Enthalpy};
|
||||
///
|
||||
/// // Create a state for a system with 3 edges
|
||||
/// let mut state = SystemState::new(3);
|
||||
/// assert_eq!(state.edge_count(), 3);
|
||||
///
|
||||
/// // Set values for edge 0
|
||||
/// state.set_pressure(0, Pressure::from_bar(2.0));
|
||||
/// state.set_enthalpy(0, Enthalpy::from_kilojoules_per_kg(400.0));
|
||||
///
|
||||
/// // Retrieve values
|
||||
/// let p = state.pressure(0).unwrap();
|
||||
/// let h = state.enthalpy(0).unwrap();
|
||||
/// assert_eq!(p.to_bar(), 2.0);
|
||||
/// assert_eq!(h.to_kilojoules_per_kg(), 400.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SystemState {
|
||||
data: Vec<f64>,
|
||||
edge_count: usize,
|
||||
}
|
||||
|
||||
impl SystemState {
|
||||
/// Creates a new `SystemState` with all values initialized to zero.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `edge_count` - Number of edges in the system. Total storage is `2 * edge_count`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::SystemState;
|
||||
///
|
||||
/// let state = SystemState::new(5);
|
||||
/// assert_eq!(state.edge_count(), 5);
|
||||
/// assert_eq!(state.as_slice().len(), 10); // 2 values per edge
|
||||
/// ```
|
||||
pub fn new(edge_count: usize) -> Self {
|
||||
Self {
|
||||
data: vec![0.0; edge_count * 2],
|
||||
edge_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `SystemState` from a raw vector of values.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `data` - Raw vector with layout `[P0, h0, P1, h1, ...]`
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `data.len()` is not even (each edge needs exactly 2 values).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::SystemState;
|
||||
///
|
||||
/// let data = vec![100000.0, 400000.0, 200000.0, 250000.0];
|
||||
/// let state = SystemState::from_vec(data);
|
||||
/// assert_eq!(state.edge_count(), 2);
|
||||
/// ```
|
||||
pub fn from_vec(data: Vec<f64>) -> Self {
|
||||
assert!(
|
||||
data.len() % 2 == 0,
|
||||
"Data length must be even (P, h pairs), got {}",
|
||||
data.len()
|
||||
);
|
||||
let edge_count = data.len() / 2;
|
||||
Self { data, edge_count }
|
||||
}
|
||||
|
||||
/// Returns the number of edges in the system.
|
||||
pub fn edge_count(&self) -> usize {
|
||||
self.edge_count
|
||||
}
|
||||
|
||||
/// Returns the pressure at the specified edge.
|
||||
///
|
||||
/// Returns `None` if `edge_idx` is out of bounds.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::{SystemState, Pressure};
|
||||
///
|
||||
/// let mut state = SystemState::new(2);
|
||||
/// state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
///
|
||||
/// let p = state.pressure(0).unwrap();
|
||||
/// assert_eq!(p.to_pascals(), 100000.0);
|
||||
///
|
||||
/// // Out of bounds returns None
|
||||
/// assert!(state.pressure(5).is_none());
|
||||
/// ```
|
||||
pub fn pressure(&self, edge_idx: usize) -> Option<Pressure> {
|
||||
self.data
|
||||
.get(edge_idx * 2)
|
||||
.map(|&p| Pressure::from_pascals(p))
|
||||
}
|
||||
|
||||
/// Returns the enthalpy at the specified edge.
|
||||
///
|
||||
/// Returns `None` if `edge_idx` is out of bounds.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::{SystemState, Enthalpy};
|
||||
///
|
||||
/// let mut state = SystemState::new(2);
|
||||
/// state.set_enthalpy(1, Enthalpy::from_joules_per_kg(300000.0));
|
||||
///
|
||||
/// let h = state.enthalpy(1).unwrap();
|
||||
/// assert_eq!(h.to_joules_per_kg(), 300000.0);
|
||||
/// ```
|
||||
pub fn enthalpy(&self, edge_idx: usize) -> Option<Enthalpy> {
|
||||
self.data
|
||||
.get(edge_idx * 2 + 1)
|
||||
.map(|&h| Enthalpy::from_joules_per_kg(h))
|
||||
}
|
||||
|
||||
/// Sets the pressure at the specified edge.
|
||||
///
|
||||
/// Does nothing if `edge_idx` is out of bounds.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::{SystemState, Pressure};
|
||||
///
|
||||
/// let mut state = SystemState::new(2);
|
||||
/// state.set_pressure(0, Pressure::from_bar(1.5));
|
||||
///
|
||||
/// assert_eq!(state.pressure(0).unwrap().to_bar(), 1.5);
|
||||
/// ```
|
||||
pub fn set_pressure(&mut self, edge_idx: usize, p: Pressure) {
|
||||
if let Some(slot) = self.data.get_mut(edge_idx * 2) {
|
||||
*slot = p.to_pascals();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the enthalpy at the specified edge.
|
||||
///
|
||||
/// Does nothing if `edge_idx` is out of bounds.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::{SystemState, Enthalpy};
|
||||
///
|
||||
/// let mut state = SystemState::new(2);
|
||||
/// state.set_enthalpy(0, Enthalpy::from_kilojoules_per_kg(250.0));
|
||||
///
|
||||
/// assert_eq!(state.enthalpy(0).unwrap().to_kilojoules_per_kg(), 250.0);
|
||||
/// ```
|
||||
pub fn set_enthalpy(&mut self, edge_idx: usize, h: Enthalpy) {
|
||||
if let Some(slot) = self.data.get_mut(edge_idx * 2 + 1) {
|
||||
*slot = h.to_joules_per_kg();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a slice of the raw data.
|
||||
///
|
||||
/// Layout: `[P0, h0, P1, h1, ...]`
|
||||
pub fn as_slice(&self) -> &[f64] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the raw data.
|
||||
///
|
||||
/// Layout: `[P0, h0, P1, h1, ...]`
|
||||
pub fn as_mut_slice(&mut self) -> &mut [f64] {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
/// Consumes the `SystemState` and returns the underlying vector.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::SystemState;
|
||||
///
|
||||
/// let state = SystemState::new(2);
|
||||
/// let data = state.into_vec();
|
||||
/// assert_eq!(data.len(), 4);
|
||||
/// ```
|
||||
pub fn into_vec(self) -> Vec<f64> {
|
||||
self.data
|
||||
}
|
||||
|
||||
/// Returns a cloned copy of the underlying vector.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::SystemState;
|
||||
///
|
||||
/// let mut state = SystemState::new(2);
|
||||
/// state.set_pressure(0, entropyk_core::Pressure::from_pascals(100000.0));
|
||||
/// let data = state.to_vec();
|
||||
/// assert_eq!(data.len(), 4);
|
||||
/// assert_eq!(data[0], 100000.0);
|
||||
/// ```
|
||||
pub fn to_vec(&self) -> Vec<f64> {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
/// Iterates over all edges, yielding `(Pressure, Enthalpy)` pairs.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::{SystemState, Pressure, Enthalpy};
|
||||
///
|
||||
/// let mut state = SystemState::new(2);
|
||||
/// state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
/// state.set_enthalpy(0, Enthalpy::from_joules_per_kg(300000.0));
|
||||
/// state.set_pressure(1, Pressure::from_pascals(200000.0));
|
||||
/// state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));
|
||||
///
|
||||
/// let edges: Vec<_> = state.iter_edges().collect();
|
||||
/// assert_eq!(edges.len(), 2);
|
||||
/// assert_eq!(edges[0].0.to_pascals(), 100000.0);
|
||||
/// assert_eq!(edges[1].0.to_pascals(), 200000.0);
|
||||
/// ```
|
||||
pub fn iter_edges(&self) -> impl Iterator<Item = (Pressure, Enthalpy)> + '_ {
|
||||
self.data.chunks_exact(2).map(|chunk| {
|
||||
(
|
||||
Pressure::from_pascals(chunk[0]),
|
||||
Enthalpy::from_joules_per_kg(chunk[1]),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the total number of state variables (2 per edge).
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the state contains no edges.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.edge_count == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SystemState {
|
||||
fn default() -> Self {
|
||||
Self::new(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[f64]> for SystemState {
|
||||
fn as_ref(&self) -> &[f64] {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[f64]> for SystemState {
|
||||
fn as_mut(&mut self) -> &mut [f64] {
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<f64>> for SystemState {
|
||||
fn from(data: Vec<f64>) -> Self {
|
||||
Self::from_vec(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemState> for Vec<f64> {
|
||||
fn from(state: SystemState) -> Self {
|
||||
state.into_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for SystemState {
|
||||
type Output = f64;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.data[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<usize> for SystemState {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
&mut self.data[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SystemState {
|
||||
type Target = [f64];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SystemState {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let state = SystemState::new(3);
|
||||
assert_eq!(state.edge_count(), 3);
|
||||
assert_eq!(state.as_slice().len(), 6);
|
||||
assert!(!state.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_zero_edges() {
|
||||
let state = SystemState::new(0);
|
||||
assert_eq!(state.edge_count(), 0);
|
||||
assert_eq!(state.as_slice().len(), 0);
|
||||
assert!(state.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let state = SystemState::default();
|
||||
assert_eq!(state.edge_count(), 0);
|
||||
assert!(state.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pressure_access() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(101325.0));
|
||||
state.set_pressure(1, Pressure::from_pascals(200000.0));
|
||||
|
||||
assert_relative_eq!(
|
||||
state.pressure(0).unwrap().to_pascals(),
|
||||
101325.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.pressure(1).unwrap().to_pascals(),
|
||||
200000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enthalpy_access() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(400000.0));
|
||||
state.set_enthalpy(1, Enthalpy::from_joules_per_kg(250000.0));
|
||||
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(0).unwrap().to_joules_per_kg(),
|
||||
400000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(1).unwrap().to_joules_per_kg(),
|
||||
250000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_bounds_pressure() {
|
||||
let state = SystemState::new(2);
|
||||
assert!(state.pressure(2).is_none());
|
||||
assert!(state.pressure(100).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_bounds_enthalpy() {
|
||||
let state = SystemState::new(2);
|
||||
assert!(state.enthalpy(2).is_none());
|
||||
assert!(state.enthalpy(100).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_out_of_bounds_silent() {
|
||||
let mut state = SystemState::new(2);
|
||||
// These should silently do nothing
|
||||
state.set_pressure(10, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(10, Enthalpy::from_joules_per_kg(300000.0));
|
||||
|
||||
// Verify nothing was set
|
||||
assert!(state.pressure(10).is_none());
|
||||
assert!(state.enthalpy(10).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec_valid() {
|
||||
let data = vec![101325.0, 400000.0, 200000.0, 250000.0];
|
||||
let state = SystemState::from_vec(data);
|
||||
|
||||
assert_eq!(state.edge_count(), 2);
|
||||
assert_relative_eq!(
|
||||
state.pressure(0).unwrap().to_pascals(),
|
||||
101325.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(0).unwrap().to_joules_per_kg(),
|
||||
400000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.pressure(1).unwrap().to_pascals(),
|
||||
200000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(1).unwrap().to_joules_per_kg(),
|
||||
250000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Data length must be even")]
|
||||
fn test_from_vec_odd_length() {
|
||||
let data = vec![1.0, 2.0, 3.0]; // 3 elements = odd
|
||||
let _ = SystemState::from_vec(data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec_empty() {
|
||||
let data: Vec<f64> = vec![];
|
||||
let state = SystemState::from_vec(data);
|
||||
assert_eq!(state.edge_count(), 0);
|
||||
assert!(state.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_edges() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(300000.0));
|
||||
state.set_pressure(1, Pressure::from_pascals(200000.0));
|
||||
state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));
|
||||
|
||||
let edges: Vec<_> = state.iter_edges().collect();
|
||||
assert_eq!(edges.len(), 2);
|
||||
assert_relative_eq!(edges[0].0.to_pascals(), 100000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(edges[0].1.to_joules_per_kg(), 300000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(edges[1].0.to_pascals(), 200000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(edges[1].1.to_joules_per_kg(), 400000.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_edges_empty() {
|
||||
let state = SystemState::new(0);
|
||||
let edges: Vec<_> = state.iter_edges().collect();
|
||||
assert!(edges.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_slice() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
state.set_pressure(1, Pressure::from_pascals(300000.0));
|
||||
state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));
|
||||
|
||||
let slice = state.as_slice();
|
||||
assert_eq!(slice.len(), 4);
|
||||
assert_relative_eq!(slice[0], 100000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(slice[1], 200000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(slice[2], 300000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(slice[3], 400000.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_mut_slice() {
|
||||
let mut state = SystemState::new(2);
|
||||
let slice = state.as_mut_slice();
|
||||
slice[0] = 100000.0;
|
||||
slice[1] = 200000.0;
|
||||
slice[2] = 300000.0;
|
||||
slice[3] = 400000.0;
|
||||
|
||||
assert_relative_eq!(
|
||||
state.pressure(0).unwrap().to_pascals(),
|
||||
100000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(0).unwrap().to_joules_per_kg(),
|
||||
200000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_vec() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
|
||||
let data = state.into_vec();
|
||||
assert_eq!(data.len(), 4);
|
||||
assert_relative_eq!(data[0], 100000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(data[1], 200000.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec_conversion() {
|
||||
let data = vec![100000.0, 200000.0, 300000.0, 400000.0];
|
||||
let state: SystemState = data.into();
|
||||
assert_eq!(state.edge_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_vec_conversion() {
|
||||
let mut state = SystemState::new(1);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
|
||||
let data: Vec<f64> = state.into();
|
||||
assert_eq!(data, vec![100000.0, 200000.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_ref_trait() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
|
||||
let state_ref: &[f64] = state.as_ref();
|
||||
assert_relative_eq!(state_ref[0], 100000.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_mut_trait() {
|
||||
let mut state = SystemState::new(2);
|
||||
let state_mut: &mut [f64] = state.as_mut();
|
||||
state_mut[0] = 500000.0;
|
||||
|
||||
assert_relative_eq!(
|
||||
state.pressure(0).unwrap().to_pascals(),
|
||||
500000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
|
||||
let cloned = state.clone();
|
||||
assert_eq!(state.edge_count(), cloned.edge_count());
|
||||
assert_relative_eq!(
|
||||
cloned.pressure(0).unwrap().to_pascals(),
|
||||
100000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eq() {
|
||||
let mut state1 = SystemState::new(2);
|
||||
state1.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state1.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
|
||||
let mut state2 = SystemState::new(2);
|
||||
state2.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state2.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
|
||||
assert_eq!(state1, state2);
|
||||
|
||||
state2.set_pressure(1, Pressure::from_pascals(1.0));
|
||||
assert_ne!(state1, state2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
let state = SystemState::new(5);
|
||||
assert_eq!(state.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_trait() {
|
||||
let mut state = SystemState::new(2);
|
||||
state.set_pressure(0, Pressure::from_pascals(100000.0));
|
||||
state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
|
||||
state.set_pressure(1, Pressure::from_pascals(300000.0));
|
||||
state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));
|
||||
|
||||
// Test Index trait
|
||||
assert_relative_eq!(state[0], 100000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(state[1], 200000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(state[2], 300000.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(state[3], 400000.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_mut_trait() {
|
||||
let mut state = SystemState::new(2);
|
||||
|
||||
// Test IndexMut trait
|
||||
state[0] = 100000.0;
|
||||
state[1] = 200000.0;
|
||||
state[2] = 300000.0;
|
||||
state[3] = 400000.0;
|
||||
|
||||
assert_relative_eq!(
|
||||
state.pressure(0).unwrap().to_pascals(),
|
||||
100000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(0).unwrap().to_joules_per_kg(),
|
||||
200000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.pressure(1).unwrap().to_pascals(),
|
||||
300000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
state.enthalpy(1).unwrap().to_joules_per_kg(),
|
||||
400000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -516,6 +516,74 @@ impl Div<f64> for Power {
|
||||
}
|
||||
}
|
||||
|
||||
/// Entropy in J/(kg·K).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub struct Entropy(pub f64);
|
||||
|
||||
impl Entropy {
|
||||
/// Creates entropy from J/(kg·K).
|
||||
pub fn from_joules_per_kg_kelvin(value: f64) -> Self {
|
||||
Entropy(value)
|
||||
}
|
||||
|
||||
/// Returns entropy in J/(kg·K).
|
||||
pub fn to_joules_per_kg_kelvin(&self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Entropy {
|
||||
fn from(value: f64) -> Self {
|
||||
Entropy(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Entropy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} J/(kg·K)", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Entropy> for Entropy {
|
||||
type Output = Entropy;
|
||||
|
||||
fn add(self, other: Entropy) -> Entropy {
|
||||
Entropy(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Entropy> for Entropy {
|
||||
type Output = Entropy;
|
||||
|
||||
fn sub(self, other: Entropy) -> Entropy {
|
||||
Entropy(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f64> for Entropy {
|
||||
type Output = Entropy;
|
||||
|
||||
fn mul(self, scalar: f64) -> Entropy {
|
||||
Entropy(self.0 * scalar)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Entropy> for f64 {
|
||||
type Output = Entropy;
|
||||
|
||||
fn mul(self, s: Entropy) -> Entropy {
|
||||
Entropy(self * s.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<f64> for Entropy {
|
||||
type Output = Entropy;
|
||||
|
||||
fn div(self, scalar: f64) -> Entropy {
|
||||
Entropy(self.0 / scalar)
|
||||
}
|
||||
}
|
||||
|
||||
/// Thermal conductance in Watts per Kelvin (W/K).
|
||||
///
|
||||
/// Represents the heat transfer coefficient (UA value) for thermal coupling
|
||||
@@ -557,6 +625,79 @@ impl From<f64> for ThermalConductance {
|
||||
}
|
||||
}
|
||||
|
||||
/// Circuit identifier for multi-circuit thermodynamic systems.
|
||||
///
|
||||
/// Represents a unique identifier for a circuit within a multi-circuit machine.
|
||||
/// Uses a compact `u8` representation for performance, allowing up to 256 circuits.
|
||||
///
|
||||
/// # Creation
|
||||
///
|
||||
/// - From number: `CircuitId::from_number(5)` or `CircuitId::from(5u8)`
|
||||
/// - From string: `CircuitId::from("primary")` (uses hash-based conversion)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_core::CircuitId;
|
||||
///
|
||||
/// let id = CircuitId::from_number(3);
|
||||
/// assert_eq!(id.as_number(), 3);
|
||||
///
|
||||
/// let from_str: CircuitId = "primary".into();
|
||||
/// let same: CircuitId = "primary".into();
|
||||
/// assert_eq!(from_str, same); // Deterministic hashing
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
|
||||
pub struct CircuitId(pub u16);
|
||||
|
||||
impl CircuitId {
|
||||
/// Maximum possible circuit identifier.
|
||||
pub const MAX: u16 = 65535;
|
||||
/// Primary circuit identifier.
|
||||
pub const ZERO: CircuitId = CircuitId(0);
|
||||
|
||||
/// Creates a new circuit identifier from a raw number.
|
||||
pub fn from_number(n: u16) -> Self {
|
||||
Self(n)
|
||||
}
|
||||
|
||||
/// Returns the raw numeric representation.
|
||||
pub fn as_number(&self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for CircuitId {
|
||||
fn from(n: u8) -> Self {
|
||||
Self(n as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for CircuitId {
|
||||
fn from(n: u16) -> Self {
|
||||
Self(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for CircuitId {
|
||||
fn from(s: &str) -> Self {
|
||||
let hash = seahash::hash(s.as_bytes());
|
||||
Self(hash as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CircuitId {
|
||||
fn from(s: String) -> Self {
|
||||
Self::from(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CircuitId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Circuit-{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -826,10 +967,18 @@ mod tests {
|
||||
use super::MIN_MASS_FLOW_REGULARIZATION_KG_S;
|
||||
let zero = MassFlow::from_kg_per_s(0.0);
|
||||
let r = zero.regularized();
|
||||
assert_relative_eq!(r.to_kg_per_s(), MIN_MASS_FLOW_REGULARIZATION_KG_S, epsilon = 1e-15);
|
||||
assert_relative_eq!(
|
||||
r.to_kg_per_s(),
|
||||
MIN_MASS_FLOW_REGULARIZATION_KG_S,
|
||||
epsilon = 1e-15
|
||||
);
|
||||
let small = MassFlow::from_kg_per_s(1e-14);
|
||||
let r2 = small.regularized();
|
||||
assert_relative_eq!(r2.to_kg_per_s(), MIN_MASS_FLOW_REGULARIZATION_KG_S, epsilon = 1e-15);
|
||||
assert_relative_eq!(
|
||||
r2.to_kg_per_s(),
|
||||
MIN_MASS_FLOW_REGULARIZATION_KG_S,
|
||||
epsilon = 1e-15
|
||||
);
|
||||
let normal = MassFlow::from_kg_per_s(0.5);
|
||||
let r3 = normal.regularized();
|
||||
assert_relative_eq!(r3.to_kg_per_s(), 0.5, epsilon = 1e-10);
|
||||
@@ -976,4 +1125,92 @@ mod tests {
|
||||
let p7 = 2.0 * p1;
|
||||
assert_relative_eq!(p7.to_watts(), 2000.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
// ==================== CIRCUIT ID TESTS ====================
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_from_number() {
|
||||
let id = CircuitId::from_number(5);
|
||||
assert_eq!(id.as_number(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_from_u16() {
|
||||
let id: CircuitId = 42u16.into();
|
||||
assert_eq!(id.0, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_from_u8() {
|
||||
let id: CircuitId = 42u8.into();
|
||||
assert_eq!(id.0, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_from_str_deterministic() {
|
||||
let id1: CircuitId = "primary".into();
|
||||
let id2: CircuitId = "primary".into();
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_from_string() {
|
||||
let id = CircuitId::from("secondary".to_string());
|
||||
let id2: CircuitId = "secondary".into();
|
||||
assert_eq!(id, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_display() {
|
||||
let id = CircuitId(3);
|
||||
assert_eq!(format!("{}", id), "Circuit-3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_default() {
|
||||
let id = CircuitId::default();
|
||||
assert_eq!(id, CircuitId::ZERO);
|
||||
assert_eq!(id.0, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_zero_constant() {
|
||||
assert_eq!(CircuitId::ZERO.0, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_ordering() {
|
||||
let id1 = CircuitId(1);
|
||||
let id2 = CircuitId(2);
|
||||
assert!(id1 < id2);
|
||||
assert!(id2 > id1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_equality() {
|
||||
let id1 = CircuitId(5);
|
||||
let id2 = CircuitId(5);
|
||||
let id3 = CircuitId(6);
|
||||
assert_eq!(id1, id2);
|
||||
assert_ne!(id1, id3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_copy() {
|
||||
let id1 = CircuitId(10);
|
||||
let id2 = id1;
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circuit_id_hash_consistency() {
|
||||
use std::collections::HashSet;
|
||||
let mut set = HashSet::new();
|
||||
let id1: CircuitId = "circuit_a".into();
|
||||
let id2: CircuitId = "circuit_a".into();
|
||||
let id3: CircuitId = "circuit_b".into();
|
||||
set.insert(id1);
|
||||
assert!(set.contains(&id2));
|
||||
assert!(!set.contains(&id3));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user