chore: remove deprecated flow_boundary and update docs to match new architecture

This commit is contained in:
Sepehr
2026-03-01 20:00:09 +01:00
parent 20700afce8
commit d88914a44f
105 changed files with 11222 additions and 2994 deletions

View File

@@ -0,0 +1,35 @@
{
"model": "ZP49KCE-TFD",
"manufacturer": "Copeland",
"refrigerant": "R410A",
"capacity_coeffs": [
16500.0,
320.0,
-110.0,
2.3,
1.6,
-3.8,
0.04,
0.025,
-0.018,
0.009
],
"power_coeffs": [
4100.0,
88.0,
42.0,
0.75,
0.45,
1.1,
0.018,
0.009,
0.008,
0.004
],
"validity": {
"t_suction_min": -10.0,
"t_suction_max": 20.0,
"t_discharge_min": 25.0,
"t_discharge_max": 65.0
}
}

View File

@@ -0,0 +1,35 @@
{
"model": "ZP54KCE-TFD",
"manufacturer": "Copeland",
"refrigerant": "R410A",
"capacity_coeffs": [
18000.0,
350.0,
-120.0,
2.5,
1.8,
-4.2,
0.05,
0.03,
-0.02,
0.01
],
"power_coeffs": [
4500.0,
95.0,
45.0,
0.8,
0.5,
1.2,
0.02,
0.01,
0.01,
0.005
],
"validity": {
"t_suction_min": -10.0,
"t_suction_max": 20.0,
"t_discharge_min": 25.0,
"t_discharge_max": 65.0
}
}

View File

@@ -0,0 +1,4 @@
[
"ZP54KCE-TFD",
"ZP49KCE-TFD"
]

View File

@@ -0,0 +1,35 @@
{
"model": "SH090-4",
"manufacturer": "Danfoss",
"refrigerant": "R410A",
"capacity_coeffs": [
25000.0,
500.0,
-150.0,
3.5,
2.5,
-5.0,
0.05,
0.03,
-0.02,
0.01
],
"power_coeffs": [
6000.0,
150.0,
60.0,
1.5,
1.0,
1.5,
0.02,
0.015,
0.01,
0.005
],
"validity": {
"t_suction_min": -15.0,
"t_suction_max": 15.0,
"t_discharge_min": 30.0,
"t_discharge_max": 65.0
}
}

View File

@@ -0,0 +1,35 @@
{
"model": "SH140-4",
"manufacturer": "Danfoss",
"refrigerant": "R410A",
"capacity_coeffs": [
38000.0,
750.0,
-200.0,
5.0,
3.8,
-7.0,
0.08,
0.045,
-0.03,
0.015
],
"power_coeffs": [
9500.0,
220.0,
90.0,
2.2,
1.5,
2.3,
0.03,
0.02,
0.015,
0.008
],
"validity": {
"t_suction_min": -15.0,
"t_suction_max": 15.0,
"t_discharge_min": 30.0,
"t_discharge_max": 65.0
}
}

View File

@@ -0,0 +1,4 @@
[
"SH090-4",
"SH140-4"
]

View File

