feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
235
crates/fluids/src/cache.rs
Normal file
235
crates/fluids/src/cache.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
//! Thread-local LRU cache for fluid property queries.
|
||||
//!
|
||||
//! Avoids redundant backend calls without mutex contention by using
|
||||
//! per-thread storage. Cache keys use quantized state values since f64
|
||||
//! does not implement Hash.
|
||||
//!
|
||||
//! # Quantization Strategy
|
||||
//!
|
||||
//! State values (P, T, h, s, x) are quantized to 1e-9 relative precision
|
||||
//! for cache key derivation. Solver iterations often repeat the same
|
||||
//! (P,T) or (P,h) states; quantization should not lose cache hits for
|
||||
//! typical thermodynamic ranges (P: 1e3–1e7 Pa, T: 200–600 K).
|
||||
|
||||
use crate::mixture::Mixture;
|
||||
use crate::types::{FluidId, Property, FluidState};
|
||||
use lru::LruCache;
|
||||
use std::cell::RefCell;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
/// Default cache capacity (entries). LRU eviction when exceeded.
|
||||
pub const DEFAULT_CACHE_CAPACITY: usize = 10_000;
|
||||
|
||||
/// Default capacity as NonZeroUsize for LruCache (avoids unwrap in production path).
|
||||
const DEFAULT_CAP_NONZERO: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(DEFAULT_CACHE_CAPACITY) };
|
||||
|
||||
/// Quantization factor: values rounded to 1e-9 relative.
|
||||
/// (v * 1e9).round() as i64 for Hash-compatible key.
|
||||
#[inline]
|
||||
fn quantize(v: f64) -> i64 {
|
||||
if v.is_nan() || v.is_infinite() {
|
||||
0
|
||||
} else {
|
||||
(v * 1e9).round() as i64
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache key for fluid property lookups.
|
||||
///
|
||||
/// Uses quantized state values since f64 does not implement Hash.
|
||||
/// Includes backend_id so multiple CachedBackend instances don't mix results.
|
||||
/// For mixtures, includes a hash of the mixture composition.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CacheKey {
|
||||
backend_id: usize,
|
||||
fluid: String,
|
||||
property: Property,
|
||||
variant: u8,
|
||||
p_quantized: i64,
|
||||
second_quantized: i64,
|
||||
mixture_hash: Option<u64>,
|
||||
}
|
||||
|
||||
impl PartialEq for CacheKey {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.backend_id == other.backend_id
|
||||
&& self.fluid == other.fluid
|
||||
&& self.property == other.property
|
||||
&& self.variant == other.variant
|
||||
&& self.p_quantized == other.p_quantized
|
||||
&& self.second_quantized == other.second_quantized
|
||||
&& self.mixture_hash == other.mixture_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for CacheKey {}
|
||||
|
||||
impl Hash for CacheKey {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.backend_id.hash(state);
|
||||
self.fluid.hash(state);
|
||||
self.property.hash(state);
|
||||
self.variant.hash(state);
|
||||
self.p_quantized.hash(state);
|
||||
self.second_quantized.hash(state);
|
||||
self.mixture_hash.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey {
|
||||
/// Build a cache key from fluid, property, state, and backend id.
|
||||
pub fn new(backend_id: usize, fluid: &FluidId, property: Property, state: &FluidState) -> Self {
|
||||
let (p, second, variant, mixture_hash) = match state {
|
||||
FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin(), 0u8, None),
|
||||
FluidState::PressureEnthalpy(p, h) => {
|
||||
(p.to_pascals(), h.to_joules_per_kg(), 1u8, None)
|
||||
}
|
||||
FluidState::PressureEntropy(p, s) => {
|
||||
(p.to_pascals(), s.to_joules_per_kg_kelvin(), 2u8, None)
|
||||
}
|
||||
FluidState::PressureQuality(p, x) => (p.to_pascals(), x.value(), 3u8, None),
|
||||
FluidState::PressureTemperatureMixture(p, t, ref m) => {
|
||||
(p.to_pascals(), t.to_kelvin(), 4u8, Some(mix_hash(m)))
|
||||
}
|
||||
FluidState::PressureEnthalpyMixture(p, h, ref m) => {
|
||||
(p.to_pascals(), h.to_joules_per_kg(), 5u8, Some(mix_hash(m)))
|
||||
}
|
||||
FluidState::PressureQualityMixture(p, x, ref m) => {
|
||||
(p.to_pascals(), x.value(), 6u8, Some(mix_hash(m)))
|
||||
}
|
||||
};
|
||||
CacheKey {
|
||||
backend_id,
|
||||
fluid: fluid.0.clone(),
|
||||
property,
|
||||
variant,
|
||||
p_quantized: quantize(p),
|
||||
second_quantized: quantize(second),
|
||||
mixture_hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a simple hash for a mixture for cache key purposes.
|
||||
fn mix_hash(mixture: &Mixture) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
let mut hasher = DefaultHasher::new();
|
||||
mixture.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CACHE: RefCell<LruCache<CacheKey, f64>> = RefCell::new(
|
||||
LruCache::new(DEFAULT_CAP_NONZERO)
|
||||
);
|
||||
}
|
||||
|
||||
/// Get a value from the thread-local cache (no allocation on key build for hot path).
|
||||
pub fn cache_get(
|
||||
backend_id: usize,
|
||||
fluid: &FluidId,
|
||||
property: Property,
|
||||
state: &FluidState,
|
||||
) -> Option<f64> {
|
||||
let key = CacheKey::new(backend_id, fluid, property, state);
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.get(&key).copied()
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert a value into the thread-local cache.
|
||||
pub fn cache_insert(
|
||||
backend_id: usize,
|
||||
fluid: &FluidId,
|
||||
property: Property,
|
||||
state: &FluidState,
|
||||
value: f64,
|
||||
) {
|
||||
let key = CacheKey::new(backend_id, fluid, property, state);
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.put(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear the thread-local cache (e.g. at solver iteration boundaries).
|
||||
pub fn cache_clear() {
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/// Resize the thread-local cache capacity.
|
||||
pub fn cache_resize(capacity: NonZeroUsize) {
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.resize(capacity);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use entropyk_core::{Pressure, Temperature};
|
||||
|
||||
#[test]
|
||||
fn test_cache_key_quantization() {
|
||||
let fluid = FluidId::new("R134a");
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let key1 = CacheKey::new(0, &fluid, Property::Density, &state);
|
||||
let key2 = CacheKey::new(0, &fluid, Property::Density, &state);
|
||||
assert_eq!(key1, key2);
|
||||
// Equal keys must have same hash (for HashMap use)
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
let mut h1 = DefaultHasher::new();
|
||||
let mut h2 = DefaultHasher::new();
|
||||
key1.hash(&mut h1);
|
||||
key2.hash(&mut h2);
|
||||
assert_eq!(h1.finish(), h2.finish());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_key_different_states() {
|
||||
let fluid = FluidId::new("R134a");
|
||||
let state1 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let state2 = FluidState::from_pt(Pressure::from_bar(2.0), Temperature::from_celsius(25.0));
|
||||
let key1 = CacheKey::new(0, &fluid, Property::Density, &state1);
|
||||
let key2 = CacheKey::new(0, &fluid, Property::Density, &state2);
|
||||
assert_ne!(key1, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lru_eviction() {
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
cache_clear();
|
||||
cache_resize(NonZeroUsize::new(2).expect("2 is non-zero"));
|
||||
|
||||
let fluid = FluidId::new("R134a");
|
||||
let state1 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let state2 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let state3 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(30.0));
|
||||
|
||||
cache_insert(0, &fluid, Property::Density, &state1, 1000.0);
|
||||
cache_insert(0, &fluid, Property::Density, &state2, 1100.0);
|
||||
cache_insert(0, &fluid, Property::Density, &state3, 1200.0);
|
||||
|
||||
assert!(cache_get(0, &fluid, Property::Density, &state1).is_none());
|
||||
assert_eq!(cache_get(0, &fluid, Property::Density, &state2), Some(1100.0));
|
||||
assert_eq!(cache_get(0, &fluid, Property::Density, &state3), Some(1200.0));
|
||||
|
||||
cache_resize(NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).expect("capacity is non-zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_key_different_backends() {
|
||||
let fluid = FluidId::new("R134a");
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let key1 = CacheKey::new(0, &fluid, Property::Density, &state);
|
||||
let key2 = CacheKey::new(1, &fluid, Property::Density, &state);
|
||||
assert_ne!(key1, key2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user