From be70a7a6c7abb90083b6bc8e6df389029ea8fc0d Mon Sep 17 00:00:00 2001 From: Sepehr Date: Sat, 14 Feb 2026 15:25:30 +0100 Subject: [PATCH] feat(core): implement physical types with NewType pattern Story 1.2: Physical Types (NewType Pattern) - Add Pressure, Temperature, Enthalpy, MassFlow types - Implement SI base units with conversion methods - Add arithmetic operations (Add, Sub, Mul, Div) - Add Display and Debug traits - Comprehensive unit tests (37 tests) - Add PSI and Fahrenheit conversions - Code review fixes applied All tests passing, clippy clean --- Cargo.toml | 2 +- .../sprint-status.yaml | 2 +- crates/core/Cargo.toml | 15 + crates/core/src/lib.rs | 43 + crates/core/src/types.rs | 751 ++++++++++++++++++ 5 files changed, 811 insertions(+), 2 deletions(-) create mode 100644 crates/core/Cargo.toml create mode 100644 crates/core/src/lib.rs create mode 100644 crates/core/src/types.rs diff --git a/Cargo.toml b/Cargo.toml index 370a843..029c85d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = [ "crates/components", - # "crates/core", # Will be added in future stories + "crates/core", # "crates/solver", # Will be added in future stories ] resolver = "2" diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index e76753b..eb258c1 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -44,7 +44,7 @@ development_status: # Epic 1: Extensible Component Framework epic-1: in-progress 1-1-component-trait-definition: done - 1-2-physical-types-newtype-pattern: backlog + 1-2-physical-types-newtype-pattern: done 1-3-port-and-connection-system: backlog 1-4-compressor-component-ahri-540: backlog 1-5-generic-heat-exchanger-framework: backlog diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 0000000..a299244 --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "entropyk-core" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Core types and primitives for Entropyk thermodynamic simulation library" + +[dependencies] +thiserror.workspace = true +serde.workspace = true + +[dev-dependencies] +approx = "0.5" diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 0000000..26f233a --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,43 @@ +//! # Entropyk Core +//! +//! Core types and primitives for the Entropyk thermodynamic simulation library. +//! +//! This crate provides the foundation types used throughout the Entropyk ecosystem, +//! including type-safe physical quantities via the NewType pattern. +//! +//! ## Physical Types +//! +//! All physical quantities use the NewType pattern to provide compile-time unit safety: +//! +//! - [`Pressure`] - Pressure in Pascals (Pa) +//! - [`Temperature`] - Temperature in Kelvin (K) +//! - [`Enthalpy`] - Specific enthalpy in Joules per kilogram (J/kg) +//! - [`MassFlow`] - Mass flow rate in kilograms per second (kg/s) +//! +//! ## Example +//! +//! ```rust +//! use entropyk_core::{Pressure, Temperature, Enthalpy, MassFlow}; +//! +//! // Create values using constructors +//! let pressure = Pressure::from_bar(1.0); +//! let temperature = Temperature::from_celsius(25.0); +//! +//! // Convert to base units +//! assert_eq!(pressure.to_pascals(), 100_000.0); +//! assert_eq!(temperature.to_kelvin(), 298.15); +//! +//! // Arithmetic operations +//! let p1 = Pressure::from_pascals(100_000.0); +//! let p2 = Pressure::from_pascals(50_000.0); +//! let p3 = p1 + p2; +//! assert_eq!(p3.to_pascals(), 150_000.0); +//! ``` + +#![deny(warnings)] +#![warn(missing_docs)] + +pub mod types; + +// Re-export all physical types for convenience +pub use types::{Enthalpy, MassFlow, Pressure, Temperature}; diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs new file mode 100644 index 0000000..6603178 --- /dev/null +++ b/crates/core/src/types.rs @@ -0,0 +1,751 @@ +//! Physical types using the NewType pattern for compile-time unit safety. +//! +//! This module provides type-safe wrappers around `f64` for physical quantities, +//! preventing accidental mixing of units at compile time. +//! +//! All types store values in SI base units internally: +//! - Pressure: Pascals (Pa) +//! - Temperature: Kelvin (K) +//! - Enthalpy: Joules per kilogram (J/kg) +//! - MassFlow: Kilograms per second (kg/s) + +use std::fmt; +use std::ops::{Add, Div, Mul, Sub}; + +/// Pressure in Pascals (Pa). +/// +/// Internally stores the value in Pascals (SI base unit). +/// Provides conversions to/from common units like bar. +/// +/// # Example +/// +/// ``` +/// use entropyk_core::Pressure; +/// +/// let p = Pressure::from_bar(1.0); +/// assert_eq!(p.to_pascals(), 100_000.0); +/// assert_eq!(p.to_bar(), 1.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Pressure(pub f64); + +impl Pressure { + /// Creates a Pressure from a value in Pascals. + pub fn from_pascals(value: f64) -> Self { + Pressure(value) + } + + /// Creates a Pressure from a value in bar. + pub fn from_bar(value: f64) -> Self { + Pressure(value * 100_000.0) + } + + /// Creates a Pressure from a value in PSI (pounds per square inch). + pub fn from_psi(value: f64) -> Self { + Pressure(value * 6894.75729) + } + + /// Returns the pressure in Pascals. + pub fn to_pascals(&self) -> f64 { + self.0 + } + + /// Returns the pressure in bar. + pub fn to_bar(&self) -> f64 { + self.0 / 100_000.0 + } + + /// Returns the pressure in PSI (pounds per square inch). + pub fn to_psi(&self) -> f64 { + self.0 / 6894.75729 + } +} + +impl fmt::Display for Pressure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} Pa", self.0) + } +} + +impl From for Pressure { + fn from(value: f64) -> Self { + Pressure(value) + } +} + +impl Add for Pressure { + type Output = Pressure; + + fn add(self, other: Pressure) -> Pressure { + Pressure(self.0 + other.0) + } +} + +impl Sub for Pressure { + type Output = Pressure; + + fn sub(self, other: Pressure) -> Pressure { + Pressure(self.0 - other.0) + } +} + +impl Mul for Pressure { + type Output = Pressure; + + fn mul(self, scalar: f64) -> Pressure { + Pressure(self.0 * scalar) + } +} + +impl Mul for f64 { + type Output = Pressure; + + fn mul(self, p: Pressure) -> Pressure { + Pressure(self * p.0) + } +} + +impl Div for Pressure { + type Output = Pressure; + + fn div(self, scalar: f64) -> Pressure { + Pressure(self.0 / scalar) + } +} + +/// Temperature in Kelvin (K). +/// +/// Internally stores the value in Kelvin (SI base unit). +/// Provides conversions to/from Celsius. +/// +/// # Example +/// +/// ``` +/// use entropyk_core::Temperature; +/// +/// let t = Temperature::from_celsius(0.0); +/// assert_eq!(t.to_kelvin(), 273.15); +/// assert_eq!(t.to_celsius(), 0.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Temperature(pub f64); + +impl Temperature { + /// Creates a Temperature from a value in Kelvin. + pub fn from_kelvin(value: f64) -> Self { + Temperature(value) + } + + /// Creates a Temperature from a value in Celsius. + pub fn from_celsius(value: f64) -> Self { + Temperature(value + 273.15) + } + + /// Creates a Temperature from a value in Fahrenheit. + pub fn from_fahrenheit(value: f64) -> Self { + Temperature((value - 32.0) * 5.0 / 9.0 + 273.15) + } + + /// Returns the temperature in Kelvin. + pub fn to_kelvin(&self) -> f64 { + self.0 + } + + /// Returns the temperature in Celsius. + pub fn to_celsius(&self) -> f64 { + self.0 - 273.15 + } + + /// Returns the temperature in Fahrenheit. + pub fn to_fahrenheit(&self) -> f64 { + (self.0 - 273.15) * 9.0 / 5.0 + 32.0 + } +} + +impl fmt::Display for Temperature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} K", self.0) + } +} + +impl From for Temperature { + fn from(value: f64) -> Self { + Temperature(value) + } +} + +impl Add for Temperature { + type Output = Temperature; + + fn add(self, other: Temperature) -> Temperature { + Temperature(self.0 + other.0) + } +} + +impl Sub for Temperature { + type Output = Temperature; + + fn sub(self, other: Temperature) -> Temperature { + Temperature(self.0 - other.0) + } +} + +impl Mul for Temperature { + type Output = Temperature; + + fn mul(self, scalar: f64) -> Temperature { + Temperature(self.0 * scalar) + } +} + +impl Mul for f64 { + type Output = Temperature; + + fn mul(self, t: Temperature) -> Temperature { + Temperature(self * t.0) + } +} + +impl Div for Temperature { + type Output = Temperature; + + fn div(self, scalar: f64) -> Temperature { + Temperature(self.0 / scalar) + } +} + +/// Specific enthalpy in Joules per kilogram (J/kg). +/// +/// Internally stores the value in Joules per kilogram (SI base unit). +/// +/// # Example +/// +/// ``` +/// use entropyk_core::Enthalpy; +/// +/// let h = Enthalpy::from_joules_per_kg(1000.0); +/// assert_eq!(h.to_joules_per_kg(), 1000.0); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Enthalpy(pub f64); + +impl Enthalpy { + /// Creates an Enthalpy from a value in Joules per kilogram. + pub fn from_joules_per_kg(value: f64) -> Self { + Enthalpy(value) + } + + /// Creates an Enthalpy from a value in kilojoules per kilogram. + pub fn from_kilojoules_per_kg(value: f64) -> Self { + Enthalpy(value * 1_000.0) + } + + /// Returns the enthalpy in Joules per kilogram. + pub fn to_joules_per_kg(&self) -> f64 { + self.0 + } + + /// Returns the enthalpy in kilojoules per kilogram. + pub fn to_kilojoules_per_kg(&self) -> f64 { + self.0 / 1_000.0 + } +} + +impl fmt::Display for Enthalpy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} J/kg", self.0) + } +} + +impl From for Enthalpy { + fn from(value: f64) -> Self { + Enthalpy(value) + } +} + +impl Add for Enthalpy { + type Output = Enthalpy; + + fn add(self, other: Enthalpy) -> Enthalpy { + Enthalpy(self.0 + other.0) + } +} + +impl Sub for Enthalpy { + type Output = Enthalpy; + + fn sub(self, other: Enthalpy) -> Enthalpy { + Enthalpy(self.0 - other.0) + } +} + +impl Mul for Enthalpy { + type Output = Enthalpy; + + fn mul(self, scalar: f64) -> Enthalpy { + Enthalpy(self.0 * scalar) + } +} + +impl Mul for f64 { + type Output = Enthalpy; + + fn mul(self, h: Enthalpy) -> Enthalpy { + Enthalpy(self * h.0) + } +} + +impl Div for Enthalpy { + type Output = Enthalpy; + + fn div(self, scalar: f64) -> Enthalpy { + Enthalpy(self.0 / scalar) + } +} + +/// Mass flow rate in kilograms per second (kg/s). +/// +/// Internally stores the value in kilograms per second (SI base unit). +/// +/// # Example +/// +/// ``` +/// use entropyk_core::MassFlow; +/// +/// let m = MassFlow::from_kg_per_s(0.5); +/// assert_eq!(m.to_kg_per_s(), 0.5); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct MassFlow(pub f64); + +impl MassFlow { + /// Creates a MassFlow from a value in kilograms per second. + pub fn from_kg_per_s(value: f64) -> Self { + MassFlow(value) + } + + /// Creates a MassFlow from a value in grams per second. + pub fn from_grams_per_s(value: f64) -> Self { + MassFlow(value / 1_000.0) + } + + /// Returns the mass flow rate in kilograms per second. + pub fn to_kg_per_s(&self) -> f64 { + self.0 + } + + /// Returns the mass flow rate in grams per second. + pub fn to_grams_per_s(&self) -> f64 { + self.0 * 1_000.0 + } +} + +impl fmt::Display for MassFlow { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} kg/s", self.0) + } +} + +impl From for MassFlow { + fn from(value: f64) -> Self { + MassFlow(value) + } +} + +impl Add for MassFlow { + type Output = MassFlow; + + fn add(self, other: MassFlow) -> MassFlow { + MassFlow(self.0 + other.0) + } +} + +impl Sub for MassFlow { + type Output = MassFlow; + + fn sub(self, other: MassFlow) -> MassFlow { + MassFlow(self.0 - other.0) + } +} + +impl Mul for MassFlow { + type Output = MassFlow; + + fn mul(self, scalar: f64) -> MassFlow { + MassFlow(self.0 * scalar) + } +} + +impl Mul for f64 { + type Output = MassFlow; + + fn mul(self, m: MassFlow) -> MassFlow { + MassFlow(self * m.0) + } +} + +impl Div for MassFlow { + type Output = MassFlow; + + fn div(self, scalar: f64) -> MassFlow { + MassFlow(self.0 / scalar) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_relative_eq; + + // ==================== PRESSURE TESTS ==================== + + #[test] + fn test_pressure_from_pascals() { + let p = Pressure::from_pascals(101325.0); + assert_relative_eq!(p.0, 101325.0, epsilon = 1e-10); + assert_relative_eq!(p.to_pascals(), 101325.0, epsilon = 1e-10); + } + + #[test] + fn test_pressure_from_bar() { + let p = Pressure::from_bar(1.0); + assert_relative_eq!(p.to_pascals(), 100_000.0, epsilon = 1e-6); + } + + #[test] + fn test_pressure_to_bar() { + let p = Pressure::from_pascals(100_000.0); + assert_relative_eq!(p.to_bar(), 1.0, epsilon = 1e-6); + } + + #[test] + fn test_pressure_round_trip() { + let p1 = Pressure::from_bar(2.5); + let bar = p1.to_bar(); + assert_relative_eq!(bar, 2.5, epsilon = 1e-6); + } + + #[test] + fn test_pressure_display() { + let p = Pressure::from_pascals(101325.0); + assert_eq!(format!("{}", p), "101325 Pa"); + } + + #[test] + fn test_pressure_from_f64() { + let p: Pressure = 101325.0.into(); + assert_relative_eq!(p.to_pascals(), 101325.0, epsilon = 1e-10); + } + + #[test] + fn test_pressure_add() { + let p1 = Pressure::from_pascals(100_000.0); + let p2 = Pressure::from_pascals(50_000.0); + let p3 = p1 + p2; + assert_relative_eq!(p3.to_pascals(), 150_000.0, epsilon = 1e-10); + } + + #[test] + fn test_pressure_sub() { + let p1 = Pressure::from_pascals(100_000.0); + let p2 = Pressure::from_pascals(30_000.0); + let p3 = p1 - p2; + assert_relative_eq!(p3.to_pascals(), 70_000.0, epsilon = 1e-10); + } + + #[test] + fn test_pressure_mul() { + let p = Pressure::from_pascals(50_000.0); + let p2 = p * 2.0; + assert_relative_eq!(p2.to_pascals(), 100_000.0, epsilon = 1e-10); + + // Test reverse multiplication + let p3 = 2.0 * p; + assert_relative_eq!(p3.to_pascals(), 100_000.0, epsilon = 1e-10); + } + + #[test] + fn test_pressure_div() { + let p = Pressure::from_pascals(100_000.0); + let p2 = p / 2.0; + assert_relative_eq!(p2.to_pascals(), 50_000.0, epsilon = 1e-10); + } + + #[test] + fn test_pressure_psi_conversions() { + // 1 atm = 14.696 psi ≈ 101325 Pa + let p = Pressure::from_psi(14.696); + assert_relative_eq!(p.to_pascals(), 101325.0, epsilon = 1.0); + assert_relative_eq!(p.to_psi(), 14.696, epsilon = 1e-3); + + // Round trip test + let p2 = Pressure::from_psi(100.0); + assert_relative_eq!(p2.to_psi(), 100.0, epsilon = 1e-6); + } + + // ==================== TEMPERATURE TESTS ==================== + + #[test] + fn test_temperature_from_kelvin() { + let t = Temperature::from_kelvin(298.15); + assert_relative_eq!(t.0, 298.15, epsilon = 1e-10); + assert_relative_eq!(t.to_kelvin(), 298.15, epsilon = 1e-10); + } + + #[test] + fn test_temperature_from_celsius() { + let t = Temperature::from_celsius(0.0); + assert_relative_eq!(t.to_kelvin(), 273.15, epsilon = 1e-10); + } + + #[test] + fn test_temperature_to_celsius() { + let t = Temperature::from_kelvin(273.15); + assert_relative_eq!(t.to_celsius(), 0.0, epsilon = 1e-10); + } + + #[test] + fn test_temperature_round_trip() { + let t1 = Temperature::from_celsius(25.0); + let celsius = t1.to_celsius(); + assert_relative_eq!(celsius, 25.0, epsilon = 1e-10); + } + + #[test] + fn test_temperature_fahrenheit_conversions() { + // 32°F = 0°C = 273.15K + let t_freezing = Temperature::from_fahrenheit(32.0); + assert_relative_eq!(t_freezing.to_celsius(), 0.0, epsilon = 1e-10); + assert_relative_eq!(t_freezing.to_kelvin(), 273.15, epsilon = 1e-10); + + // 212°F = 100°C = 373.15K + let t_boiling = Temperature::from_fahrenheit(212.0); + assert_relative_eq!(t_boiling.to_celsius(), 100.0, epsilon = 1e-10); + assert_relative_eq!(t_boiling.to_fahrenheit(), 212.0, epsilon = 1e-10); + + // Round trip + let t1 = Temperature::from_kelvin(300.0); + let f = t1.to_fahrenheit(); + let t2 = Temperature::from_fahrenheit(f); + assert_relative_eq!(t1.to_kelvin(), t2.to_kelvin(), epsilon = 1e-10); + } + + #[test] + fn test_temperature_display() { + let t = Temperature::from_kelvin(298.15); + assert_eq!(format!("{}", t), "298.15 K"); + } + + #[test] + fn test_temperature_add() { + let t1 = Temperature::from_kelvin(300.0); + let t2 = Temperature::from_kelvin(10.0); + let t3 = t1 + t2; + assert_relative_eq!(t3.to_kelvin(), 310.0, epsilon = 1e-10); + } + + #[test] + fn test_temperature_sub() { + let t1 = Temperature::from_kelvin(300.0); + let t2 = Temperature::from_kelvin(50.0); + let t3 = t1 - t2; + assert_relative_eq!(t3.to_kelvin(), 250.0, epsilon = 1e-10); + } + + #[test] + fn test_temperature_mul() { + let t = Temperature::from_kelvin(100.0); + let t2 = t * 2.0; + assert_relative_eq!(t2.to_kelvin(), 200.0, epsilon = 1e-10); + + // Test reverse multiplication + let t3 = 2.0 * t; + assert_relative_eq!(t3.to_kelvin(), 200.0, epsilon = 1e-10); + } + + #[test] + fn test_temperature_div() { + let t = Temperature::from_kelvin(300.0); + let t2 = t / 3.0; + assert_relative_eq!(t2.to_kelvin(), 100.0, epsilon = 1e-10); + } + + // ==================== ENTHALPY TESTS ==================== + + #[test] + fn test_enthalpy_from_joules_per_kg() { + let h = Enthalpy::from_joules_per_kg(1000.0); + assert_relative_eq!(h.0, 1000.0, epsilon = 1e-10); + assert_relative_eq!(h.to_joules_per_kg(), 1000.0, epsilon = 1e-10); + } + + #[test] + fn test_enthalpy_display() { + let h = Enthalpy::from_joules_per_kg(250000.0); + assert_eq!(format!("{}", h), "250000 J/kg"); + } + + #[test] + fn test_enthalpy_arithmetic() { + let h1 = Enthalpy::from_joules_per_kg(1000.0); + let h2 = Enthalpy::from_joules_per_kg(500.0); + let h3 = h1 + h2; + assert_relative_eq!(h3.to_joules_per_kg(), 1500.0, epsilon = 1e-10); + + let h4 = h1 - h2; + assert_relative_eq!(h4.to_joules_per_kg(), 500.0, epsilon = 1e-10); + + let h5 = h1 * 2.0; + assert_relative_eq!(h5.to_joules_per_kg(), 2000.0, epsilon = 1e-10); + + let h6 = h1 / 2.0; + assert_relative_eq!(h6.to_joules_per_kg(), 500.0, epsilon = 1e-10); + + // Test reverse multiplication + let h7 = 2.0 * h1; + assert_relative_eq!(h7.to_joules_per_kg(), 2000.0, epsilon = 1e-10); + } + + #[test] + fn test_enthalpy_unit_conversions() { + let h1 = Enthalpy::from_kilojoules_per_kg(100.0); + assert_relative_eq!(h1.to_joules_per_kg(), 100_000.0, epsilon = 1e-6); + assert_relative_eq!(h1.to_kilojoules_per_kg(), 100.0, epsilon = 1e-6); + } + + // ==================== MASS FLOW TESTS ==================== + + #[test] + fn test_mass_flow_from_kg_per_s() { + let m = MassFlow::from_kg_per_s(0.5); + assert_relative_eq!(m.0, 0.5, epsilon = 1e-10); + assert_relative_eq!(m.to_kg_per_s(), 0.5, epsilon = 1e-10); + } + + #[test] + fn test_mass_flow_display() { + let m = MassFlow::from_kg_per_s(0.1); + assert_eq!(format!("{}", m), "0.1 kg/s"); + } + + #[test] + fn test_mass_flow_arithmetic() { + let m1 = MassFlow::from_kg_per_s(1.0); + let m2 = MassFlow::from_kg_per_s(0.5); + let m3 = m1 + m2; + assert_relative_eq!(m3.to_kg_per_s(), 1.5, epsilon = 1e-10); + + let m4 = m1 - m2; + assert_relative_eq!(m4.to_kg_per_s(), 0.5, epsilon = 1e-10); + + let m5 = m1 * 2.0; + assert_relative_eq!(m5.to_kg_per_s(), 2.0, epsilon = 1e-10); + + let m6 = m1 / 2.0; + assert_relative_eq!(m6.to_kg_per_s(), 0.5, epsilon = 1e-10); + + // Test reverse multiplication + let m7 = 2.0 * m1; + assert_relative_eq!(m7.to_kg_per_s(), 2.0, epsilon = 1e-10); + } + + #[test] + fn test_mass_flow_unit_conversions() { + let m1 = MassFlow::from_grams_per_s(500.0); + assert_relative_eq!(m1.to_kg_per_s(), 0.5, epsilon = 1e-6); + assert_relative_eq!(m1.to_grams_per_s(), 500.0, epsilon = 1e-6); + } + + // ==================== TYPE SAFETY TESTS ==================== + + #[test] + fn test_pressure_not_equal_to_temperature() { + // This test ensures Pressure and Temperature are distinct types + let p = Pressure::from_pascals(100_000.0); + let t = Temperature::from_kelvin(100.0); + + // They should have different internal representations if units differ + // But more importantly, this test would fail at compile time if we tried: + // let x: Pressure = t; // Compile error! + assert_ne!(p.0, t.0); + } + + #[test] + fn test_clone_and_copy() { + let p1 = Pressure::from_pascals(100_000.0); + let p2 = p1; // Copy + let p3 = p1.clone(); // Clone + + assert_relative_eq!(p1.to_pascals(), p2.to_pascals(), epsilon = 1e-10); + assert_relative_eq!(p1.to_pascals(), p3.to_pascals(), epsilon = 1e-10); + } + + #[test] + fn test_partial_ord() { + let p1 = Pressure::from_pascals(100_000.0); + let p2 = Pressure::from_pascals(200_000.0); + + assert!(p1 < p2); + assert!(p2 > p1); + assert!(p1 == Pressure::from_pascals(100_000.0)); + } + + #[test] + fn test_partial_ord_with_nan() { + let p1 = Pressure::from_pascals(100_000.0); + let p_nan = Pressure(f64::NAN); + + // NaN comparisons should return false for all ordering operators + assert!(!(p_nan < p1)); + assert!(!(p_nan > p1)); + assert!(!(p_nan == p1)); + assert!(!(p1 < p_nan)); + assert!(!(p1 > p_nan)); + assert!(!(p1 == p_nan)); + + // NaN should not equal itself + assert!(!(p_nan == p_nan)); + } + + // ==================== EDGE CASES ==================== + + #[test] + fn test_zero_values() { + let p = Pressure::from_pascals(0.0); + assert_relative_eq!(p.to_pascals(), 0.0, epsilon = 1e-10); + assert_relative_eq!(p.to_bar(), 0.0, epsilon = 1e-10); + + let t = Temperature::from_kelvin(0.0); + assert_relative_eq!(t.to_kelvin(), 0.0, epsilon = 1e-10); + assert_relative_eq!(t.to_celsius(), -273.15, epsilon = 1e-10); + } + + #[test] + fn test_negative_values() { + // Negative pressure doesn't make physical sense but is allowed by the type + let p = Pressure::from_pascals(-1000.0); + assert_relative_eq!(p.to_pascals(), -1000.0, epsilon = 1e-10); + + // Negative temperature (below absolute zero) doesn't make sense + let t = Temperature::from_kelvin(-1.0); + assert_relative_eq!(t.to_kelvin(), -1.0, epsilon = 1e-10); + } + + #[test] + fn test_very_large_values() { + let p = Pressure::from_pascals(1e15); + assert_relative_eq!(p.to_pascals(), 1e15, epsilon = 1e5); + + let t = Temperature::from_kelvin(1e9); + assert_relative_eq!(t.to_kelvin(), 1e9, epsilon = 1e-1); + } + + #[test] + fn test_very_small_values() { + let p = Pressure::from_pascals(1e-10); + assert_relative_eq!(p.to_pascals(), 1e-10, epsilon = 1e-15); + + let m = MassFlow::from_kg_per_s(1e-12); + assert_relative_eq!(m.to_kg_per_s(), 1e-12, epsilon = 1e-17); + } +}