chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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
View 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
);
}
}

View File

@@ -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));
}
}