chore: sync project state and current artifacts
This commit is contained in:
432
_bmad-output/implementation-artifacts/11-1-node-passive-probe.md
Normal file
432
_bmad-output/implementation-artifacts/11-1-node-passive-probe.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Story 11.1: Node - Sonde Passive
|
||||
|
||||
**Epic:** 11 - Advanced HVAC Components
|
||||
**Priorité:** P0-CRITIQUE
|
||||
**Estimation:** 4h
|
||||
**Statut:** done
|
||||
**Dépendances:** Epic 9 (Coherence Corrections)
|
||||
|
||||
---
|
||||
|
||||
## Story
|
||||
|
||||
> En tant que modélisateur de systèmes thermodynamiques,
|
||||
> Je veux un composant Node passif (0 équations),
|
||||
> Afin de pouvoir extraire P, h, T, titre, surchauffe, sous-refroidissement à n'importe quel point du circuit.
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
Actuellement, il n'existe pas de moyen simple d'extraire des mesures à un point donné du circuit sans affecter le système d'équations. Les composants existants (FlowSplitter, FlowMerger) ajoutent des équations et ne sont pas conçus comme des sondes.
|
||||
|
||||
**Besoin métier:**
|
||||
- Extraire la surchauffe après l'évaporateur
|
||||
- Mesurer le sous-refroidissement après le condenseur
|
||||
- Obtenir la température en un point quelconque
|
||||
- Servir de point de jonction dans la topologie sans ajouter de contraintes
|
||||
|
||||
---
|
||||
|
||||
## Solution Proposée
|
||||
|
||||
### Composant Node
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
in ───►│ Node │───► out
|
||||
└─────────┘
|
||||
|
||||
0 équations (passif)
|
||||
|
||||
Mesures disponibles:
|
||||
- pressure (Pa)
|
||||
- temperature (K)
|
||||
- enthalpy (J/kg)
|
||||
- quality (-) [si diphasique]
|
||||
- superheat (K) [si surchauffé]
|
||||
- subcooling (K) [si sous-refroidi]
|
||||
- mass_flow (kg/s)
|
||||
- saturation_temp (K)
|
||||
- phase (SubcooledLiquid|TwoPhase|SuperheatedVapor|Supercritical)
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
```rust
|
||||
// crates/components/src/node.rs
|
||||
|
||||
use entropyk_core::{Pressure, Temperature, Enthalpy, MassFlow, Power};
|
||||
use entropyk_fluids::{FluidBackend, FluidId};
|
||||
use crate::{Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Node - Sonde passive pour extraction de mesures
|
||||
#[derive(Debug)]
|
||||
pub struct Node {
|
||||
name: String,
|
||||
inlet: ConnectedPort,
|
||||
outlet: ConnectedPort,
|
||||
fluid_backend: Option<Arc<dyn FluidBackend>>,
|
||||
measurements: NodeMeasurements,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NodeMeasurements {
|
||||
pub pressure: f64,
|
||||
pub temperature: f64,
|
||||
pub enthalpy: f64,
|
||||
pub entropy: Option<f64>,
|
||||
pub quality: Option<f64>,
|
||||
pub superheat: Option<f64>,
|
||||
pub subcooling: Option<f64>,
|
||||
pub mass_flow: f64,
|
||||
pub saturation_temp: Option<f64>,
|
||||
pub phase: Option<Phase>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Phase {
|
||||
SubcooledLiquid,
|
||||
TwoPhase,
|
||||
SuperheatedVapor,
|
||||
Supercritical,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à Créer/Modifier
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `crates/components/src/node.rs` | Créer |
|
||||
| `crates/components/src/lib.rs` | Ajouter `mod node; pub use node::*` |
|
||||
|
||||
---
|
||||
|
||||
## Implémentation Détaillée
|
||||
|
||||
### Constructeurs
|
||||
|
||||
```rust
|
||||
impl Node {
|
||||
/// Crée une sonde passive simple
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
inlet: ConnectedPort,
|
||||
outlet: ConnectedPort,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
inlet,
|
||||
outlet,
|
||||
fluid_backend: None,
|
||||
measurements: NodeMeasurements::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un backend fluide pour calculs avancés
|
||||
pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
|
||||
self.fluid_backend = Some(backend);
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Méthodes d'accès
|
||||
|
||||
```rust
|
||||
impl Node {
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
pub fn pressure(&self) -> f64 { self.measurements.pressure }
|
||||
pub fn temperature(&self) -> f64 { self.measurements.temperature }
|
||||
pub fn enthalpy(&self) -> f64 { self.measurements.enthalpy }
|
||||
pub fn quality(&self) -> Option<f64> { self.measurements.quality }
|
||||
pub fn superheat(&self) -> Option<f64> { self.measurements.superheat }
|
||||
pub fn subcooling(&self) -> Option<f64> { self.measurements.subcooling }
|
||||
pub fn mass_flow(&self) -> f64 { self.measurements.mass_flow }
|
||||
pub fn measurements(&self) -> &NodeMeasurements { &self.measurements }
|
||||
}
|
||||
```
|
||||
|
||||
### Implémentation Component
|
||||
|
||||
```rust
|
||||
impl Component for Node {
|
||||
fn n_equations(&self) -> usize { 0 } // Passif!
|
||||
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
_residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(()) // Pas de résidus
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
_jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(()) // Pas de Jacobien
|
||||
}
|
||||
|
||||
fn post_solve(&mut self, state: &SystemState) -> Result<(), ComponentError> {
|
||||
self.update_measurements(state)
|
||||
}
|
||||
|
||||
fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
|
||||
Some((Power::from_watts(0.0), Power::from_watts(0.0)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Calcul des mesures avancées
|
||||
|
||||
```rust
|
||||
impl Node {
|
||||
pub fn update_measurements(&mut self, state: &SystemState) -> Result<(), ComponentError> {
|
||||
// Extraction des valeurs de base
|
||||
self.measurements.pressure = self.inlet.pressure().to_pascals();
|
||||
self.measurements.enthalpy = self.inlet.enthalpy().to_joules_per_kg();
|
||||
self.measurements.mass_flow = self.inlet.mass_flow().to_kg_per_s();
|
||||
|
||||
// Calculs avancés si backend disponible
|
||||
if let Some(ref backend) = self.fluid_backend {
|
||||
if let Some(ref fluid_id) = self.inlet.fluid_id() {
|
||||
self.compute_advanced_measurements(backend, fluid_id)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_advanced_measurements(
|
||||
&mut self,
|
||||
backend: &dyn FluidBackend,
|
||||
fluid_id: &FluidId,
|
||||
) -> Result<(), ComponentError> {
|
||||
let p = self.measurements.pressure;
|
||||
let h = self.measurements.enthalpy;
|
||||
|
||||
// Température
|
||||
self.measurements.temperature = backend.temperature_ph(fluid_id, p, h)?;
|
||||
|
||||
// Propriétés de saturation
|
||||
let h_sat_l = backend.enthalpy_px(fluid_id, p, 0.0).ok();
|
||||
let h_sat_v = backend.enthalpy_px(fluid_id, p, 1.0).ok();
|
||||
let t_sat = backend.saturation_temperature(fluid_id, p).ok();
|
||||
|
||||
self.measurements.saturation_temp = t_sat;
|
||||
|
||||
// Détermination de la phase
|
||||
if let (Some(h_l), Some(h_v), Some(_t_sat)) = (h_sat_l, h_sat_v, t_sat) {
|
||||
if h <= h_l {
|
||||
// Liquide sous-refroidi
|
||||
self.measurements.phase = Some(Phase::SubcooledLiquid);
|
||||
self.measurements.quality = None;
|
||||
let cp_l = backend.cp_ph(fluid_id, p, h_l).unwrap_or(4180.0);
|
||||
self.measurements.subcooling = Some((h_l - h) / cp_l);
|
||||
self.measurements.superheat = None;
|
||||
} else if h >= h_v {
|
||||
// Vapeur surchauffée
|
||||
self.measurements.phase = Some(Phase::SuperheatedVapor);
|
||||
self.measurements.quality = None;
|
||||
let cp_v = backend.cp_ph(fluid_id, p, h_v).unwrap_or(1000.0);
|
||||
self.measurements.superheat = Some((h - h_v) / cp_v);
|
||||
self.measurements.subcooling = None;
|
||||
} else {
|
||||
// Zone diphasique
|
||||
self.measurements.phase = Some(Phase::TwoPhase);
|
||||
self.measurements.quality = Some((h - h_l) / (h_v - h_l));
|
||||
self.measurements.superheat = None;
|
||||
self.measurements.subcooling = None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Critères d'Acceptation
|
||||
|
||||
- [ ] `Node::n_equations()` retourne `0`
|
||||
- [ ] `Node::compute_residuals()` ne modifie pas les résidus
|
||||
- [ ] `Node::post_solve()` met à jour les mesures
|
||||
- [ ] `pressure()`, `temperature()`, `enthalpy()`, `mass_flow()` retournent les valeurs du port
|
||||
- [ ] `quality()` retourne `Some(x)` en zone diphasique, `None` sinon
|
||||
- [ ] `superheat()` retourne `Some(SH)` si surchauffé, `None` sinon
|
||||
- [ ] `subcooling()` retourne `Some(SC)` si sous-refroidi, `None` sinon
|
||||
- [ ] `energy_transfers()` retourne `(Power(0), Power(0))`
|
||||
- [ ] Node peut être inséré dans la topologie entre deux composants
|
||||
- [ ] Node fonctionne sans backend (mesures de base uniquement)
|
||||
|
||||
---
|
||||
|
||||
## Tests Requis
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_node_zero_equations() {
|
||||
let node = Node::new("test", inlet, outlet);
|
||||
assert_eq!(node.n_equations(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_no_residuals() {
|
||||
let node = Node::new("test", inlet, outlet);
|
||||
let state = SystemState::default();
|
||||
let mut residuals = ResidualVector::new(10);
|
||||
|
||||
node.compute_residuals(&state, &mut residuals).unwrap();
|
||||
|
||||
// Aucun résidu modifié
|
||||
assert!(residuals.iter().all(|&r| r == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_extract_pressure() {
|
||||
let mut node = Node::new("test", inlet, outlet);
|
||||
// Configurer port avec P = 300 kPa
|
||||
|
||||
node.update_measurements(&state).unwrap();
|
||||
|
||||
assert!((node.pressure() - 300_000.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_superheat_calculation() {
|
||||
// Test avec R410A surchauffé
|
||||
let backend = CoolPropBackend::new();
|
||||
let mut node = Node::new("evap_out", inlet, outlet)
|
||||
.with_fluid_backend(Arc::new(backend));
|
||||
|
||||
// Configurer: P = 10 bar, T = 15°C (surchauffe ~5K)
|
||||
node.update_measurements(&state).unwrap();
|
||||
|
||||
assert!(node.superheat().is_some());
|
||||
assert!(node.superheat().unwrap() > 0.0);
|
||||
assert!(node.subcooling().is_none());
|
||||
assert_eq!(node.measurements().phase, Some(Phase::SuperheatedVapor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_subcooling_calculation() {
|
||||
// Test avec R410A sous-refroidi
|
||||
let backend = CoolPropBackend::new();
|
||||
let mut node = Node::new("cond_out", inlet, outlet)
|
||||
.with_fluid_backend(Arc::new(backend));
|
||||
|
||||
// Configurer: P = 25 bar, T = 40°C (sous-refroidissement ~5K)
|
||||
node.update_measurements(&state).unwrap();
|
||||
|
||||
assert!(node.subcooling().is_some());
|
||||
assert!(node.subcooling().unwrap() > 0.0);
|
||||
assert!(node.superheat().is_none());
|
||||
assert_eq!(node.measurements().phase, Some(Phase::SubcooledLiquid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_two_phase_quality() {
|
||||
// Test avec R410A diphasique
|
||||
let backend = CoolPropBackend::new();
|
||||
let mut node = Node::new("mid_evap", inlet, outlet)
|
||||
.with_fluid_backend(Arc::new(backend));
|
||||
|
||||
// Configurer: P = 10 bar, x = 0.5
|
||||
node.update_measurements(&state).unwrap();
|
||||
|
||||
assert!(node.quality().is_some());
|
||||
assert!((node.quality().unwrap() - 0.5).abs() < 0.1);
|
||||
assert!(node.superheat().is_none());
|
||||
assert!(node.subcooling().is_none());
|
||||
assert_eq!(node.measurements().phase, Some(Phase::TwoPhase));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_no_backend_graceful() {
|
||||
// Test sans backend - mesures de base uniquement
|
||||
let mut node = Node::new("test", inlet, outlet);
|
||||
// Pas de with_fluid_backend()
|
||||
|
||||
node.update_measurements(&state).unwrap();
|
||||
|
||||
// Mesures de base disponibles
|
||||
assert!(node.pressure() > 0.0);
|
||||
assert!(node.mass_flow() > 0.0);
|
||||
|
||||
// Mesures avancées non disponibles
|
||||
assert!(node.quality().is_none());
|
||||
assert!(node.superheat().is_none());
|
||||
assert!(node.subcooling().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_in_topology() {
|
||||
// Test que Node peut être inséré dans la topologie
|
||||
let mut system = System::new();
|
||||
|
||||
let comp = system.add_component(Box::new(Compressor::new(...)));
|
||||
let node = system.add_component(Box::new(Node::new("probe", ...)));
|
||||
let cond = system.add_component(Box::new(Condenser::new(...)));
|
||||
|
||||
// Connecter: comp → node → cond
|
||||
system.connect(comp_outlet, node_inlet).unwrap();
|
||||
system.connect(node_outlet, cond_inlet).unwrap();
|
||||
|
||||
// Le système doit avoir le même nombre d'équations
|
||||
// (Node n'ajoute pas d'équations)
|
||||
system.finalize().unwrap();
|
||||
|
||||
// Solve devrait fonctionner normalement
|
||||
let result = system.solve();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example d'Utilisation
|
||||
|
||||
```rust
|
||||
use entropyk_components::Node;
|
||||
use entropyk_fluids::CoolPropBackend;
|
||||
|
||||
// Créer une sonde après l'évaporateur
|
||||
let backend = Arc::new(CoolPropBackend::new());
|
||||
|
||||
let probe = Node::new(
|
||||
"evaporator_outlet",
|
||||
evaporator.outlet_port(),
|
||||
compressor.inlet_port(),
|
||||
)
|
||||
.with_fluid_backend(backend);
|
||||
|
||||
// Après convergence
|
||||
let t_sh = probe.superheat().expect("Should be superheated");
|
||||
println!("Superheat: {:.1} K", t_sh);
|
||||
|
||||
let p = probe.pressure();
|
||||
let t = probe.temperature();
|
||||
let m = probe.mass_flow();
|
||||
|
||||
println!("P = {:.2} bar, T = {:.1}°C, m = {:.3} kg/s",
|
||||
p / 1e5, t - 273.15, m);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Références
|
||||
|
||||
- [Epic 11 Technical Specifications](../planning-artifacts/epic-11-technical-specifications.md)
|
||||
- [Story 7.2 - Energy Balance Validation](./7-2-energy-balance-validation.md)
|
||||
- [Component Trait](./1-1-component-trait-definition.md)
|
||||
Reference in New Issue
Block a user