Skip to main content

refprop/
converter.rs

1//! Configurable unit conversion for REFPROP values.
2//!
3//! REFPROP internally uses: **K, kPa, mol/L, J/mol, J/(mol·K), µPa·s,
4//! W/(m·K), m/s**.  This module lets you work in whatever units you
5//! prefer (°C, bar, kg/m³, kJ/kg, …) and handles the conversion
6//! transparently.
7//!
8//! # Presets
9//!
10//! | Preset          | T   | P   | D     | H      | S         |
11//! |-----------------|-----|-----|-------|--------|-----------|
12//! | `refprop()`     | K   | kPa | mol/L | J/mol  | J/(mol·K) |
13//! | `engineering()` | °C  | bar | kg/m³ | kJ/kg  | kJ/(kg·K) |
14//! | `si()`          | K   | Pa  | kg/m³ | J/kg   | J/(kg·K)  |
15//!
16//! # Builder
17//!
18//! ```
19//! use refprop::{UnitSystem, TempUnit, PressUnit};
20//!
21//! let units = UnitSystem::new()
22//!     .temperature(TempUnit::Celsius)
23//!     .pressure(PressUnit::Bar);
24//! ```
25
26use serde::{Deserialize, Serialize};
27
28use crate::error::{RefpropError, Result};
29
30// ────────────────────────────────────────────────────────────────────
31//  Unit enums
32// ────────────────────────────────────────────────────────────────────
33
34/// Temperature unit.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum TempUnit {
37    /// Kelvin (REFPROP native)
38    Kelvin,
39    /// Degrees Celsius
40    Celsius,
41    /// Degrees Fahrenheit
42    Fahrenheit,
43}
44
45/// Pressure unit.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub enum PressUnit {
48    /// Kilopascal (REFPROP native)
49    KPa,
50    /// Bar (1 bar = 100 kPa)
51    Bar,
52    /// Megapascal
53    MPa,
54    /// Pascal
55    Pa,
56    /// Standard atmosphere (101.325 kPa)
57    Atm,
58    /// Pounds per square inch
59    Psi,
60}
61
62/// Density unit.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64pub enum DensityUnit {
65    /// mol/L (REFPROP native)
66    MolPerL,
67    /// kg/m³ (requires molar mass)
68    KgPerM3,
69}
70
71/// Energy / enthalpy unit.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum EnergyUnit {
74    /// J/mol (REFPROP native)
75    JPerMol,
76    /// kJ/kg (requires molar mass)
77    KJPerKg,
78    /// J/kg (requires molar mass)
79    JPerKg,
80}
81
82/// Entropy / heat-capacity unit (energy per temperature).
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum EntropyUnit {
85    /// J/(mol·K) (REFPROP native)
86    JPerMolK,
87    /// kJ/(kg·K) (requires molar mass)
88    KJPerKgK,
89    /// J/(kg·K) (requires molar mass)
90    JPerKgK,
91}
92
93/// Dynamic viscosity unit.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95pub enum ViscosityUnit {
96    /// µPa·s (REFPROP native)
97    MicroPaS,
98    /// mPa·s (= centipoise)
99    MilliPaS,
100    /// Pa·s
101    PaS,
102}
103
104/// Thermal conductivity unit.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106pub enum ConductivityUnit {
107    /// W/(m·K) (REFPROP native)
108    WPerMK,
109    /// mW/(m·K)
110    MilliWPerMK,
111}
112
113// ────────────────────────────────────────────────────────────────────
114//  UnitSystem — user configuration (no molar mass needed yet)
115// ────────────────────────────────────────────────────────────────────
116
117/// Describes the set of units the user wants to work in.
118///
119/// Create one with a preset (`refprop()`, `engineering()`, `si()`) or
120/// customise individual properties with the builder methods.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UnitSystem {
123    pub temperature: TempUnit,
124    pub pressure: PressUnit,
125    pub density: DensityUnit,
126    pub energy: EnergyUnit,
127    pub entropy: EntropyUnit,
128    pub viscosity: ViscosityUnit,
129    pub conductivity: ConductivityUnit,
130}
131
132impl UnitSystem {
133    /// Start from REFPROP-native units.  Use the builder methods to
134    /// change individual properties.
135    pub fn new() -> Self {
136        Self::refprop()
137    }
138
139    // ── Presets ──────────────────────────────────────────────────────
140
141    /// REFPROP native: K, kPa, mol/L, J/mol, J/(mol·K), µPa·s, W/(m·K).
142    pub fn refprop() -> Self {
143        Self {
144            temperature: TempUnit::Kelvin,
145            pressure: PressUnit::KPa,
146            density: DensityUnit::MolPerL,
147            energy: EnergyUnit::JPerMol,
148            entropy: EntropyUnit::JPerMolK,
149            viscosity: ViscosityUnit::MicroPaS,
150            conductivity: ConductivityUnit::WPerMK,
151        }
152    }
153
154    /// Engineering / HVAC: °C, bar, kg/m³, kJ/kg, kJ/(kg·K).
155    pub fn engineering() -> Self {
156        Self {
157            temperature: TempUnit::Celsius,
158            pressure: PressUnit::Bar,
159            density: DensityUnit::KgPerM3,
160            energy: EnergyUnit::KJPerKg,
161            entropy: EntropyUnit::KJPerKgK,
162            viscosity: ViscosityUnit::MicroPaS,
163            conductivity: ConductivityUnit::WPerMK,
164        }
165    }
166
167    /// Strict SI: K, Pa, kg/m³, J/kg, J/(kg·K), Pa·s.
168    pub fn si() -> Self {
169        Self {
170            temperature: TempUnit::Kelvin,
171            pressure: PressUnit::Pa,
172            density: DensityUnit::KgPerM3,
173            energy: EnergyUnit::JPerKg,
174            entropy: EntropyUnit::JPerKgK,
175            viscosity: ViscosityUnit::PaS,
176            conductivity: ConductivityUnit::WPerMK,
177        }
178    }
179
180    // ── Builder methods ─────────────────────────────────────────────
181
182    pub fn temperature(mut self, u: TempUnit) -> Self {
183        self.temperature = u;
184        self
185    }
186    pub fn pressure(mut self, u: PressUnit) -> Self {
187        self.pressure = u;
188        self
189    }
190    pub fn density(mut self, u: DensityUnit) -> Self {
191        self.density = u;
192        self
193    }
194    pub fn energy(mut self, u: EnergyUnit) -> Self {
195        self.energy = u;
196        self
197    }
198    pub fn entropy(mut self, u: EntropyUnit) -> Self {
199        self.entropy = u;
200        self
201    }
202    pub fn viscosity(mut self, u: ViscosityUnit) -> Self {
203        self.viscosity = u;
204        self
205    }
206    pub fn conductivity(mut self, u: ConductivityUnit) -> Self {
207        self.conductivity = u;
208        self
209    }
210}
211
212impl Default for UnitSystem {
213    fn default() -> Self {
214        Self::refprop()
215    }
216}
217
218// ────────────────────────────────────────────────────────────────────
219//  Converter — UnitSystem + molar mass → ready to convert
220// ────────────────────────────────────────────────────────────────────
221
222/// Performs conversions between user units and REFPROP internal units.
223///
224/// Created by combining a [`UnitSystem`] with the fluid's molar mass
225/// (needed for mol ↔ kg conversions).
226#[derive(Debug, Clone)]
227pub struct Converter {
228    pub units: UnitSystem,
229    /// Molar mass in g/mol (mixture-averaged for mixtures).
230    pub molar_mass: f64,
231}
232
233impl Converter {
234    pub fn new(units: UnitSystem, molar_mass: f64) -> Self {
235        Self { units, molar_mass }
236    }
237
238    /// Identity converter — no conversion at all (REFPROP native units,
239    /// molar mass = 1 so mass-based formulas still work formally).
240    pub fn identity() -> Self {
241        Self {
242            units: UnitSystem::refprop(),
243            molar_mass: 1.0,
244        }
245    }
246
247    // ── Temperature ─────────────────────────────────────────────────
248
249    /// User → REFPROP (K)
250    pub fn t_to_rp(&self, t: f64) -> f64 {
251        match self.units.temperature {
252            TempUnit::Kelvin => t,
253            TempUnit::Celsius => t + 273.15,
254            TempUnit::Fahrenheit => (t - 32.0) * 5.0 / 9.0 + 273.15,
255        }
256    }
257
258    /// REFPROP (K) → User
259    pub fn t_from_rp(&self, t: f64) -> f64 {
260        match self.units.temperature {
261            TempUnit::Kelvin => t,
262            TempUnit::Celsius => t - 273.15,
263            TempUnit::Fahrenheit => (t - 273.15) * 9.0 / 5.0 + 32.0,
264        }
265    }
266
267    // ── Pressure ────────────────────────────────────────────────────
268
269    /// User → REFPROP (kPa)
270    pub fn p_to_rp(&self, p: f64) -> f64 {
271        match self.units.pressure {
272            PressUnit::KPa => p,
273            PressUnit::Bar => p * 100.0,
274            PressUnit::MPa => p * 1000.0,
275            PressUnit::Pa => p / 1000.0,
276            PressUnit::Atm => p * 101.325,
277            PressUnit::Psi => p * 6.894_757,
278        }
279    }
280
281    /// REFPROP (kPa) → User
282    pub fn p_from_rp(&self, p: f64) -> f64 {
283        match self.units.pressure {
284            PressUnit::KPa => p,
285            PressUnit::Bar => p / 100.0,
286            PressUnit::MPa => p / 1000.0,
287            PressUnit::Pa => p * 1000.0,
288            PressUnit::Atm => p / 101.325,
289            PressUnit::Psi => p / 6.894_757,
290        }
291    }
292
293    // ── Density ─────────────────────────────────────────────────────
294
295    /// User → REFPROP (mol/L)
296    pub fn d_to_rp(&self, d: f64) -> f64 {
297        match self.units.density {
298            DensityUnit::MolPerL => d,
299            DensityUnit::KgPerM3 => d / self.molar_mass,
300        }
301    }
302
303    /// REFPROP (mol/L) → User
304    pub fn d_from_rp(&self, d: f64) -> f64 {
305        match self.units.density {
306            DensityUnit::MolPerL => d,
307            DensityUnit::KgPerM3 => d * self.molar_mass,
308        }
309    }
310
311    // ── Energy / Enthalpy / Internal energy ─────────────────────────
312
313    /// User → REFPROP (J/mol)
314    pub fn h_to_rp(&self, h: f64) -> f64 {
315        match self.units.energy {
316            EnergyUnit::JPerMol => h,
317            EnergyUnit::KJPerKg => h * self.molar_mass,
318            EnergyUnit::JPerKg => h * self.molar_mass / 1000.0,
319        }
320    }
321
322    /// REFPROP (J/mol) → User
323    pub fn h_from_rp(&self, h: f64) -> f64 {
324        match self.units.energy {
325            EnergyUnit::JPerMol => h,
326            EnergyUnit::KJPerKg => h / self.molar_mass,
327            EnergyUnit::JPerKg => h * 1000.0 / self.molar_mass,
328        }
329    }
330
331    // ── Entropy / Cv / Cp ───────────────────────────────────────────
332
333    /// User → REFPROP (J/(mol·K))
334    pub fn s_to_rp(&self, s: f64) -> f64 {
335        match self.units.entropy {
336            EntropyUnit::JPerMolK => s,
337            EntropyUnit::KJPerKgK => s * self.molar_mass,
338            EntropyUnit::JPerKgK => s * self.molar_mass / 1000.0,
339        }
340    }
341
342    /// REFPROP (J/(mol·K)) → User
343    pub fn s_from_rp(&self, s: f64) -> f64 {
344        match self.units.entropy {
345            EntropyUnit::JPerMolK => s,
346            EntropyUnit::KJPerKgK => s / self.molar_mass,
347            EntropyUnit::JPerKgK => s * 1000.0 / self.molar_mass,
348        }
349    }
350
351    // ── Viscosity ───────────────────────────────────────────────────
352
353    /// REFPROP (µPa·s) → User
354    pub fn eta_from_rp(&self, eta: f64) -> f64 {
355        match self.units.viscosity {
356            ViscosityUnit::MicroPaS => eta,
357            ViscosityUnit::MilliPaS => eta / 1000.0,
358            ViscosityUnit::PaS => eta / 1_000_000.0,
359        }
360    }
361
362    /// User → REFPROP (µPa·s)
363    pub fn eta_to_rp(&self, eta: f64) -> f64 {
364        match self.units.viscosity {
365            ViscosityUnit::MicroPaS => eta,
366            ViscosityUnit::MilliPaS => eta * 1000.0,
367            ViscosityUnit::PaS => eta * 1_000_000.0,
368        }
369    }
370
371    // ── Thermal conductivity ────────────────────────────────────────
372
373    /// REFPROP (W/(m·K)) → User
374    pub fn tcx_from_rp(&self, tcx: f64) -> f64 {
375        match self.units.conductivity {
376            ConductivityUnit::WPerMK => tcx,
377            ConductivityUnit::MilliWPerMK => tcx * 1000.0,
378        }
379    }
380
381    /// User → REFPROP (W/(m·K))
382    pub fn tcx_to_rp(&self, tcx: f64) -> f64 {
383        match self.units.conductivity {
384            ConductivityUnit::WPerMK => tcx,
385            ConductivityUnit::MilliWPerMK => tcx / 1000.0,
386        }
387    }
388
389    // ── Quality (vapour fraction) ────────────────────────────────────
390
391    /// User (0–100 %) → REFPROP (0–1 molar fraction).
392    ///
393    /// Returns [`InvalidInput`](RefpropError::InvalidInput) when `q`
394    /// is outside the 0–100 range.
395    pub fn q_to_rp(&self, q: f64) -> Result<f64> {
396        if q < 0.0 || q > 100.0 {
397            return Err(RefpropError::InvalidInput(format!(
398                "Quality Q must be between 0 and 100 (got {q})"
399            )));
400        }
401        Ok(q / 100.0)
402    }
403
404    /// REFPROP (0–1 molar fraction) → User (0–100 %).
405    pub fn q_from_rp(&self, q: f64) -> f64 {
406        q * 100.0
407    }
408
409    // ── Generic key-based conversion ────────────────────────────────
410
411    /// Convert a user-provided input value to REFPROP units, choosing
412    /// the right conversion based on the property key (e.g. `"T"`,
413    /// `"P"`, `"H"`, …).
414    ///
415    /// Quality `"Q"` is expected in **percent** (0–100) and is converted
416    /// to the REFPROP molar fraction (0–1).  Values outside 0–100 yield
417    /// an [`InvalidInput`](RefpropError::InvalidInput) error.
418    pub fn input_to_rp(&self, key: &str, val: f64) -> Result<f64> {
419        match key.to_uppercase().as_str() {
420            "T" => Ok(self.t_to_rp(val)),
421            "P" => Ok(self.p_to_rp(val)),
422            "D" | "RHO" => Ok(self.d_to_rp(val)),
423            "H" => Ok(self.h_to_rp(val)),
424            "S" => Ok(self.s_to_rp(val)),
425            "E" | "U" => Ok(self.h_to_rp(val)),
426            "CV" | "CP" => Ok(self.s_to_rp(val)),
427            "ETA" | "V" | "VIS" => Ok(self.eta_to_rp(val)),
428            "TCX" | "L" | "LAMBDA" => Ok(self.tcx_to_rp(val)),
429            "Q" => self.q_to_rp(val),
430            _ => Ok(val), // W, etc.
431        }
432    }
433
434    /// Convert a REFPROP output value to user units.
435    ///
436    /// Quality `"Q"` is returned in **percent** (0–100), converted from
437    /// the REFPROP molar fraction (0–1).
438    pub fn output_from_rp(&self, key: &str, val: f64) -> f64 {
439        match key.to_uppercase().as_str() {
440            "T" => self.t_from_rp(val),
441            "P" => self.p_from_rp(val),
442            "D" | "RHO" => self.d_from_rp(val),
443            "H" => self.h_from_rp(val),
444            "S" => self.s_from_rp(val),
445            "E" | "U" => self.h_from_rp(val),
446            "CV" | "CP" => self.s_from_rp(val),
447            "ETA" | "V" | "VIS" => self.eta_from_rp(val),
448            "TCX" | "L" | "LAMBDA" => self.tcx_from_rp(val),
449            "Q" => self.q_from_rp(val),
450            _ => val, // W, etc.
451        }
452    }
453}