fix: resolve CLI solver state dimension mismatch
Removed mathematical singularity in HeatExchanger models (q_hot - q_cold = 0 was redundant) causing them to incorrectly request 3 equations without internal variables. Fixed ScrewEconomizerCompressor internal_state_len to perfectly align with the solver dimensions.
This commit is contained in:
15
crates/vendors/Cargo.toml
vendored
Normal file
15
crates/vendors/Cargo.toml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "entropyk-vendors"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Vendor equipment data backends for Entropyk (Copeland, SWEP, Danfoss, Bitzer)"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5"
|
||||
41
crates/vendors/data/swep/bphx/B5THx20.json
vendored
Normal file
41
crates/vendors/data/swep/bphx/B5THx20.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"model": "B5THx20",
|
||||
"manufacturer": "SWEP",
|
||||
"num_plates": 20,
|
||||
"area": 0.45,
|
||||
"dh": 0.003,
|
||||
"chevron_angle": 65.0,
|
||||
"ua_nominal": 1500.0,
|
||||
"ua_curve": {
|
||||
"points": [
|
||||
[
|
||||
0.2,
|
||||
0.30
|
||||
],
|
||||
[
|
||||
0.4,
|
||||
0.55
|
||||
],
|
||||
[
|
||||
0.6,
|
||||
0.72
|
||||
],
|
||||
[
|
||||
0.8,
|
||||
0.88
|
||||
],
|
||||
[
|
||||
1.0,
|
||||
1.00
|
||||
],
|
||||
[
|
||||
1.2,
|
||||
1.08
|
||||
],
|
||||
[
|
||||
1.5,
|
||||
1.15
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
9
crates/vendors/data/swep/bphx/B8THx30.json
vendored
Normal file
9
crates/vendors/data/swep/bphx/B8THx30.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"model": "B8THx30",
|
||||
"manufacturer": "SWEP",
|
||||
"num_plates": 30,
|
||||
"area": 0.72,
|
||||
"dh": 0.0025,
|
||||
"chevron_angle": 60.0,
|
||||
"ua_nominal": 2500.0
|
||||
}
|
||||
4
crates/vendors/data/swep/bphx/index.json
vendored
Normal file
4
crates/vendors/data/swep/bphx/index.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"B5THx20",
|
||||
"B8THx30"
|
||||
]
|
||||
286
crates/vendors/src/compressors/copeland.rs
vendored
Normal file
286
crates/vendors/src/compressors/copeland.rs
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
//! Copeland (Emerson) compressor data backend.
|
||||
//!
|
||||
//! Loads AHRI 540 compressor coefficients from JSON files in the
|
||||
//! `data/copeland/compressors/` directory.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::VendorError;
|
||||
use crate::vendor_api::{
|
||||
BphxParameters, CompressorCoefficients, UaCalcParams, VendorBackend,
|
||||
};
|
||||
|
||||
/// Backend for Copeland (Emerson) 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::copeland::CopelandBackend;
|
||||
/// use entropyk_vendors::VendorBackend;
|
||||
///
|
||||
/// let backend = CopelandBackend::new().expect("load copeland data");
|
||||
/// let models = backend.list_compressor_models().unwrap();
|
||||
/// println!("Available: {:?}", models);
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct CopelandBackend {
|
||||
/// Root path to the Copeland 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 CopelandBackend {
|
||||
/// Create a new Copeland 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("copeland");
|
||||
|
||||
let mut backend = Self {
|
||||
data_path,
|
||||
compressor_cache: HashMap::new(),
|
||||
sorted_models: Vec::new(),
|
||||
};
|
||||
|
||||
backend.load_index()?;
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Create a new Copeland 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)?;
|
||||
|
||||
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 Copeland 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> {
|
||||
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)?;
|
||||
|
||||
Ok(coeffs)
|
||||
}
|
||||
}
|
||||
|
||||
impl VendorBackend for CopelandBackend {
|
||||
fn vendor_name(&self) -> &str {
|
||||
"Copeland (Emerson)"
|
||||
}
|
||||
|
||||
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> {
|
||||
// Copeland does not provide BPHX data
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError> {
|
||||
Err(VendorError::InvalidFormat(format!(
|
||||
"Copeland does not provide BPHX data (requested: {})",
|
||||
model
|
||||
)))
|
||||
}
|
||||
|
||||
fn compute_ua(&self, model: &str, _params: &UaCalcParams) -> Result<f64, VendorError> {
|
||||
Err(VendorError::InvalidFormat(format!(
|
||||
"Copeland does not provide BPHX/UA data (requested: {})",
|
||||
model
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_copeland_backend_new() {
|
||||
let backend = CopelandBackend::new();
|
||||
assert!(backend.is_ok(), "CopelandBackend::new() should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_vendor_name() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
assert_eq!(backend.vendor_name(), "Copeland (Emerson)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_list_compressor_models() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
let models = backend.list_compressor_models().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
assert!(models.contains(&"ZP54KCE-TFD".to_string()));
|
||||
assert!(models.contains(&"ZP49KCE-TFD".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_get_compressor_zp54() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
let coeffs = backend
|
||||
.get_compressor_coefficients("ZP54KCE-TFD")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(coeffs.model, "ZP54KCE-TFD");
|
||||
assert_eq!(coeffs.manufacturer, "Copeland");
|
||||
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] - 18000.0).abs() < 1e-10);
|
||||
// Check first power coefficient
|
||||
assert!((coeffs.power_coeffs[0] - 4500.0).abs() < 1e-10);
|
||||
// mass_flow_coeffs not provided in Copeland data
|
||||
assert!(coeffs.mass_flow_coeffs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_get_compressor_zp49() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
let coeffs = backend
|
||||
.get_compressor_coefficients("ZP49KCE-TFD")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(coeffs.model, "ZP49KCE-TFD");
|
||||
assert_eq!(coeffs.manufacturer, "Copeland");
|
||||
assert_eq!(coeffs.refrigerant, "R410A");
|
||||
assert!((coeffs.capacity_coeffs[0] - 16500.0).abs() < 1e-10);
|
||||
assert!((coeffs.power_coeffs[0] - 4100.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_validity_range() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
let coeffs = backend
|
||||
.get_compressor_coefficients("ZP54KCE-TFD")
|
||||
.unwrap();
|
||||
|
||||
assert!((coeffs.validity.t_suction_min - (-10.0)).abs() < 1e-10);
|
||||
assert!((coeffs.validity.t_suction_max - 20.0).abs() < 1e-10);
|
||||
assert!((coeffs.validity.t_discharge_min - 25.0).abs() < 1e-10);
|
||||
assert!((coeffs.validity.t_discharge_max - 65.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_model_not_found() {
|
||||
let backend = CopelandBackend::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_copeland_list_bphx_empty() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
let models = backend.list_bphx_models().unwrap();
|
||||
assert!(models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_get_bphx_returns_error() {
|
||||
let backend = CopelandBackend::new().unwrap();
|
||||
let result = backend.get_bphx_parameters("anything");
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
VendorError::InvalidFormat(msg) => {
|
||||
assert!(msg.contains("Copeland does not provide BPHX"));
|
||||
}
|
||||
other => panic!("Expected InvalidFormat, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copeland_object_safety() {
|
||||
let backend: Box<dyn VendorBackend> = Box::new(CopelandBackend::new().unwrap());
|
||||
assert_eq!(backend.vendor_name(), "Copeland (Emerson)");
|
||||
let models = backend.list_compressor_models().unwrap();
|
||||
assert!(!models.is_empty());
|
||||
}
|
||||
}
|
||||
340
crates/vendors/src/heat_exchangers/swep.rs
vendored
Normal file
340
crates/vendors/src/heat_exchangers/swep.rs
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
//! SWEP brazed-plate heat exchanger (BPHX) data backend.
|
||||
//!
|
||||
//! Loads BPHX parameters and UA curves from JSON files in the
|
||||
//! `data/swep/bphx/` directory.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::VendorError;
|
||||
use crate::vendor_api::{
|
||||
BphxParameters, CompressorCoefficients, UaCalcParams, VendorBackend,
|
||||
};
|
||||
|
||||
/// Backend for SWEP brazed-plate heat exchanger data.
|
||||
///
|
||||
/// Loads an index file (`index.json`) listing available BPHX models,
|
||||
/// then eagerly pre-caches each model's JSON file into memory.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use entropyk_vendors::heat_exchangers::swep::SwepBackend;
|
||||
/// use entropyk_vendors::VendorBackend;
|
||||
///
|
||||
/// let backend = SwepBackend::new().expect("load SWEP data");
|
||||
/// let models = backend.list_bphx_models().unwrap();
|
||||
/// println!("Available: {:?}", models);
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct SwepBackend {
|
||||
/// Root path to the SWEP data directory.
|
||||
data_path: PathBuf,
|
||||
/// Pre-loaded BPHX parameters keyed by model name.
|
||||
bphx_cache: HashMap<String, BphxParameters>,
|
||||
/// Sorted list of available models.
|
||||
sorted_models: Vec<String>,
|
||||
}
|
||||
|
||||
impl SwepBackend {
|
||||
/// Create a new SWEP backend, loading all BPHX 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("swep");
|
||||
|
||||
let mut backend = Self {
|
||||
data_path,
|
||||
bphx_cache: HashMap::new(),
|
||||
sorted_models: Vec::new(),
|
||||
};
|
||||
|
||||
backend.load_index()?;
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Create a new SWEP 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,
|
||||
bphx_cache: HashMap::new(),
|
||||
sorted_models: Vec::new(),
|
||||
};
|
||||
|
||||
backend.load_index()?;
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// Load the BPHX index and pre-cache all referenced models.
|
||||
fn load_index(&mut self) -> Result<(), VendorError> {
|
||||
let index_path = self.data_path.join("bphx").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)?;
|
||||
|
||||
for model in models {
|
||||
match self.load_model(&model) {
|
||||
Ok(params) => {
|
||||
self.bphx_cache.insert(model.clone(), params);
|
||||
self.sorted_models.push(model);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[entropyk-vendors] Skipping SWEP model {}: {}", model, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.sorted_models.sort();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a single BPHX model from its JSON file.
|
||||
fn load_model(&self, model: &str) -> Result<BphxParameters, VendorError> {
|
||||
let model_path = self
|
||||
.data_path
|
||||
.join("bphx")
|
||||
.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 params: BphxParameters = serde_json::from_str(&content)?;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
impl VendorBackend for SwepBackend {
|
||||
fn vendor_name(&self) -> &str {
|
||||
"SWEP"
|
||||
}
|
||||
|
||||
fn list_compressor_models(&self) -> Result<Vec<String>, VendorError> {
|
||||
// SWEP does not provide compressor data
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn get_compressor_coefficients(
|
||||
&self,
|
||||
model: &str,
|
||||
) -> Result<CompressorCoefficients, VendorError> {
|
||||
Err(VendorError::InvalidFormat(format!(
|
||||
"SWEP does not provide compressor data (requested: {})",
|
||||
model
|
||||
)))
|
||||
}
|
||||
|
||||
fn list_bphx_models(&self) -> Result<Vec<String>, VendorError> {
|
||||
Ok(self.sorted_models.clone())
|
||||
}
|
||||
|
||||
fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError> {
|
||||
self.bphx_cache
|
||||
.get(model)
|
||||
.cloned()
|
||||
.ok_or_else(|| VendorError::ModelNotFound(model.to_string()))
|
||||
}
|
||||
|
||||
fn compute_ua(&self, model: &str, params: &UaCalcParams) -> Result<f64, VendorError> {
|
||||
let bphx = self.get_bphx_parameters(model)?;
|
||||
match bphx.ua_curve {
|
||||
Some(ref curve) => {
|
||||
let ratio = if params.mass_flow_ref > 0.0 {
|
||||
params.mass_flow / params.mass_flow_ref
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let ua_ratio = curve.interpolate(ratio).unwrap_or(1.0);
|
||||
Ok(bphx.ua_nominal * ua_ratio)
|
||||
}
|
||||
None => Ok(bphx.ua_nominal),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_swep_backend_new() {
|
||||
let backend = SwepBackend::new();
|
||||
assert!(backend.is_ok(), "SwepBackend::new() should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_vendor_name() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
assert_eq!(backend.vendor_name(), "SWEP");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_list_bphx_models() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let models = backend.list_bphx_models().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
// Sorted order
|
||||
assert_eq!(models, vec!["B5THx20".to_string(), "B8THx30".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_get_bphx_b5thx20() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let params = backend.get_bphx_parameters("B5THx20").unwrap();
|
||||
|
||||
assert_eq!(params.model, "B5THx20");
|
||||
assert_eq!(params.manufacturer, "SWEP");
|
||||
assert_eq!(params.num_plates, 20);
|
||||
assert!((params.area - 0.45).abs() < 1e-10);
|
||||
assert!((params.dh - 0.003).abs() < 1e-10);
|
||||
assert!((params.chevron_angle - 65.0).abs() < 1e-10);
|
||||
assert!((params.ua_nominal - 1500.0).abs() < 1e-10);
|
||||
assert!(params.ua_curve.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_get_bphx_b8thx30() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let params = backend.get_bphx_parameters("B8THx30").unwrap();
|
||||
|
||||
assert_eq!(params.model, "B8THx30");
|
||||
assert_eq!(params.manufacturer, "SWEP");
|
||||
assert_eq!(params.num_plates, 30);
|
||||
assert!((params.area - 0.72).abs() < 1e-10);
|
||||
assert!((params.dh - 0.0025).abs() < 1e-10);
|
||||
assert!((params.chevron_angle - 60.0).abs() < 1e-10);
|
||||
assert!((params.ua_nominal - 2500.0).abs() < 1e-10);
|
||||
assert!(params.ua_curve.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_ua_curve_present() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let params = backend.get_bphx_parameters("B5THx20").unwrap();
|
||||
let curve = params.ua_curve.as_ref().unwrap();
|
||||
|
||||
// Verify curve has correct number of points
|
||||
assert_eq!(curve.points.len(), 7);
|
||||
|
||||
// Verify interpolation at known points
|
||||
assert!((curve.interpolate(1.0).unwrap() - 1.0).abs() < 1e-10);
|
||||
assert!((curve.interpolate(0.2).unwrap() - 0.30).abs() < 1e-10);
|
||||
assert!((curve.interpolate(1.5).unwrap() - 1.15).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_model_not_found() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let result = backend.get_bphx_parameters("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_swep_compute_ua_with_curve() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let params = UaCalcParams {
|
||||
mass_flow: 0.5,
|
||||
mass_flow_ref: 1.0,
|
||||
temperature_hot_in: 340.0,
|
||||
temperature_cold_in: 280.0,
|
||||
refrigerant: "R410A".into(),
|
||||
};
|
||||
let ua = backend.compute_ua("B5THx20", ¶ms).unwrap();
|
||||
// mass_flow_ratio = 0.5 → interpolate between (0.4, 0.55) and (0.6, 0.72)
|
||||
// t = (0.5 - 0.4) / (0.6 - 0.4) = 0.5
|
||||
// ua_ratio = 0.55 + 0.5 * (0.72 - 0.55) = 0.55 + 0.085 = 0.635
|
||||
// ua = 1500.0 * 0.635 = 952.5
|
||||
assert!((ua - 952.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_compute_ua_without_curve() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let params = UaCalcParams {
|
||||
mass_flow: 0.3,
|
||||
mass_flow_ref: 0.5,
|
||||
temperature_hot_in: 340.0,
|
||||
temperature_cold_in: 280.0,
|
||||
refrigerant: "R410A".into(),
|
||||
};
|
||||
let ua = backend.compute_ua("B8THx30", ¶ms).unwrap();
|
||||
// No curve → returns ua_nominal
|
||||
assert!((ua - 2500.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_compute_ua_model_not_found() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let params = UaCalcParams {
|
||||
mass_flow: 0.3,
|
||||
mass_flow_ref: 0.5,
|
||||
temperature_hot_in: 340.0,
|
||||
temperature_cold_in: 280.0,
|
||||
refrigerant: "R410A".into(),
|
||||
};
|
||||
let result = backend.compute_ua("NONEXISTENT", ¶ms);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_list_compressor_models_empty() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let models = backend.list_compressor_models().unwrap();
|
||||
assert!(models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_get_compressor_returns_error() {
|
||||
let backend = SwepBackend::new().unwrap();
|
||||
let result = backend.get_compressor_coefficients("anything");
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
VendorError::InvalidFormat(msg) => {
|
||||
assert!(msg.contains("SWEP does not provide compressor"));
|
||||
}
|
||||
other => panic!("Expected InvalidFormat, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swep_object_safety() {
|
||||
let backend: Box<dyn VendorBackend> = Box::new(SwepBackend::new().unwrap());
|
||||
assert_eq!(backend.vendor_name(), "SWEP");
|
||||
let models = backend.list_bphx_models().unwrap();
|
||||
assert!(!models.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user