mod error;
mod nodes;
mod resolved;
pub use error::SolveError;
pub(super) use nodes::Nodes;
pub(super) use resolved::Resolved;
use super::{
Given, Known, Results,
metrics::{compute_min_delta_t, compute_ua},
traits::{DiscretizedArrangement, DiscretizedHxThermoModel},
};
pub(super) fn solve<Arrangement, TopFluid, BottomFluid, const N: usize>(
known: &Known<TopFluid, BottomFluid>,
given: Given,
thermo_top: &impl DiscretizedHxThermoModel<TopFluid>,
thermo_bottom: &impl DiscretizedHxThermoModel<BottomFluid>,
) -> Result<Results<TopFluid, BottomFluid, N>, SolveError>
where
Arrangement: DiscretizedArrangement + Default,
TopFluid: Clone,
BottomFluid: Clone,
{
const {
assert!(
N >= 2,
"discretized heat exchanger requires at least 2 nodes (inlet and outlet)"
);
};
let resolved = Resolved::new(known, given, thermo_top, thermo_bottom)?;
let nodes = Nodes::new::<Arrangement>(&resolved, thermo_top, thermo_bottom)?;
let min_delta_t = compute_min_delta_t::<Arrangement, _, _, N>(&nodes);
SolveError::check_second_law(&resolved, min_delta_t)?;
let ua = compute_ua(
&Arrangement::default(),
resolved.top.m_dot,
resolved.bottom.m_dot,
resolved.q_dot,
&nodes,
)?;
Ok(Results {
top: nodes.top,
bottom: nodes.bottom,
q_dot: resolved.q_dot,
ua,
min_delta_t,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::hx::HeatFlow;
use approx::assert_relative_eq;
use uom::si::{
f64::{MassRate, Power, ThermodynamicTemperature},
mass_rate::kilogram_per_second,
power::kilowatt,
thermal_conductance::kilowatt_per_kelvin,
thermodynamic_temperature::kelvin,
};
use crate::models::thermal::hx::discretized::core::{
HeatTransferRate, Inlets, MassFlows, PressureDrops,
test_support::{TestThermoModel, state},
};
use crate::support::hx::{
CapacitanceRate, Stream, StreamInlet,
arrangement::{CounterFlow, ParallelFlow},
functional,
};
#[test]
fn rejects_second_law_violation() {
let model = TestThermoModel::new();
let known = Known {
inlets: Inlets {
top: state(300.0), bottom: state(400.0), },
m_dot: MassFlows::new_unchecked(
MassRate::new::<kilogram_per_second>(1.0),
MassRate::new::<kilogram_per_second>(1.0),
),
dp: PressureDrops::default(),
};
let q_dot = HeatTransferRate::TopToBottom(Power::new::<kilowatt>(10.0));
let result =
solve::<CounterFlow, _, _, 5>(&known, Given::HeatTransferRate(q_dot), &model, &model);
match result {
Err(SolveError::SecondLawViolation {
q_dot, min_delta_t, ..
}) => {
assert_relative_eq!(q_dot.get::<kilowatt>(), 10.0);
let delta_t_kelvin = min_delta_t.get::<uom::si::temperature_interval::kelvin>();
assert!(
delta_t_kelvin > 0.0 && delta_t_kelvin.is_finite(),
"min_delta_t should be positive and finite, got: {delta_t_kelvin}"
);
}
other => panic!("Expected SecondLawViolation, got: {other:?}"),
}
}
#[test]
fn rejects_temperature_crossover() {
let model = TestThermoModel::new();
let known = Known {
inlets: Inlets {
top: state(400.0),
bottom: state(300.0),
},
m_dot: MassFlows::new_unchecked(
MassRate::new::<kilogram_per_second>(1.0),
MassRate::new::<kilogram_per_second>(1.0),
),
dp: PressureDrops::default(),
};
let result = solve::<CounterFlow, _, _, 5>(
&known,
Given::TopOutletTemp(ThermodynamicTemperature::new::<kelvin>(200.0)),
&model,
&model,
);
match result {
Err(SolveError::SecondLawViolation {
min_delta_t,
violation_node,
..
}) => {
let delta_t_kelvin = min_delta_t.get::<uom::si::temperature_interval::kelvin>();
assert!(
delta_t_kelvin <= 0.0,
"min_delta_t should be non-positive for temperature crossover, got: {delta_t_kelvin}"
);
assert!(
violation_node.is_some(),
"violation_node should be reported for temperature crossover"
);
}
other => panic!("Expected SecondLawViolation, got: {other:?}"),
}
}
#[test]
fn counterflow_ua_matches_functional_solver() {
let model = TestThermoModel::new();
let m_dot_top = MassRate::new::<kilogram_per_second>(2.0);
let m_dot_bottom = MassRate::new::<kilogram_per_second>(3.0);
let known = Known {
inlets: Inlets {
top: state(400.0),
bottom: state(300.0),
},
m_dot: MassFlows::new_unchecked(m_dot_top, m_dot_bottom),
dp: PressureDrops::default(),
};
let q_dot = HeatTransferRate::TopToBottom(Power::new::<kilowatt>(60.0));
let result =
solve::<CounterFlow, _, _, 5>(&known, Given::HeatTransferRate(q_dot), &model, &model)
.expect("discretized solve should succeed");
assert_relative_eq!(result.top[4].temperature.get::<kelvin>(), 370.0);
assert_relative_eq!(result.bottom[0].temperature.get::<kelvin>(), 320.0);
let functional_result = functional::known_conditions_and_inlets(
&CounterFlow,
(
StreamInlet::new(
CapacitanceRate::from_quantity(m_dot_top * model.cp()).unwrap(),
known.inlets.top.temperature,
),
Stream::new_from_heat_flow(
CapacitanceRate::from_quantity(m_dot_bottom * model.cp()).unwrap(),
known.inlets.bottom.temperature,
HeatFlow::outgoing(q_dot.magnitude()).unwrap(),
),
),
)
.expect("functional solve should succeed");
assert_relative_eq!(
result.ua.get::<kilowatt_per_kelvin>(),
functional_result.ua.get::<kilowatt_per_kelvin>(),
epsilon = 1e-12,
);
}
#[test]
fn parallel_flow_ua_matches_functional_solver() {
let model = TestThermoModel::new();
let m_dot_top = MassRate::new::<kilogram_per_second>(2.0);
let m_dot_bottom = MassRate::new::<kilogram_per_second>(3.0);
let known = Known {
inlets: Inlets {
top: state(400.0),
bottom: state(300.0),
},
m_dot: MassFlows::new_unchecked(m_dot_top, m_dot_bottom),
dp: PressureDrops::default(),
};
let q_dot = HeatTransferRate::TopToBottom(Power::new::<kilowatt>(60.0));
let result =
solve::<ParallelFlow, _, _, 5>(&known, Given::HeatTransferRate(q_dot), &model, &model)
.expect("discretized solve should succeed");
assert_relative_eq!(result.top[4].temperature.get::<kelvin>(), 370.0);
assert_relative_eq!(result.bottom[4].temperature.get::<kelvin>(), 320.0);
let functional_result = functional::known_conditions_and_inlets(
&ParallelFlow,
(
StreamInlet::new(
CapacitanceRate::from_quantity(m_dot_top * model.cp()).unwrap(),
known.inlets.top.temperature,
),
Stream::new_from_heat_flow(
CapacitanceRate::from_quantity(m_dot_bottom * model.cp()).unwrap(),
known.inlets.bottom.temperature,
HeatFlow::outgoing(q_dot.magnitude()).unwrap(),
),
),
)
.expect("functional solve should succeed");
assert_relative_eq!(
result.ua.get::<kilowatt_per_kelvin>(),
functional_result.ua.get::<kilowatt_per_kelvin>(),
);
}
}