|
|
|
|
@@ -0,0 +1,418 @@
|
|
|
|
|
//! Bitzer compressor data backend.
|
|
|
|
|
//!
|
|
|
|
|
//! Loads AHRI 540 compressor coefficients from CSV files in the
|
|
|
|
|
//! `data/bitzer/compressors/` directory. The CSV columns `c0`..`c9` (capacity)
|
|
|
|
|
//! and `p0`..`p9` (power) are in AHRI 540 standard polynomial order:
|
|
|
|
|
//! Ts, Td, Ts², Ts·Td, Td², Ts³, Td·Ts², Ts·Td², Td³ (see [`CompressorCoefficients`]).
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
use crate::error::VendorError;
|
|
|
|
|
use crate::vendor_api::{
|
|
|
|
|
BphxParameters, CompressorCoefficients, CompressorValidityRange, UaCalcParams, VendorBackend,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// Backend for Bitzer compressor data.
|
|
|
|
|
///
|
|
|
|
|
/// Discovers compressor models by scanning `*.csv` files in `data/bitzer/compressors/`
|
|
|
|
|
/// and uses the file stem (e.g. `4NFC-20Y`) as the model id. Each CSV row is mapped
|
|
|
|
|
/// to AHRI 540 `CompressorCoefficients`.
|
|
|
|
|
///
|
|
|
|
|
/// # CSV format
|
|
|
|
|
///
|
|
|
|
|
/// Header row with columns: `model`, `manufacturer`, `refrigerant`, `c0`..`c9` (capacity),
|
|
|
|
|
/// `p0`..`p9` (power), `t_suction_min`, `t_suction_max`, `t_discharge_min`, `t_discharge_max`.
|
|
|
|
|
/// One data row per file (one model per CSV file).
|
|
|
|
|
///
|
|
|
|
|
/// # Example
|
|
|
|
|
///
|
|
|
|
|
/// ```no_run
|
|
|
|
|
/// use entropyk_vendors::compressors::bitzer::BitzerBackend;
|
|
|
|
|
/// use entropyk_vendors::VendorBackend;
|
|
|
|
|
///
|
|
|
|
|
/// let backend = BitzerBackend::new().expect("load bitzer data");
|
|
|
|
|
/// let models = backend.list_compressor_models().unwrap();
|
|
|
|
|
/// println!("Available: {:?}", models);
|
|
|
|
|
/// ```
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BitzerBackend {
|
|
|
|
|
/// Root path to the Bitzer 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 BitzerBackend {
|
|
|
|
|
/// Create a new Bitzer 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("bitzer");
|
|
|
|
|
|
|
|
|
|
let mut backend = Self {
|
|
|
|
|
data_path,
|
|
|
|
|
compressor_cache: HashMap::new(),
|
|
|
|
|
sorted_models: Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
backend.load_compressors()?;
|
|
|
|
|
|
|
|
|
|
Ok(backend)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a new Bitzer 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_compressors()?;
|
|
|
|
|
|
|
|
|
|
Ok(backend)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Discover CSV files in `data/bitzer/compressors/` and pre-cache all models.
|
|
|
|
|
fn load_compressors(&mut self) -> Result<(), VendorError> {
|
|
|
|
|
let compressors_dir = self.data_path.join("compressors");
|
|
|
|
|
|
|
|
|
|
let entries = match std::fs::read_dir(&compressors_dir) {
|
|
|
|
|
Ok(e) => e,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
return Err(VendorError::IoError {
|
|
|
|
|
path: compressors_dir.display().to_string(),
|
|
|
|
|
source: e,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for entry in entries.flatten() {
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
if path.extension().is_some_and(|e| e == "csv") {
|
|
|
|
|
if let Some(stem) = path.file_stem() {
|
|
|
|
|
let model = stem.to_string_lossy().into_owned();
|
|
|
|
|
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 Bitzer model {}: {}", model, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.sorted_models.sort();
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load a single compressor model from its CSV 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!("{}.csv", model));
|
|
|
|
|
|
|
|
|
|
let content = std::fs::read_to_string(&model_path).map_err(|e| VendorError::IoError {
|
|
|
|
|
path: model_path.display().to_string(),
|
|
|
|
|
source: e,
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
parse_bitzer_csv(&content, model).map_err(|e| {
|
|
|
|
|
VendorError::InvalidFormat(format!("Parse error in {}: {}", model_path.display(), e))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse a Bitzer CSV string into CompressorCoefficients.
|
|
|
|
|
///
|
|
|
|
|
/// CSV must have a header row and at least one data row (one model per file).
|
|
|
|
|
/// Only the first data row is used; additional rows are ignored.
|
|
|
|
|
/// Columns `c0`..`c9` and `p0`..`p9` are in AHRI 540 order: Ts, Td, Ts², Ts·Td, Td², Ts³, Td·Ts², Ts·Td², Td³.
|
|
|
|
|
/// The `model_id` argument is used as fallback when the CSV `model` column is missing.
|
|
|
|
|
fn parse_bitzer_csv(content: &str, model_id: &str) -> Result<CompressorCoefficients, String> {
|
|
|
|
|
let mut rdr = csv::Reader::from_reader(content.as_bytes());
|
|
|
|
|
let headers: Vec<String> = rdr
|
|
|
|
|
.headers()
|
|
|
|
|
.map_err(|e| e.to_string())?
|
|
|
|
|
.iter()
|
|
|
|
|
.map(String::from)
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let mut record = csv::StringRecord::new();
|
|
|
|
|
if !rdr.read_record(&mut record).map_err(|e| e.to_string())? {
|
|
|
|
|
return Err("CSV has no data row".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let get = |name: &str| -> Result<f64, String> {
|
|
|
|
|
let i = headers
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|h| h == name)
|
|
|
|
|
.ok_or_else(|| format!("missing column {}", name))?;
|
|
|
|
|
record
|
|
|
|
|
.get(i)
|
|
|
|
|
.ok_or_else(|| format!("missing value for {}", name))?
|
|
|
|
|
.trim()
|
|
|
|
|
.parse()
|
|
|
|
|
.map_err(|e: std::num::ParseFloatError| e.to_string())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let get_str = |name: &str| -> Result<String, String> {
|
|
|
|
|
let i = headers
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|h| h == name)
|
|
|
|
|
.ok_or_else(|| format!("missing column {}", name))?;
|
|
|
|
|
record
|
|
|
|
|
.get(i)
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
.ok_or_else(|| format!("missing value for {}", name))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let model = get_str("model").unwrap_or_else(|_| model_id.to_string());
|
|
|
|
|
let manufacturer = get_str("manufacturer").unwrap_or_else(|_| "Bitzer".to_string());
|
|
|
|
|
let refrigerant = get_str("refrigerant").unwrap_or_else(|_| "R134a".to_string());
|
|
|
|
|
|
|
|
|
|
let mut capacity_coeffs = [0.0_f64; 10];
|
|
|
|
|
for (i, c) in capacity_coeffs.iter_mut().enumerate() {
|
|
|
|
|
*c = get(&format!("c{}", i))?;
|
|
|
|
|
}
|
|
|
|
|
let mut power_coeffs = [0.0_f64; 10];
|
|
|
|
|
for (i, p) in power_coeffs.iter_mut().enumerate() {
|
|
|
|
|
*p = get(&format!("p{}", i))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let validity = CompressorValidityRange {
|
|
|
|
|
t_suction_min: get("t_suction_min")?,
|
|
|
|
|
t_suction_max: get("t_suction_max")?,
|
|
|
|
|
t_discharge_min: get("t_discharge_min")?,
|
|
|
|
|
t_discharge_max: get("t_discharge_max")?,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if validity.t_suction_min > validity.t_suction_max {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"Invalid suction temperature range: min ({}) > max ({})",
|
|
|
|
|
validity.t_suction_min, validity.t_suction_max
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if validity.t_discharge_min > validity.t_discharge_max {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"Invalid discharge temperature range: min ({}) > max ({})",
|
|
|
|
|
validity.t_discharge_min, validity.t_discharge_max
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(CompressorCoefficients {
|
|
|
|
|
model,
|
|
|
|
|
manufacturer,
|
|
|
|
|
refrigerant,
|
|
|
|
|
capacity_coeffs,
|
|
|
|
|
power_coeffs,
|
|
|
|
|
mass_flow_coeffs: None,
|
|
|
|
|
validity,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl VendorBackend for BitzerBackend {
|
|
|
|
|
fn vendor_name(&self) -> &str {
|
|
|
|
|
"Bitzer"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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> {
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_bphx_parameters(&self, model: &str) -> Result<BphxParameters, VendorError> {
|
|
|
|
|
Err(VendorError::InvalidFormat(format!(
|
|
|
|
|
"Bitzer does not provide BPHX data (requested: {})",
|
|
|
|
|
model
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_ua(&self, model: &str, _params: &UaCalcParams) -> Result<f64, VendorError> {
|
|
|
|
|
Err(VendorError::InvalidFormat(format!(
|
|
|
|
|
"Bitzer does not provide BPHX/UA data (requested: {})",
|
|
|
|
|
model
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_backend_new() {
|
|
|
|
|
let backend = BitzerBackend::new();
|
|
|
|
|
assert!(backend.is_ok(), "BitzerBackend::new() should succeed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_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 = BitzerBackend::from_path(base_path.join("bitzer"));
|
|
|
|
|
assert!(backend.is_ok(), "BitzerBackend::from_path() should succeed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_vendor_name() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
assert_eq!(backend.vendor_name(), "Bitzer");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_list_compressor_models() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
let models = backend.list_compressor_models().unwrap();
|
|
|
|
|
assert_eq!(models.len(), 2);
|
|
|
|
|
assert!(models.contains(&"4NFC-20Y".to_string()));
|
|
|
|
|
assert!(models.contains(&"4HES-5Y".to_string()));
|
|
|
|
|
assert_eq!(models, vec!["4HES-5Y".to_string(), "4NFC-20Y".to_string()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_get_compressor_4nfc_20y() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
let coeffs = backend
|
|
|
|
|
.get_compressor_coefficients("4NFC-20Y")
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(coeffs.model, "4NFC-20Y");
|
|
|
|
|
assert_eq!(coeffs.manufacturer, "Bitzer");
|
|
|
|
|
assert_eq!(coeffs.refrigerant, "R134a");
|
|
|
|
|
assert_eq!(coeffs.capacity_coeffs.len(), 10);
|
|
|
|
|
assert_eq!(coeffs.power_coeffs.len(), 10);
|
|
|
|
|
assert!((coeffs.capacity_coeffs[0] - 32000.0).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.power_coeffs[0] - 8200.0).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.capacity_coeffs[9] - 0.01).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.power_coeffs[9] - 0.006).abs() < 1e-10);
|
|
|
|
|
assert!(coeffs.mass_flow_coeffs.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_get_compressor_4hes_5y() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
let coeffs = backend.get_compressor_coefficients("4HES-5Y").unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(coeffs.model, "4HES-5Y");
|
|
|
|
|
assert_eq!(coeffs.manufacturer, "Bitzer");
|
|
|
|
|
assert_eq!(coeffs.refrigerant, "R410A");
|
|
|
|
|
assert!((coeffs.capacity_coeffs[0] - 12000.0).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.power_coeffs[0] - 3200.0).abs() < 1e-10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_validity_range() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
let coeffs = backend.get_compressor_coefficients("4NFC-20Y").unwrap();
|
|
|
|
|
|
|
|
|
|
assert!((coeffs.validity.t_suction_min - (-10.0)).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.validity.t_suction_max - 15.0).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.validity.t_discharge_min - 25.0).abs() < 1e-10);
|
|
|
|
|
assert!((coeffs.validity.t_discharge_max - 60.0).abs() < 1e-10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_model_not_found() {
|
|
|
|
|
let backend = BitzerBackend::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_bitzer_list_bphx_empty() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
let models = backend.list_bphx_models().unwrap();
|
|
|
|
|
assert!(models.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_get_bphx_returns_error() {
|
|
|
|
|
let backend = BitzerBackend::new().unwrap();
|
|
|
|
|
let result = backend.get_bphx_parameters("anything");
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
match result.unwrap_err() {
|
|
|
|
|
VendorError::InvalidFormat(msg) => {
|
|
|
|
|
assert!(msg.contains("Bitzer does not provide BPHX"));
|
|
|
|
|
}
|
|
|
|
|
other => panic!("Expected InvalidFormat, got: {:?}", other),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_object_safety() {
|
|
|
|
|
let backend: Box<dyn VendorBackend> = Box::new(BitzerBackend::new().unwrap());
|
|
|
|
|
assert_eq!(backend.vendor_name(), "Bitzer");
|
|
|
|
|
let models = backend.list_compressor_models().unwrap();
|
|
|
|
|
assert!(!models.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_bitzer_csv_multiple_rows_first_used() {
|
|
|
|
|
// When CSV has multiple data rows, only the first is used (one model per file).
|
|
|
|
|
let csv = "model,manufacturer,refrigerant,c0,c1,c2,c3,c4,c5,c6,c7,c8,c9,p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,t_suction_min,t_suction_max,t_discharge_min,t_discharge_max\n\
|
|
|
|
|
4NFC-20Y,Bitzer,R134a,32000.0,580.0,-150.0,4.2,2.5,-5.0,0.06,0.03,-0.02,0.01,8200.0,180.0,70.0,1.8,1.2,1.8,0.025,0.015,0.01,0.006,-10.0,15.0,25.0,60.0\n\
|
|
|
|
|
Other,Bitzer,R410A,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-15.0,15.0,30.0,60.0";
|
|
|
|
|
let result = parse_bitzer_csv(csv, "test");
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
let coeffs = result.unwrap();
|
|
|
|
|
assert_eq!(coeffs.model, "4NFC-20Y");
|
|
|
|
|
assert!((coeffs.capacity_coeffs[0] - 32000.0).abs() < 1e-10);
|
|
|
|
|
}
|
|
|
|
|
}
|