use uom::si::f64::Pressure;
use crate::support::{
thermo::{
State,
capability::{HasEnthalpy, HasEntropy, HasPressure, StateFrom, ThermoModel},
},
turbomachinery::{
CompressionWork, InletProperties, IsentropicEfficiency,
compressor::{CompressionError, CompressionResult},
},
units::{SpecificEnthalpy, SpecificEntropy},
};
pub fn isentropic<Fluid, Model>(
inlet: &State<Fluid>,
p_out: Pressure,
eta: IsentropicEfficiency,
thermo: &Model,
) -> Result<CompressionResult<Fluid>, CompressionError<Fluid>>
where
Fluid: Clone,
Model: ThermoModel<Fluid = Fluid>
+ HasPressure
+ HasEnthalpy
+ HasEntropy
+ StateFrom<(Fluid, Pressure, SpecificEnthalpy)>
+ StateFrom<(Fluid, Pressure, SpecificEntropy)>,
{
let p_in = thermo
.pressure(inlet)
.map_err(CompressionError::inlet_pressure_failed)?;
let h_in = thermo
.enthalpy(inlet)
.map_err(CompressionError::inlet_enthalpy_failed)?;
let s_in = thermo
.entropy(inlet)
.map_err(CompressionError::inlet_entropy_failed)?;
let inlet_props = InletProperties {
thermo,
fluid: inlet.fluid.clone(),
p_in,
h_in,
s_in,
};
isentropic_core(inlet_props, p_out, eta)
}
#[allow(clippy::needless_pass_by_value)]
fn isentropic_core<Fluid, Model>(
inlet_props: InletProperties<'_, Fluid, Model>,
p_out: Pressure,
eta: IsentropicEfficiency,
) -> Result<CompressionResult<Fluid>, CompressionError<Fluid>>
where
Model: ThermoModel<Fluid = Fluid>
+ HasEnthalpy
+ StateFrom<(Fluid, Pressure, SpecificEnthalpy)>
+ StateFrom<(Fluid, Pressure, SpecificEntropy)>,
{
let InletProperties {
thermo,
fluid,
p_in,
h_in,
s_in,
} = inlet_props;
if p_out < p_in {
return Err(CompressionError::OutletPressureLessThanInlet { p_in, p_out });
}
let isentropic_outlet = thermo.state_from((fluid, p_out, s_in)).map_err(|source| {
CompressionError::ideal_outlet_state_from_pressure_entropy_failed(p_out, s_in, source)
})?;
let h_out_s = thermo
.enthalpy(&isentropic_outlet)
.map_err(CompressionError::ideal_outlet_enthalpy_failed)?;
let State { fluid, .. } = isentropic_outlet;
let dh_s = h_out_s - h_in;
let dh_actual = dh_s / eta.ratio();
let h_out_target = h_in + dh_actual;
let outlet = thermo
.state_from((fluid, p_out, h_out_target))
.map_err(|source| {
CompressionError::outlet_state_from_pressure_enthalpy_failed(
p_out,
h_out_target,
source,
)
})?;
let raw_work = h_out_target - h_in;
match CompressionWork::new(raw_work) {
Ok(work) => Ok(CompressionResult { outlet, work }),
Err(_) => Err(CompressionError::NonPhysicalWork { outlet, raw_work }),
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use uom::si::{
f64::{MassDensity, Pressure, ThermodynamicTemperature},
mass_density::kilogram_per_cubic_meter,
pressure::{kilopascal, pascal},
specific_heat_capacity::joule_per_kilogram_kelvin,
thermodynamic_temperature::kelvin,
};
use crate::support::{
thermo::{PropertyError, capability::HasPressure},
turbomachinery::{
IsentropicEfficiency,
test_utils::{FakeMode, FakeThermo, MockGas, enth_si, mock_gas_model},
},
units::SpecificEntropy,
};
#[test]
fn normal_compression_matches_expected_work() {
let thermo = mock_gas_model();
let p_in = Pressure::new::<kilopascal>(100.0);
let t_in = ThermodynamicTemperature::new::<kelvin>(300.0);
let inlet: State<MockGas> = thermo.state_from((MockGas, t_in, p_in)).unwrap();
let p_out = Pressure::new::<kilopascal>(12_800.0);
let eta = IsentropicEfficiency::new(0.9).unwrap();
let result = isentropic(&inlet, p_out, eta, &thermo).unwrap();
let expected_work_j_per_kg = 1_000_000.0;
assert_relative_eq!(result.work.quantity().value, expected_work_j_per_kg);
assert_relative_eq!(result.outlet.temperature.get::<kelvin>(), 1300.0);
}
#[test]
fn outlet_pressure_less_than_inlet_is_an_error() {
let thermo = mock_gas_model();
let inlet = State {
temperature: ThermodynamicTemperature::new::<kelvin>(300.0),
density: MassDensity::new::<kilogram_per_cubic_meter>(1.0),
fluid: MockGas,
};
let p_in = thermo.pressure(&inlet).unwrap();
let p_out = p_in - Pressure::new::<pascal>(1.0);
let err = isentropic(
&inlet,
p_out,
IsentropicEfficiency::new(1.0).unwrap(),
&thermo,
)
.unwrap_err();
match err {
CompressionError::OutletPressureLessThanInlet {
p_in: reported_p_in,
p_out: reported_p_out,
} => {
assert_eq!(reported_p_in, p_in);
assert_eq!(reported_p_out, p_out);
}
other => panic!("unexpected error: {other:?}"),
}
}
fn core_inputs() -> (
Pressure,
SpecificEnthalpy,
SpecificEntropy,
Pressure,
IsentropicEfficiency,
) {
let p_in = Pressure::new::<kilopascal>(100.0);
let h_in = enth_si(0.0);
let s_in = SpecificEntropy::new::<joule_per_kilogram_kelvin>(0.0);
let p_out = Pressure::new::<kilopascal>(200.0);
let eta = IsentropicEfficiency::new(1.0).unwrap();
(p_in, h_in, s_in, p_out, eta)
}
#[test]
fn core_state_from_pressure_entropy_failure_is_wrapped() {
let thermo = FakeThermo {
mode: FakeMode::FailStateFromPressureEntropy,
};
let (p_in, h_in, s_in, p_out, eta) = core_inputs();
let inlet_props = InletProperties {
thermo: &thermo,
fluid: MockGas,
p_in,
h_in,
s_in,
};
let err = isentropic_core(inlet_props, p_out, eta).unwrap_err();
match err {
CompressionError::ThermodynamicModelFailed { context, source: _ } => {
assert!(context.contains("ideal_outlet_state_from("));
assert!(context.contains("p_out="));
assert!(context.contains("s_in="));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn core_state_from_pressure_enthalpy_failure_is_wrapped() {
let thermo = FakeThermo {
mode: FakeMode::FailStateFromPressureEnthalpy,
};
let (p_in, h_in, s_in, p_out, eta) = core_inputs();
let inlet_props = InletProperties {
thermo: &thermo,
fluid: MockGas,
p_in,
h_in,
s_in,
};
let err = isentropic_core(inlet_props, p_out, eta).unwrap_err();
match err {
CompressionError::ThermodynamicModelFailed { context, source: _ } => {
assert!(context.contains("outlet_state_from("));
assert!(context.contains("p_out="));
assert!(context.contains("h_out_target="));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn core_property_failure_is_wrapped() {
let thermo = FakeThermo {
mode: FakeMode::FailEnthalpy,
};
let (p_in, h_in, s_in, p_out, eta) = core_inputs();
let inlet_props = InletProperties {
thermo: &thermo,
fluid: MockGas,
p_in,
h_in,
s_in,
};
let err = isentropic_core(inlet_props, p_out, eta).unwrap_err();
match err {
CompressionError::ThermodynamicModelFailed { context, source } => {
assert_eq!(context, "enthalpy(ideal outlet)");
let source = source
.downcast_ref::<PropertyError>()
.expect("expected PropertyError source");
assert!(matches!(source, PropertyError::Calculation { .. }));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn core_non_physical_work_returns_outlet_state() {
let thermo = FakeThermo {
mode: FakeMode::FixedEnthalpy(enth_si(-1.0)),
};
let (p_in, h_in, s_in, p_out, eta) = core_inputs();
let inlet_props = InletProperties {
thermo: &thermo,
fluid: MockGas,
p_in,
h_in,
s_in,
};
let err = isentropic_core(inlet_props, p_out, eta).unwrap_err();
match err {
CompressionError::NonPhysicalWork {
outlet: _,
raw_work,
..
} => {
assert!(raw_work.value < 0.0);
}
other => panic!("unexpected error: {other:?}"),
}
}
}