@@ -0,0 +1,320 @@
//! Danfoss compressor data backend.
//!
//! Loads AHRI 540 compressor coefficients from JSON files in the
//! `data/danfoss/compressors/` directory.
use std::collections::HashMap;
use std::path::PathBuf;
use crate::error::VendorError;
use crate::vendor_api::{
BphxParameters, CompressorCoefficients, UaCalcParams, VendorBackend,
};
/// Backend for Danfoss scroll compressor data.
///
/// Loads an index file (`index.json`) listing available compressor models,
/// then eagerly pre-caches each model's JSON file into memory.
///
/// # Example
///
/// ```no_run
/// use entropyk_vendors::compressors::danfoss::DanfossBackend;
/// use entropyk_vendors::VendorBackend;
///
/// let backend = DanfossBackend::new().expect("load danfoss data");
/// let models = backend.list_compressor_models().unwrap();
/// println!("Available: {:?}", models);
/// ```
#[derive(Debug)]
pub struct DanfossBackend {
/// Root path to the Danfoss data directory.
data_path: PathBuf,
/// Pre-loaded compressor coefficients keyed by model name.
compressor_cache: HashMap<String, CompressorCoefficients>,
/// Sorted list of available models.
sorted_models: Vec<String>,
}
impl DanfossBackend {
/// Create a new Danfoss backend, loading all compressor models from disk.
///
/// The data directory is resolved via the `ENTROPYK_DATA` environment variable.
/// If unset, it falls back to the compile-time `CARGO_MANIFEST_DIR/data` in debug mode,
/// or `./data` in release mode.
pub fn new() -> Result<Self, VendorError> {
let base_path = std::env::var("ENTROPYK_DATA")
.map(PathBuf::from)
.unwrap_or_else(|_| {
#[cfg(debug_assertions)]
{
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data")
}
#[cfg(not(debug_assertions))]
{
PathBuf::from("data")
}
});
let data_path = base_path.join("danfoss");
let mut backend = Self {
data_path,
compressor_cache: HashMap::new(),
sorted_models: Vec::new(),
};
backend.load_index()?;
Ok(backend)
}
/// Create a new Danfoss backend from a custom data path.
///
/// Useful for testing with alternative data directories.
pub fn from_path(data_path: PathBuf) -> Result<Self, VendorError> {
let mut backend = Self {
data_path,
compressor_cache: HashMap::new(),
sorted_models: Vec::new(),
};
backend.load_index()?;
Ok(backend)
}
/// Load the compressor index and pre-cache all referenced models.
fn load_index(&mut self) -> Result<(), VendorError> {
let index_path = self.data_path.join("compressors").join("index.json");
let index_content = std::fs::read_to_string(&index_path).map_err(|e| {
VendorError::IoError {
path: index_path.display().to_string(),
source: e,
}
})?;
let models: Vec<String> = serde_json::from_str(&index_content).map_err(|e| {
VendorError::InvalidFormat(format!("Parse error in {}: {}", index_path.display(), e))
})?;
for model in models {
match self.load_model(&model) {
Ok(coeffs) => {
self.compressor_cache.insert(model.clone(), coeffs);
self.sorted_models.push(model);
}
Err(e) => {
log::warn!("[entropyk-vendors] Skipping Danfoss model {}: {}", model, e);
}
}
}
self.sorted_models.sort();
Ok(())
}
/// Load a single compressor model from its JSON file.
fn load_model(&self, model: &str) -> Result<CompressorCoefficients, VendorError> {
if model.contains('/') || model.contains('\\') || model.contains("..") {
return Err(VendorError::ModelNotFound(model.to_string()));
}
let model_path = self
.data_path
.join("compressors")
.join(format!("{}.json", model));
let content = std::fs::read_to_string(&model_path).map_err(|e| VendorError::IoError {
path: model_path.display().to_string(),
source: e,
})?;
let coeffs: CompressorCoefficients = serde_json::from_str(&content).map_err(|e| {
VendorError::InvalidFormat(format!("Parse error in {}: {}", model_path.display(), e))
})?;
Ok(coeffs)
}
}
impl VendorBackend for DanfossBackend {
fn vendor_name(&self) -> &str {
"Danfoss"
}
fn list_compressor_models(&self) -> Result<Vec<String>, VendorError> {
Ok(self.sorted_models.clone())
}
fn get_compressor_coefficients(
&self,
model: &str,
) -> Result<CompressorCoefficients, VendorError> {
self.compressor_cache
.get(model)
.cloned()
.ok_or_else(|| VendorError::ModelNotFound(model.to_string()))
}
fn list_bphx_models(&self) -> Result<Vec<String>, VendorError> {
// Danfoss does not provide BPHX data
Ok(vec![])
}
fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError> {
Err(VendorError::InvalidFormat(format!(
"Danfoss does not provide BPHX data (requested: {})",
model
)))
}
fn compute_ua(&self, model: &str, _params: &UaCalcParams) -> Result<f64, VendorError> {
Err(VendorError::InvalidFormat(format!(
"Danfoss does not provide BPHX/UA data (requested: {})",
model
)))
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_danfoss_backend_new() {
let backend = DanfossBackend::new();
assert!(backend.is_ok(), "DanfossBackend::new() should succeed");
}
#[test]
fn test_danfoss_backend_from_path() {
let base_path = std::env::var("ENTROPYK_DATA")
.map(PathBuf::from)
.unwrap_or_else(|_| {
#[cfg(debug_assertions)]
{
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data")
}
#[cfg(not(debug_assertions))]
{
PathBuf::from("data")
}
});
let backend = DanfossBackend::from_path(base_path.join("danfoss"));
assert!(backend.is_ok(), "DanfossBackend::from_path() should succeed");
}
#[test]
fn test_danfoss_vendor_name() {
let backend = DanfossBackend::new().unwrap();
assert_eq!(backend.vendor_name(), "Danfoss");
}
#[test]
fn test_danfoss_list_compressor_models() {
let backend = DanfossBackend::new().unwrap();
let models = backend.list_compressor_models().unwrap();
assert_eq!(models.len(), 2);
assert!(models.contains(&"SH140-4".to_string()));
assert!(models.contains(&"SH090-4".to_string()));
assert_eq!(models, vec!["SH090-4".to_string(), "SH140-4".to_string()]);
}
#[test]
fn test_danfoss_get_compressor_sh140() {
let backend = DanfossBackend::new().unwrap();
let coeffs = backend
.get_compressor_coefficients("SH140-4")
.unwrap();
assert_eq!(coeffs.model, "SH140-4");
assert_eq!(coeffs.manufacturer, "Danfoss");
assert_eq!(coeffs.refrigerant, "R410A");
assert_eq!(coeffs.capacity_coeffs.len(), 10);
assert_eq!(coeffs.power_coeffs.len(), 10);
// Check first capacity coefficient
assert!((coeffs.capacity_coeffs[0] - 38000.0).abs() < 1e-10);
// Check first power coefficient
assert!((coeffs.power_coeffs[0] - 9500.0).abs() < 1e-10);
// Check last capacity coefficient
assert!((coeffs.capacity_coeffs[9] - 0.015).abs() < 1e-10);
// Check last power coefficient
assert!((coeffs.power_coeffs[9] - 0.008).abs() < 1e-10);
// mass_flow_coeffs not provided in Danfoss data
assert!(coeffs.mass_flow_coeffs.is_none());
}
#[test]
fn test_danfoss_get_compressor_sh090() {
let backend = DanfossBackend::new().unwrap();
let coeffs = backend
.get_compressor_coefficients("SH090-4")
.unwrap();
assert_eq!(coeffs.model, "SH090-4");
assert_eq!(coeffs.manufacturer, "Danfoss");
assert_eq!(coeffs.refrigerant, "R410A");
assert!((coeffs.capacity_coeffs[0] - 25000.0).abs() < 1e-10);
assert!((coeffs.power_coeffs[0] - 6000.0).abs() < 1e-10);
assert!((coeffs.capacity_coeffs[9] - 0.01).abs() < 1e-10);
assert!((coeffs.power_coeffs[9] - 0.005).abs() < 1e-10);
}
#[test]
fn test_danfoss_validity_range() {
let backend = DanfossBackend::new().unwrap();
let coeffs = backend
.get_compressor_coefficients("SH140-4")
.unwrap();
assert!((coeffs.validity.t_suction_min - (-15.0)).abs() < 1e-10);
assert!((coeffs.validity.t_suction_max - 15.0).abs() < 1e-10);
assert!((coeffs.validity.t_discharge_min - 30.0).abs() < 1e-10);
assert!((coeffs.validity.t_discharge_max - 65.0).abs() < 1e-10);
}
#[test]
fn test_danfoss_model_not_found() {
let backend = DanfossBackend::new().unwrap();
let result = backend.get_compressor_coefficients("NONEXISTENT");
assert!(result.is_err());
match result.unwrap_err() {
VendorError::ModelNotFound(m) => assert_eq!(m, "NONEXISTENT"),
other => panic!("Expected ModelNotFound, got: {:?}", other),
}
}
#[test]
fn test_danfoss_list_bphx_empty() {
let backend = DanfossBackend::new().unwrap();
let models = backend.list_bphx_models().unwrap();
assert!(models.is_empty());
}
#[test]
fn test_danfoss_get_bphx_returns_error() {
let backend = DanfossBackend::new().unwrap();
let result = backend.get_bphx_parameters("anything");
assert!(result.is_err());
match result.unwrap_err() {
VendorError::InvalidFormat(msg) => {
assert!(msg.contains("Danfoss does not provide BPHX"));
}
other => panic!("Expected InvalidFormat, got: {:?}", other),
}
}
#[test]
fn test_danfoss_object_safety() {
let backend: Box<dyn VendorBackend> = Box::new(DanfossBackend::new().unwrap());
assert_eq!(backend.vendor_name(), "Danfoss");
let models = backend.list_compressor_models().unwrap();
assert!(!models.is_empty());
}
}

11
crates/vendors/src/compressors/mod.rs vendored Normal file
View File

@@ -0,0 +1,11 @@
//! Compressor vendor backend implementations.
//!
//! Each vendor module implements [`VendorBackend`](crate::VendorBackend) for
//! loading AHRI 540 compressor coefficients from vendor-specific data files.
/// Copeland (Emerson) compressor data backend.
pub mod copeland;
// Future vendor implementations (stories 11.14, 11.15):
pub mod danfoss;
// pub mod bitzer; // Story 11.15

View File

@@ -0,0 +1,7 @@
//! Heat exchanger vendor backend implementations.
//!
//! Each vendor module implements [`VendorBackend`](crate::VendorBackend) for
//! loading BPHX parameters and UA curves from vendor-specific data files.
/// SWEP brazed-plate heat exchanger data backend.
pub mod swep;

View File

@@ -21,6 +21,9 @@ pub mod compressors;
pub mod heat_exchangers;
// Public re-exports for convenience
pub use compressors::copeland::CopelandBackend;
pub use compressors::danfoss::DanfossBackend;
pub use heat_exchangers::swep::SwepBackend;
pub use error::VendorError;
pub use vendor_api::{
BphxParameters, CompressorCoefficients, CompressorValidityRange, UaCalcParams, UaCurve,