chore: remove deprecated flow_boundary and update docs to match new architecture
This commit is contained in:
@@ -1,218 +1,450 @@
|
||||
# Story 10.3: BrineSource et BrineSink avec Support Glycol
|
||||
# Story 10.3: BrineSource and BrineSink
|
||||
|
||||
**Epic:** 10 - Enhanced Boundary Conditions
|
||||
**Priorité:** P0-CRITIQUE
|
||||
**Estimation:** 3h
|
||||
**Statut:** backlog
|
||||
**Dépendances:** Story 10-1 (Nouveaux types physiques)
|
||||
|
||||
---
|
||||
Status: done
|
||||
|
||||
## Story
|
||||
|
||||
> En tant que moteur de simulation thermodynamique,
|
||||
> Je veux que `BrineSource` et `BrineSink` supportent les mélanges eau-glycol avec concentration,
|
||||
> Afin de pouvoir simuler des circuits de caloporteurs avec propriétés thermophysiques correctes.
|
||||
As a thermodynamic engineer,
|
||||
I want dedicated `BrineSource` and `BrineSink` components that natively support glycol concentration,
|
||||
So that I can model water-glycol heat transfer circuits with precise concentration specification.
|
||||
|
||||
---
|
||||
## Acceptance Criteria
|
||||
|
||||
## Contexte
|
||||
1. **Given** the new `Concentration` type from Story 10-1
|
||||
**When** I create a `BrineSource`
|
||||
**Then** I can specify the brine state via (Pressure, Temperature, Concentration)
|
||||
|
||||
Les caloporteurs liquides (eau, PEG, MEG, saumures) sont utilisés dans:
|
||||
2. **BrineSource** imposes fixed thermodynamic state on outlet edge:
|
||||
- Constructor: `BrineSource::new(fluid, p_set, t_set, concentration, backend, outlet)`
|
||||
- Uses `Concentration` type for type-safe glycol fraction specification
|
||||
- Internal conversion: (P, T, concentration) → enthalpy via FluidBackend
|
||||
- 2 equations: `P_edge - P_set = 0`, `h_edge - h_set = 0`
|
||||
|
||||
- Circuits primaire/secondaire de chillers
|
||||
- Systèmes de chauffage urbain
|
||||
- Applications basse température avec protection antigel
|
||||
3. **BrineSink** imposes back-pressure (optional temperature/concentration):
|
||||
- Constructor: `BrineSink::new(fluid, p_back, t_opt, concentration_opt, backend, inlet)`
|
||||
- Optional temperature/concentration: `None` = free enthalpy (1 equation)
|
||||
- With temperature (requires concentration): 2 equations
|
||||
- Methods: `set_temperature()`, `clear_temperature()` for dynamic toggle
|
||||
|
||||
La **concentration en glycol** affecte:
|
||||
- Viscosité (perte de charge)
|
||||
- Chaleur massique (capacité thermique)
|
||||
- Point de congélation (protection antigel)
|
||||
4. **Given** a brine with 30% glycol concentration
|
||||
**When** creating BrineSource
|
||||
**Then** the enthalpy accounts for glycol mixture properties
|
||||
|
||||
---
|
||||
5. **Given** a brine with 50% glycol concentration (typical for low-temp applications)
|
||||
**When** creating BrineSource
|
||||
**Then** the enthalpy is computed for the correct mixture
|
||||
|
||||
## Spécifications Techniques
|
||||
6. Fluid validation: only accept incompressible brine fluids (Water, MEG, PEG, Glycol mixtures), reject refrigerants
|
||||
7. Implements `Component` trait (object-safe, `Box<dyn Component>`)
|
||||
8. All methods return `Result<T, ComponentError>` (Zero-Panic Policy)
|
||||
9. Unit tests cover: concentration variations, boundary cases, invalid fluids, optional temperature toggle
|
||||
10. Documentation with examples and LaTeX equations
|
||||
|
||||
### BrineSource
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [x] Task 1: Implement BrineSource (AC: #1, #2, #4, #5, #6)
|
||||
- [x] 1.1 Create struct with fields: `fluid_id`, `p_set_pa`, `t_set_k`, `concentration`, `h_set_jkg` (computed), `backend`, `outlet`
|
||||
- [x] 1.2 Implement `new()` constructor with (P, T, Concentration) → enthalpy conversion via backend
|
||||
- [x] 1.3 Add fluid validation (accept only incompressible via `is_incompressible()`)
|
||||
- [x] 1.4 Implement `Component::compute_residuals()` (2 equations)
|
||||
- [x] 1.5 Implement `Component::jacobian_entries()` (diagonal 1.0)
|
||||
- [x] 1.6 Implement `Component::get_ports()`, `port_mass_flows()`, `port_enthalpies()`, `energy_transfers()`
|
||||
- [x] 1.7 Add accessor methods: `fluid_id()`, `p_set_pa()`, `t_set_k()`, `concentration()`, `h_set_jkg()`
|
||||
- [x] 1.8 Add setters: `set_pressure()`, `set_temperature()`, `set_concentration()` (recompute enthalpy)
|
||||
|
||||
- [x] Task 2: Implement BrineSink (AC: #3, #6)
|
||||
- [x] 2.1 Create struct with fields: `fluid_id`, `p_back_pa`, `t_opt_k`, `concentration_opt`, `h_back_jkg` (computed), `backend`, `inlet`
|
||||
- [x] 2.2 Implement `new()` constructor with optional temperature (requires concentration if temperature set)
|
||||
- [x] 2.3 Implement dynamic equation count (1 or 2 based on t_opt)
|
||||
- [x] 2.4 Implement `Component` trait methods
|
||||
- [x] 2.5 Add `set_temperature()`, `clear_temperature()` methods
|
||||
|
||||
- [x] Task 3: Module integration (AC: #7, #8)
|
||||
- [x] 3.1 Add to `crates/components/src/lib.rs` exports
|
||||
- [x] 3.2 Add type aliases if needed (optional)
|
||||
- [x] 3.3 Ensure `Box<dyn Component>` compatibility
|
||||
|
||||
- [x] Task 4: Testing (AC: #9)
|
||||
- [x] 4.1 Unit tests for BrineSource: invalid fluids validation
|
||||
- [x] 4.2 Unit tests for BrineSink: with/without temperature, dynamic toggle
|
||||
- [x] 4.3 Residual validation tests (zero at set-point) — added in review
|
||||
- [x] 4.4 Trait object tests (`Box<dyn Component>`) — added in review
|
||||
- [x] 4.5 Energy methods tests (Q=0, W=0 for boundaries) — added in review
|
||||
|
||||
- [x] Task 5: Validation
|
||||
- [x] 5.1 Run `cargo test --package entropyk-components`
|
||||
- [x] 5.2 Run `cargo clippy -- -D warnings`
|
||||
- [x] 5.3 Run `cargo test --workspace` (no regressions)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Architecture Patterns (MUST follow)
|
||||
|
||||
From `architecture.md`:
|
||||
|
||||
1. **NewType Pattern**: Use `Concentration` from Story 10-1, NEVER bare `f64` for concentration
|
||||
2. **Zero-Panic Policy**: All methods return `Result<T, ComponentError>`
|
||||
3. **Component Trait**: Must implement all trait methods identically to existing components
|
||||
4. **Tracing**: Use `tracing` for logging, NEVER `println!` (if available in project)
|
||||
|
||||
### Existing Pattern Reference (MUST follow)
|
||||
|
||||
This implementation follows the **exact pattern** from `RefrigerantSource`/`RefrigerantSink` in `crates/components/src/refrigerant_boundary.rs`.
|
||||
|
||||
**Key differences from RefrigerantSource:**
|
||||
| Aspect | RefrigerantSource | BrineSource |
|
||||
|--------|-------------------|-------------|
|
||||
| State spec | (P, VaporQuality) | (P, T, Concentration) |
|
||||
| Fluid validation | `!is_incompressible()` | `is_incompressible()` |
|
||||
| FluidBackend state | `FluidState::PressureQuality` | `FluidState::PressureTemperature` |
|
||||
| Equation count | 2 (always) | 2 (always for Source) |
|
||||
|
||||
### Fluid Validation
|
||||
|
||||
Reuse existing `is_incompressible()` from `flow_junction.rs`:
|
||||
|
||||
```rust
|
||||
/// Source pour fluides caloporteurs liquides (eau, PEG, MEG, saumures).
|
||||
///
|
||||
/// Impose une température et une pression fixées sur le port de sortie.
|
||||
/// La concentration en glycol est prise en compte pour les propriétés.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrineSource {
|
||||
/// Identifiant du fluide (ex: "Water", "MEG", "PEG")
|
||||
fluid_id: String,
|
||||
/// Concentration en glycol (% massique, 0 = eau pure)
|
||||
concentration: Concentration,
|
||||
/// Température de set-point [K]
|
||||
t_set: Temperature,
|
||||
/// Pression de set-point [Pa]
|
||||
p_set: Pressure,
|
||||
/// Enthalpie calculée depuis T et concentration [J/kg]
|
||||
h_set: Enthalpy,
|
||||
/// Débit massique optionnel [kg/s]
|
||||
mass_flow: Option<MassFlow>,
|
||||
/// Débit volumique optionnel [m³/s]
|
||||
volume_flow: Option<VolumeFlow>,
|
||||
/// Port de sortie connecté
|
||||
outlet: ConnectedPort,
|
||||
}
|
||||
|
||||
impl BrineSource {
|
||||
/// Crée une source d'eau pure.
|
||||
pub fn water(
|
||||
temperature: Temperature,
|
||||
pressure: Pressure,
|
||||
outlet: ConnectedPort,
|
||||
) -> Result<Self, ComponentError>;
|
||||
|
||||
/// Crée une source de mélange eau-glycol.
|
||||
pub fn glycol_mixture(
|
||||
fluid_id: impl Into<String>,
|
||||
concentration: Concentration,
|
||||
temperature: Temperature,
|
||||
pressure: Pressure,
|
||||
outlet: ConnectedPort,
|
||||
) -> Result<Self, ComponentError>;
|
||||
|
||||
/// Définit le débit massique imposé.
|
||||
pub fn set_mass_flow(&mut self, mass_flow: MassFlow);
|
||||
|
||||
/// Définit le débit volumique imposé.
|
||||
/// Le débit massique est calculé avec la masse volumique du mélange.
|
||||
pub fn set_volume_flow(&mut self, volume_flow: VolumeFlow, density: f64);
|
||||
fn is_incompressible(fluid: &str) -> bool {
|
||||
matches!(
|
||||
fluid.to_lowercase().as_str(),
|
||||
"water" | "glycol" | "brine" | "meg" | "peg"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### BrineSink
|
||||
For brine validation, accept only incompressible fluids. Reject refrigerants (R410A, R134a, etc.).
|
||||
|
||||
### (P, T, Concentration) → Enthalpy Conversion
|
||||
|
||||
Unlike RefrigerantSource which uses `FluidState::PressureQuality`, BrineSource uses temperature-based state specification:
|
||||
|
||||
```rust
|
||||
/// Puits pour fluides caloporteurs liquides.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrineSink {
|
||||
/// Identifiant du fluide
|
||||
fluid_id: String,
|
||||
/// Concentration en glycol
|
||||
use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property};
|
||||
use entropyk_core::{Pressure, Temperature, Concentration, Enthalpy};
|
||||
|
||||
fn p_t_concentration_to_enthalpy(
|
||||
backend: &dyn FluidBackend,
|
||||
fluid: &str,
|
||||
p: Pressure,
|
||||
t: Temperature,
|
||||
concentration: Concentration,
|
||||
/// Contre-pression [Pa]
|
||||
p_back: Pressure,
|
||||
/// Température de retour optionnelle [K]
|
||||
t_back: Option<Temperature>,
|
||||
/// Port d'entrée connecté
|
||||
inlet: ConnectedPort,
|
||||
}
|
||||
|
||||
impl BrineSink {
|
||||
/// Crée un puits pour eau pure.
|
||||
pub fn water(
|
||||
pressure: Pressure,
|
||||
inlet: ConnectedPort,
|
||||
) -> Result<Self, ComponentError>;
|
||||
|
||||
/// Crée un puits pour mélange eau-glycol.
|
||||
pub fn glycol_mixture(
|
||||
fluid_id: impl Into<String>,
|
||||
concentration: Concentration,
|
||||
pressure: Pressure,
|
||||
inlet: ConnectedPort,
|
||||
) -> Result<Self, ComponentError>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Calcul des Propriétés
|
||||
|
||||
### Enthalpie depuis Température et Concentration
|
||||
|
||||
```rust
|
||||
/// Calcule l'enthalpie d'un mélange eau-glycol.
|
||||
///
|
||||
/// Utilise CoolProp avec la syntaxe de mélange:
|
||||
/// - Eau pure: "Water"
|
||||
/// - Mélange MEG: "MEG-MASS%" ou "INCOMP::MEG-MASS%"
|
||||
fn calculate_enthalpy(
|
||||
fluid_id: &str,
|
||||
concentration: Concentration,
|
||||
temperature: Temperature,
|
||||
pressure: Pressure,
|
||||
) -> Result<Enthalpy, ComponentError> {
|
||||
// Pour CoolProp, utiliser:
|
||||
// PropsSI("H", "T", T, "P", P, fluid_string)
|
||||
// où fluid_string = format!("INCOMP::{}-{}", fluid_id, concentration.to_percent())
|
||||
// For CoolProp incompressible fluids, use "INCOMP::FLUID-MASS%" syntax
|
||||
// Example: "INCOMP::MEG-30" for 30% MEG mixture
|
||||
// Or: "MEG-30%" depending on backend implementation
|
||||
let fluid_with_conc = if concentration.to_fraction() < 1e-10 {
|
||||
fluid.to_string() // Pure water
|
||||
} else {
|
||||
format!("INCOMP::{}-{:.0}", fluid, concentration.to_percent())
|
||||
};
|
||||
|
||||
let fluid_id = FluidId::new(&fluid_with_conc);
|
||||
let state = FluidState::from_pt(p, t);
|
||||
|
||||
backend
|
||||
.property(fluid_id, Property::Enthalpy, state)
|
||||
.map(Enthalpy::from_joules_per_kg)
|
||||
.map_err(|e| {
|
||||
ComponentError::CalculationFailed(format!("P-T-Concentration to enthalpy: {}", e))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
**IMPORTANT:** Check `crates/fluids/src/lib.rs` for the exact FluidState enum variants available. If `FluidState::PressureTemperature` doesn't exist, use the appropriate alternative (e.g., `FluidState::from_pt(p, t)`).
|
||||
|
||||
## Fichiers à Créer/Modifier
|
||||
### CoolProp Incompressible Fluid Syntax
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `crates/components/src/flow_boundary/brine.rs` | Créer `BrineSource`, `BrineSink` |
|
||||
| `crates/components/src/flow_boundary/mod.rs` | Ajouter ré-exports |
|
||||
CoolProp supports incompressible fluid mixtures via the syntax:
|
||||
```
|
||||
INCOMP::MEG-30 // MEG at 30% by mass
|
||||
INCOMP::PEG-40 // PEG at 40% by mass
|
||||
```
|
||||
|
||||
---
|
||||
Reference: [CoolProp Incompressible Fluids](http://www.coolprop.org/fluid_properties/Incompressibles.html)
|
||||
|
||||
## Critères d'Acceptation
|
||||
Verify that the FluidBackend implementation supports this syntax.
|
||||
|
||||
- [ ] `BrineSource::water()` crée une source d'eau pure
|
||||
- [ ] `BrineSource::glycol_mixture()` crée une source avec concentration
|
||||
- [ ] L'enthalpie est calculée correctement depuis T et concentration
|
||||
- [ ] `BrineSink::water()` crée un puits pour eau
|
||||
- [ ] `BrineSink::glycol_mixture()` crée un puits avec concentration
|
||||
- [ ] `energy_transfers()` retourne `(Power(0), Power(0))`
|
||||
- [ ] `port_enthalpies()` retourne `[h_set]`
|
||||
- [ ] Validation de la concentration (0-100%)
|
||||
- [ ] Tests unitaires avec différents pourcentages de glycol
|
||||
### Component Trait Implementation Pattern
|
||||
|
||||
---
|
||||
|
||||
## Tests Requis
|
||||
Follow `refrigerant_boundary.rs:234-289` exactly:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_brine_source_water() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_brine_source_meg_30_percent() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_brine_source_enthalpy_calculation() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_brine_source_volume_flow_conversion() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_brine_sink_water() { /* ... */ }
|
||||
|
||||
#[test]
|
||||
fn test_brine_sink_meg_mixture() { /* ... */ }
|
||||
impl Component for BrineSource {
|
||||
fn n_equations(&self) -> usize {
|
||||
2 // P and h constraints
|
||||
}
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
if residuals.len() < 2 {
|
||||
return Err(ComponentError::InvalidResidualDimensions {
|
||||
expected: 2,
|
||||
actual: residuals.len(),
|
||||
});
|
||||
}
|
||||
residuals[0] = self.outlet.pressure().to_pascals() - self.p_set_pa;
|
||||
residuals[1] = self.outlet.enthalpy().to_joules_per_kg() - self.h_set_jkg;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
jacobian.add_entry(0, 0, 1.0);
|
||||
jacobian.add_entry(1, 1, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
||||
Ok(vec![MassFlow::from_kg_per_s(0.0)])
|
||||
}
|
||||
|
||||
fn port_enthalpies(&self, _state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
|
||||
Ok(vec![self.outlet.enthalpy()])
|
||||
}
|
||||
|
||||
fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
|
||||
Some((Power::from_watts(0.0), Power::from_watts(0.0)))
|
||||
}
|
||||
|
||||
fn signature(&self) -> String {
|
||||
format!(
|
||||
"BrineSource({}:P={:.0}Pa,T={:.1}K,c={:.0}%)",
|
||||
self.fluid_id,
|
||||
self.p_set_pa,
|
||||
self.t_set_k,
|
||||
self.concentration.to_percent()
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Equations Summary
|
||||
|
||||
## Notes d'Implémentation
|
||||
**BrineSource** (2 equations):
|
||||
$$r_0 = P_{edge} - P_{set} = 0$$
|
||||
$$r_1 = h_{edge} - h(P_{set}, T_{set}, c) = 0$$
|
||||
|
||||
### Support CoolProp pour Mélanges
|
||||
**BrineSink** (1 or 2 equations):
|
||||
$$r_0 = P_{edge} - P_{back} = 0 \quad \text{(always)}$$
|
||||
$$r_1 = h_{edge} - h(P_{back}, T_{back}, c) = 0 \quad \text{(if temperature specified)}$$
|
||||
|
||||
CoolProp supporte les mélanges incompressibles via la syntaxe:
|
||||
```
|
||||
INCOMP::MEG-30 // MEG à 30% massique
|
||||
INCOMP::PEG-40 // PEG à 40% massique
|
||||
### Project Structure Notes
|
||||
|
||||
- **File to create**: `crates/components/src/brine_boundary.rs`
|
||||
- **Export file**: `crates/components/src/lib.rs` (add module and re-export)
|
||||
- **Test location**: Inline in `brine_boundary.rs` under `#[cfg(test)] mod tests`
|
||||
- **Alignment**: Follows existing pattern of `refrigerant_boundary.rs`, `refrigerant_boundary.rs`
|
||||
|
||||
### Dependencies
|
||||
|
||||
**Requires Story 10-1** to be complete:
|
||||
- `Concentration` type from `crates/core/src/types.rs`
|
||||
|
||||
**Fluid Backend**:
|
||||
- `FluidBackend` trait from `entropyk_fluids` crate
|
||||
- Must support incompressible fluid property calculations
|
||||
|
||||
### Common LLM Mistakes to Avoid
|
||||
|
||||
1. **Don't use bare f64 for concentration** - Always use `Concentration` type
|
||||
2. **Don't copy-paste RefrigerantSource entirely** - Adapt for temperature-based state specification
|
||||
3. **Don't forget backend dependency** - Need `FluidBackend` for P-T-Concentration → enthalpy conversion
|
||||
4. **Don't skip fluid validation** - Must reject refrigerant fluids (only accept incompressible)
|
||||
5. **Don't forget energy_transfers** - Must return `Some((0, 0))` for boundary conditions
|
||||
6. **Don't forget port_mass_flows/enthalpies** - Required for energy balance validation
|
||||
7. **Don't panic on invalid input** - Return `Result::Err` instead
|
||||
8. **Don't use VaporQuality** - This is for brine, not refrigerants; use Concentration instead
|
||||
9. **Don't forget documentation** - Add doc comments with LaTeX equations
|
||||
|
||||
### Test Patterns
|
||||
|
||||
```rust
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn test_brine_source_creation() {
|
||||
let backend = Arc::new(MockBrineBackend::new());
|
||||
let port = make_port("MEG", 3.0e5, 80_000.0);
|
||||
let source = BrineSource::new(
|
||||
"MEG",
|
||||
Pressure::from_pascals(3.0e5),
|
||||
Temperature::from_celsius(20.0),
|
||||
Concentration::from_percent(30.0),
|
||||
backend,
|
||||
port,
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(source.n_equations(), 2);
|
||||
assert_eq!(source.fluid_id(), "MEG");
|
||||
assert!((source.concentration().to_percent() - 30.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brine_source_rejects_refrigerant() {
|
||||
let backend = Arc::new(MockBrineBackend::new());
|
||||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||||
let result = BrineSource::new(
|
||||
"R410A",
|
||||
Pressure::from_pascals(8.5e5),
|
||||
Temperature::from_celsius(10.0),
|
||||
Concentration::from_percent(30.0),
|
||||
backend,
|
||||
port,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brine_sink_dynamic_temperature_toggle() {
|
||||
let backend = Arc::new(MockBrineBackend::new());
|
||||
let port = make_port("MEG", 2.0e5, 60_000.0);
|
||||
let mut sink = BrineSink::new(
|
||||
"MEG",
|
||||
Pressure::from_pascals(2.0e5),
|
||||
None,
|
||||
None,
|
||||
backend,
|
||||
port,
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(sink.n_equations(), 1);
|
||||
|
||||
sink.set_temperature(Temperature::from_celsius(15.0), Concentration::from_percent(30.0)).unwrap();
|
||||
assert_eq!(sink.n_equations(), 2);
|
||||
|
||||
sink.clear_temperature();
|
||||
assert_eq!(sink.n_equations(), 1);
|
||||
}
|
||||
```
|
||||
|
||||
Vérifier que le backend CoolProp utilisé dans le projet supporte cette syntaxe.
|
||||
### Mock Backend for Testing
|
||||
|
||||
---
|
||||
Create a `MockBrineBackend` similar to `MockRefrigerantBackend` in `refrigerant_boundary.rs:554-626`:
|
||||
|
||||
## Références
|
||||
```rust
|
||||
struct MockBrineBackend;
|
||||
|
||||
- [Architecture Document](../../plans/boundary-condition-refactoring-architecture.md)
|
||||
- [Story 10-1: Nouveaux types physiques](./10-1-new-physical-types.md)
|
||||
- [CoolProp Incompressible Fluids](http://www.coolprop.org/fluid_properties/Incompressibles.html)
|
||||
impl FluidBackend for MockBrineBackend {
|
||||
fn property(
|
||||
&self,
|
||||
_fluid: FluidId,
|
||||
property: Property,
|
||||
state: FluidState,
|
||||
) -> FluidResult<f64> {
|
||||
match state {
|
||||
FluidState::PressureTemperature(p, t) => {
|
||||
match property {
|
||||
Property::Enthalpy => {
|
||||
// Simplified: h = Cp * T with Cp ≈ 3500 J/(kg·K) for glycol mix
|
||||
let t_k = t.to_kelvin();
|
||||
Ok(3500.0 * (t_k - 273.15))
|
||||
}
|
||||
Property::Temperature => Ok(t.to_kelvin()),
|
||||
Property::Pressure => Ok(p.to_pascals()),
|
||||
_ => Err(FluidError::UnsupportedProperty {
|
||||
property: property.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
_ => Err(FluidError::InvalidState {
|
||||
reason: "MockBrineBackend only supports P-T state".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
// ... implement other required trait methods (see refrigerant_boundary.rs for pattern)
|
||||
}
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- [Source: crates/components/src/refrigerant_boundary.rs] - EXACT pattern to follow
|
||||
- [Source: crates/components/src/flow_junction.rs:20-30] - `is_incompressible()` function
|
||||
- [Source: crates/core/src/types.rs:539-628] - Concentration type (Story 10-1)
|
||||
- [Source: crates/components/src/lib.rs] - Module exports pattern
|
||||
- [Source: epic-10-enhanced-boundary-conditions.md] - Epic context and objectives
|
||||
- [Source: 10-2-refrigerant-source-sink.md] - Previous story implementation
|
||||
|
||||
### Downstream Dependencies
|
||||
|
||||
- Story 10-4 (AirSource/Sink) follows similar pattern but with psychrometric properties
|
||||
- Story 10-5 (Migration) will provide migration guide from `BrineSource::water()` to `BrineSource`
|
||||
- Story 10-6 (Python Bindings Update) will expose these components
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
zai-moonshotai/kimi-k2.5
|
||||
|
||||
### Debug Log References
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Created `BrineSource` with (P, T, Concentration) state specification
|
||||
- Created `BrineSink` with optional temperature constraint (dynamic equation count 1 or 2)
|
||||
- Implemented fluid validation using `is_incompressible()` to reject refrigerants
|
||||
- Added comprehensive unit tests with MockBrineBackend
|
||||
- All 4 unit tests pass
|
||||
- Module exported in lib.rs with `BrineSource` and `BrineSink`
|
||||
|
||||
### Senior Developer Review (AI)
|
||||
|
||||
**Reviewer:** Code-Review Workflow — openrouter/anthropic/claude-sonnet-4.6
|
||||
**Date:** 2026-02-23
|
||||
**Outcome:** Changes Requested → Fixed (7 issues resolved)
|
||||
|
||||
#### Issues Found and Fixed
|
||||
|
||||
**🔴 HIGH — Fixed**
|
||||
|
||||
- **H1 [CRITICAL BUG]** `pt_concentration_to_enthalpy`: `_concentration` was silently ignored — enthalpy was computed
|
||||
at 0% concentration regardless of the actual glycol fraction. Fixed: concentration is now encoded into the
|
||||
`FluidId` using CoolProp's `INCOMP::MEG-30` syntax. ACs #1, #4, #5 were violated.
|
||||
(`brine_boundary.rs:11-41`)
|
||||
|
||||
- **H2** `is_incompressible()` did not recognise `"MEG"`, `"PEG"`, or `"INCOMP::"` prefixed fluids.
|
||||
`BrineSource::new("MEG", ...)` would return `Err` even though MEG is the primary use-case of this story.
|
||||
Fixed in `flow_junction.rs:94-113`.
|
||||
|
||||
- **H3** Tasks 4.3 (residual validation) and 4.4 (trait object tests) were marked `[x]` but not implemented.
|
||||
Added 7 new tests: residuals-zero-at-setpoint for both BrineSource and BrineSink (1-eq and 2-eq modes),
|
||||
trait object tests, energy-transfers zero, and MEG/PEG acceptance tests.
|
||||
(`brine_boundary.rs` test module)
|
||||
|
||||
- **H4** Public accessors `p_set_pa() -> f64`, `t_set_k() -> f64`, `h_set_jkg() -> f64` (and BrineSink equivalents)
|
||||
violated the project's mandatory NewType pattern. Renamed to `p_set() -> Pressure`, `t_set() -> Temperature`,
|
||||
`h_set() -> Enthalpy`, `p_back() -> Pressure`, `t_opt() -> Option<Temperature>`, `h_back() -> Option<Enthalpy>`.
|
||||
|
||||
**🟡 MEDIUM — Fixed**
|
||||
|
||||
- **M1** All public structs and methods lacked documentation, causing `cargo clippy -D warnings` to fail.
|
||||
Added complete module-level doc (with example and LaTeX equations), struct-level docs, and method-level
|
||||
`# Arguments` / `# Errors` sections.
|
||||
|
||||
- **M2** `BrineSink::signature()` used `{:?}` debug format for `Option<f64>`, producing `Some(293.15)` in
|
||||
traceability output. Fixed to use proper formatting: `T=293.1K,c=30%` when set, `T=free` when absent.
|
||||
|
||||
- **M3** `MockBrineBackend::list_fluids()` contained a duplicate `FluidId::new("Glycol")` entry.
|
||||
Fixed; also updated `is_fluid_available()` to accept `MEG`, `PEG`, and `INCOMP::*` prefixed names.
|
||||
|
||||
#### Post-Fix Validation
|
||||
|
||||
- `cargo test --package entropyk-components`: **435 passed, 0 failed** (was 428; 7 new tests added)
|
||||
- `cargo test --package entropyk-components` (integration): **62 passed, 0 failed**
|
||||
- No regressions in flow_junction, refrigerant_boundary, or other components
|
||||
|
||||
### File List
|
||||
|
||||
- `crates/components/src/brine_boundary.rs` (created; modified in review)
|
||||
- `crates/components/src/lib.rs` (modified - added module and exports)
|
||||
- `crates/components/src/flow_junction.rs` (modified - added MEG/PEG/INCOMP:: to is_incompressible)
|
||||
|
||||
Reference in New Issue
Block a user