use crate::{
process::{ContinuousVariable, GeneralConstraint, ProcessModel},
ChemicalRecord,
};
use feos_ad::{HelmholtzEnergyWrapper, PhaseEquilibriumAD, StateAD, TotalHelmholtzEnergy};
use feos_core::{DensityInitialization, EosResult, ReferenceSystem};
use nalgebra::SVector;
use num_dual::DualNum;
use quantity::{
HeatCapacityRate, MoleFlowRate, Power, Pressure, Temperature, BAR, CELSIUS, KELVIN, KILO, MEGA,
WATT,
};
struct PressureChanger<'a, E: TotalHelmholtzEnergy<1>, D: DualNum<f64> + Copy> {
inlet: &'a StateAD<'a, E, D, 1>,
outlet: StateAD<'a, E, D, 1>,
}
impl<'a, E: TotalHelmholtzEnergy<1>, D: DualNum<f64> + Copy> PressureChanger<'a, E, D> {
fn pump(
inlet: &'a StateAD<'a, E, D, 1>,
pressure: Pressure<D>,
efficiency: f64,
) -> EosResult<Self> {
let state2s = StateAD::new_ps(
inlet.eos,
pressure,
inlet.molar_entropy(),
inlet.molefracs,
DensityInitialization::Liquid,
Some(inlet.temperature.re()),
)?;
let h1 = inlet.molar_enthalpy();
let h2s = state2s.molar_enthalpy();
let h2 = h1 + (h2s - h1) / efficiency;
let outlet = StateAD::new_ph(
inlet.eos,
pressure,
h2,
inlet.molefracs,
DensityInitialization::Liquid,
Some(state2s.temperature.re()),
)?;
Ok(Self { inlet, outlet })
}
fn turbine(
inlet: &'a StateAD<'a, E, D, 1>,
pressure: Pressure<D>,
vle_condenser: &PhaseEquilibriumAD<'a, E, D, 1>,
efficiency: f64,
) -> EosResult<Self> {
let s1 = inlet.molar_entropy();
let s_v = vle_condenser.vapor.molar_entropy();
let h2s = if s1.re() < s_v.re() {
let s_l = vle_condenser.liquid.molar_entropy();
let h_l = vle_condenser.liquid.molar_enthalpy();
let h_v = vle_condenser.vapor.molar_enthalpy();
let x = (s1 - s_l) / (s_v - s_l);
h_l + x * (h_v - h_l)
} else {
StateAD::new_ps(
inlet.eos,
pressure,
s1,
inlet.molefracs,
DensityInitialization::Vapor,
Some(inlet.temperature.re()),
)?
.molar_enthalpy()
};
let h1 = inlet.molar_enthalpy();
let h2 = h1 + (h2s - h1) * efficiency;
let outlet = StateAD::new_ph(
inlet.eos,
pressure,
h2,
inlet.molefracs,
DensityInitialization::Vapor,
Some(inlet.temperature.re()),
)?;
Ok(Self { inlet, outlet })
}
fn power(&self, flow_rate: MoleFlowRate<D>) -> Power<D> {
flow_rate * (self.outlet.molar_enthalpy() - self.inlet.molar_enthalpy())
}
}
pub struct OrganicRankineCycle {
c_p_hs: HeatCapacityRate,
t_hs: Temperature,
dt_hs: Temperature,
eta_st: f64,
eta_sp: f64,
p_min: Pressure,
p_max: Pressure,
p_min_r: f64,
p_max_r: f64,
t_cool_in: Temperature,
t_cool_out: Temperature,
dt_cool: Temperature,
}
impl<E: TotalHelmholtzEnergy<1>> ProcessModel<E, 3, 1> for OrganicRankineCycle {
fn variables(&self) -> [ContinuousVariable; 3] {
[
ContinuousVariable::new(298.15 / 300.0, 400.15 / 300.0, 1.0),
ContinuousVariable::new(298.15 / 300.0, 400.15 / 300.0, 1.2),
ContinuousVariable::new(0.0, 1.0, 0.1),
]
}
fn constraints(&self) -> Vec<GeneralConstraint> {
vec![
GeneralConstraint::Inequality(Some(1.0), None),
GeneralConstraint::Inequality(Some(1.0), None),
GeneralConstraint::Inequality(Some(self.p_min_r), Some(self.p_max_r)),
GeneralConstraint::Inequality(Some(self.p_min_r), Some(self.p_max_r)),
GeneralConstraint::Inequality(
Some(self.p_min.convert_into(BAR)),
Some(self.p_max.convert_into(BAR)),
),
GeneralConstraint::Inequality(
Some(self.p_min.convert_into(BAR)),
Some(self.p_max.convert_into(BAR)),
),
GeneralConstraint::Inequality(Some(1.0), None),
GeneralConstraint::Inequality(Some(1.0), None),
]
}
fn evaluate<D: DualNum<f64> + Copy>(
&self,
eos: &HelmholtzEnergyWrapper<E, D, 1>,
_: [&ChemicalRecord<D>; 1],
x: [D; 3],
) -> EosResult<(D, Vec<D>)> {
let [t_cond, t_evap, dt_sh] = x;
let t_cond = Temperature::from_reduced(t_cond * 300.0);
let t_evap = Temperature::from_reduced(t_evap * 300.0);
let dt_sh = Temperature::from_reduced(dt_sh * 50.0);
let molefracs = SVector::from([D::one()]);
let (vle_cond, p_cond) = PhaseEquilibriumAD::new_t(eos, t_cond)?;
let (vle_evap, p_evap) = PhaseEquilibriumAD::new_t(eos, t_evap)?;
let pump = PressureChanger::pump(&vle_cond.liquid, p_evap, self.eta_sp)?;
let turbine_in = StateAD::new_tp(
eos,
t_evap + dt_sh,
p_evap,
molefracs,
DensityInitialization::Vapor,
)?;
let turbine = PressureChanger::turbine(&turbine_in, p_cond, &vle_cond, self.eta_st)?;
let t_hs_pinch = vle_evap.liquid.temperature + self.dt_hs;
let m_wf = (t_hs_pinch - self.t_hs) * self.c_p_hs
/ (vle_evap.liquid.molar_enthalpy() - turbine_in.molar_enthalpy());
let target = (pump.power(m_wf) + turbine.power(m_wf)).convert_into(MEGA * WATT);
let pinch_hs = ((-turbine_in.temperature + self.t_hs) / self.dt_hs).into_value();
let h1 = vle_cond.liquid.molar_enthalpy();
let h2 = vle_cond.vapor.molar_enthalpy();
let h3 = turbine.outlet.molar_enthalpy();
let t_cool_pinch =
(h2 - h1) / (h3 - h1) * (self.t_cool_out - self.t_cool_in) + self.t_cool_in;
let pinch_cond = ((t_cool_pinch - t_cond) / self.dt_cool).into_value();
let p_crit = StateAD::critical_point_pure(eos)?.pressure();
let pr_cond = p_cond.convert_into(p_crit);
let pr_evap = p_evap.convert_into(p_crit);
let pa_cond = p_cond.convert_into(BAR);
let pa_evap = p_evap.convert_into(BAR);
let p_const = p_evap.convert_into(p_cond);
let turb_out = turbine.outlet.temperature.convert_into(t_cond);
let constraints = vec![
pinch_hs, pinch_cond, pr_cond, pr_evap, pa_cond, pa_evap, p_const, turb_out,
];
Ok((target, constraints))
}
}
impl Default for OrganicRankineCycle {
fn default() -> Self {
Self {
c_p_hs: 65.0 * KILO * WATT / KELVIN,
t_hs: 175.0 * CELSIUS,
dt_hs: 20.0 * KELVIN,
eta_st: 0.65,
eta_sp: 0.8,
p_min: BAR,
p_max: 50.0 * BAR,
p_min_r: 1e-3,
p_max_r: 0.8,
t_cool_in: 25. * CELSIUS,
t_cool_out: 40.0 * CELSIUS,
dt_cool: -10.0 * KELVIN,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::assert_relative_eq;
use feos_ad::eos::{Joback, PcSaftPure};
use feos_ad::{EquationOfStateAD, ParametersAD};
use feos_core::EosResult;
use std::collections::HashMap;
#[test]
fn test_orc() -> EosResult<()> {
let pcsaft = PcSaftPure([
1.93988,
3.229270435095473,
225.21099088603418,
0.0,
0.0,
0.0,
0.0,
0.0,
]);
let joback = Joback([
13.939999999999998,
0.19491999999999998,
-0.00013533000000000002,
0.000000042889999999999996,
0.0,
]);
let eos = EquationOfStateAD::new([joback], pcsaft).wrap();
let x0 = [1.0, 1.2, 0.1];
let orc = OrganicRankineCycle::default();
let cr = ChemicalRecord::new(HashMap::new(), HashMap::new());
let (obj, con) = orc.evaluate(&eos, [&cr], x0)?;
println!("{obj} {con:?}");
let ref_obj = -0.601275463470434;
let ref_con = [
4.157499999999999,
-1.275381108337291,
0.13413664979435497,
0.5086603251144651,
9.56756117623695,
36.281201192353244,
3.7921054826871905,
1.0230776382730467,
];
assert_relative_eq!(obj, ref_obj, max_relative = 1e-9);
for (c, r) in con.into_iter().zip(ref_con) {
assert_relative_eq!(c, r, max_relative = 1e-9);
}
Ok(())
}
}