use std::convert::Infallible;
use uom::{
ConstZero,
si::{
f64::{MassDensity, Pressure, SpecificHeatCapacity, ThermodynamicTemperature},
temperature_interval, thermodynamic_temperature,
},
};
use crate::{
PropertyError, State,
fluid::Stateless,
units::{
SpecificEnthalpy, SpecificEntropy, SpecificGasConstant, SpecificInternalEnergy,
TemperatureDifference,
},
};
use super::{StateFrom, ThermodynamicProperties};
pub trait IdealGasFluid {
fn gas_constant(&self) -> SpecificGasConstant;
fn cp(&self) -> SpecificHeatCapacity;
fn reference_temperature(&self) -> ThermodynamicTemperature;
fn reference_pressure(&self) -> Pressure;
fn reference_enthalpy(&self) -> SpecificEnthalpy {
SpecificEnthalpy::ZERO
}
fn reference_entropy(&self) -> SpecificEntropy {
SpecificEntropy::ZERO
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct IdealGas;
impl IdealGas {
#[must_use]
pub fn reference_state<F: IdealGasFluid>(fluid: F) -> State<F> {
let temperature = fluid.reference_temperature();
let pressure = fluid.reference_pressure();
let density = IdealGas::density(temperature, pressure, fluid.gas_constant());
State {
temperature,
density,
fluid,
}
}
#[must_use]
pub fn pressure(
temperature: ThermodynamicTemperature,
density: MassDensity,
gas_constant: SpecificGasConstant,
) -> Pressure {
density * gas_constant * temperature
}
#[must_use]
pub fn density(
temperature: ThermodynamicTemperature,
pressure: Pressure,
gas_constant: SpecificGasConstant,
) -> MassDensity {
pressure / (gas_constant * temperature)
}
#[must_use]
pub fn temperature(
pressure: Pressure,
density: MassDensity,
gas_constant: SpecificGasConstant,
) -> ThermodynamicTemperature {
let temperature = pressure / (density * gas_constant);
ThermodynamicTemperature::new::<thermodynamic_temperature::kelvin>(
temperature.get::<temperature_interval::kelvin>(),
)
}
}
impl<F: IdealGasFluid> ThermodynamicProperties<F> for IdealGas {
fn pressure(&self, state: &State<F>) -> Result<Pressure, PropertyError> {
let t = state.temperature;
let d = state.density;
let r = state.fluid.gas_constant();
Ok(IdealGas::pressure(t, d, r))
}
fn internal_energy(&self, state: &State<F>) -> Result<SpecificInternalEnergy, PropertyError> {
Ok(self.enthalpy(state)? - state.fluid.gas_constant() * state.temperature)
}
fn enthalpy(&self, state: &State<F>) -> Result<SpecificEnthalpy, PropertyError> {
let cp = state.fluid.cp();
let t_ref = state.fluid.reference_temperature();
let h_ref = state.fluid.reference_enthalpy();
Ok(h_ref + cp * state.temperature.minus(t_ref))
}
fn entropy(&self, state: &State<F>) -> Result<SpecificEntropy, PropertyError> {
let cp = state.fluid.cp();
let r = state.fluid.gas_constant();
let t_ref = state.fluid.reference_temperature();
let p_ref = state.fluid.reference_pressure();
let s_ref = state.fluid.reference_entropy();
let p = self.pressure(state)?;
Ok(s_ref + cp * (state.temperature / t_ref).ln() - r * (p / p_ref).ln())
}
fn cp(&self, state: &State<F>) -> Result<SpecificHeatCapacity, PropertyError> {
Ok(state.fluid.cp())
}
fn cv(&self, state: &State<F>) -> Result<SpecificHeatCapacity, PropertyError> {
Ok(state.fluid.cp() - state.fluid.gas_constant())
}
}
impl<F: IdealGasFluid + Stateless> StateFrom<F, (ThermodynamicTemperature, Pressure)> for IdealGas {
type Error = Infallible;
fn state_from(
&self,
(temperature, pressure): (ThermodynamicTemperature, Pressure),
) -> Result<State<F>, Self::Error> {
let fluid = F::default();
let density = IdealGas::density(temperature, pressure, fluid.gas_constant());
Ok(State {
temperature,
density,
fluid,
})
}
}
impl<F: IdealGasFluid + Stateless> StateFrom<F, (Pressure, MassDensity)> for IdealGas {
type Error = Infallible;
fn state_from(
&self,
(pressure, density): (Pressure, MassDensity),
) -> Result<State<F>, Self::Error> {
let fluid = F::default();
let temperature = IdealGas::temperature(pressure, density, fluid.gas_constant());
Ok(State {
temperature,
density,
fluid,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use uom::si::{
mass_density::pound_per_cubic_foot,
pressure::{atmosphere, kilopascal, pascal, psi},
specific_heat_capacity::joule_per_kilogram_kelvin,
thermodynamic_temperature::degree_celsius,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct MockGas;
impl Stateless for MockGas {}
impl IdealGasFluid for MockGas {
fn gas_constant(&self) -> SpecificGasConstant {
SpecificGasConstant::new::<joule_per_kilogram_kelvin>(400.0)
}
fn cp(&self) -> SpecificHeatCapacity {
SpecificHeatCapacity::new::<joule_per_kilogram_kelvin>(1000.0)
}
fn reference_temperature(&self) -> ThermodynamicTemperature {
ThermodynamicTemperature::new::<degree_celsius>(0.0)
}
fn reference_pressure(&self) -> Pressure {
Pressure::new::<atmosphere>(1.0)
}
}
#[test]
fn basic_properties() {
let state = IdealGas::reference_state(MockGas);
let pressure_in_kpa = IdealGas.pressure(&state).unwrap().get::<kilopascal>();
assert_relative_eq!(pressure_in_kpa, 101.325);
let h_ref = IdealGas.enthalpy(&state).unwrap();
assert_eq!(h_ref, SpecificEnthalpy::ZERO);
}
#[test]
fn increase_temperature_at_constant_density() -> Result<(), PropertyError> {
let temp = ThermodynamicTemperature::new::<degree_celsius>(50.0);
let pres = Pressure::new::<kilopascal>(100.0);
let state_a: State<MockGas> = IdealGas.state_from((temp, pres)).unwrap();
let state_b =
state_a.with_temperature(ThermodynamicTemperature::new::<degree_celsius>(100.0));
let temp_ratio = state_b.temperature / state_a.temperature;
let expected_pressure = IdealGas.pressure(&state_a)? * temp_ratio;
assert_relative_eq!(
IdealGas.pressure(&state_b)?.get::<pascal>(),
expected_pressure.get::<pascal>(),
);
let h_a = IdealGas.enthalpy(&state_a)?;
let h_b = IdealGas.enthalpy(&state_b)?;
assert!(h_b > h_a);
Ok(())
}
#[test]
fn increase_density_at_constant_temperature() -> Result<(), PropertyError> {
let pres = Pressure::new::<psi>(100.0);
let dens = MassDensity::new::<pound_per_cubic_foot>(0.1);
let state_a: State<MockGas> = IdealGas.state_from((pres, dens)).unwrap();
let state_b = state_a.with_density(dens * 2.0);
let expected_pressure = 2.0 * IdealGas.pressure(&state_a)?;
assert_eq!(IdealGas.pressure(&state_b)?, expected_pressure);
let s_a = IdealGas.entropy(&state_a)?;
let s_b = IdealGas.entropy(&state_b)?;
assert!(s_b < s_a);
Ok(())
}
}