use serde::{Deserialize, Serialize};
use crate::smr::{ForwardRate, SmrCase};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeckError {
Parse(String),
Invalid(String),
}
impl std::fmt::Display for DeckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeckError::Parse(m) => write!(f, "deck does not parse: {m}"),
DeckError::Invalid(m) => write!(f, "deck is invalid: {m}"),
}
}
}
impl std::error::Error for DeckError {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Deck {
pub schema_version: u32,
pub case: CaseBlock,
pub reactor: ReactorBlock,
pub feed: FeedBlock,
pub kinetics: KineticsBlock,
#[serde(default)]
pub sweep: Option<SweepBlock>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CaseBlock {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReactorBlock {
pub kind: String,
pub volume_m3: f64,
pub temperature_k: f64,
pub pressure_pa: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeedBlock {
pub methane_mol_s: f64,
pub steam_to_carbon: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KineticsBlock {
pub reforming: RateBlock,
pub water_gas_shift: RateBlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RateBlock {
pub a_forward: f64,
pub e_forward_j_mol: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SweepBlock {
pub variable: String,
pub from: f64,
pub to: f64,
pub steps: usize,
}
impl SweepBlock {
#[must_use]
pub fn values(&self) -> Vec<f64> {
if self.steps < 2 {
return vec![self.from];
}
let n = self.steps - 1;
(0..self.steps)
.map(|i| self.from + (self.to - self.from) * (i as f64) / (n as f64))
.collect()
}
}
impl Deck {
pub fn from_toml(text: &str) -> Result<Self, DeckError> {
let deck: Deck = toml::from_str(text).map_err(|e| DeckError::Parse(e.to_string()))?;
deck.validate()?;
Ok(deck)
}
pub fn to_toml(&self) -> Result<String, DeckError> {
toml::to_string_pretty(self).map_err(|e| DeckError::Parse(e.to_string()))
}
pub fn validate(&self) -> Result<(), DeckError> {
let bad = |m: String| Err(DeckError::Invalid(m));
if self.schema_version != SCHEMA_VERSION {
return bad(format!(
"schema_version {} is not the {SCHEMA_VERSION} this build reads",
self.schema_version
));
}
if self.case.name.trim().is_empty() {
return bad("case.name is empty".into());
}
if self.reactor.kind != "cstr" {
return bad(format!(
"reactor.kind {:?} is not implemented; only \"cstr\" is",
self.reactor.kind
));
}
for (label, v) in [
("reactor.volume_m3", self.reactor.volume_m3),
("reactor.temperature_k", self.reactor.temperature_k),
("reactor.pressure_pa", self.reactor.pressure_pa),
("feed.methane_mol_s", self.feed.methane_mol_s),
("feed.steam_to_carbon", self.feed.steam_to_carbon),
] {
if !(v > 0.0) || !v.is_finite() {
return bad(format!("{label} must be finite and positive, got {v}"));
}
}
for (label, r) in [
("kinetics.reforming", self.kinetics.reforming),
("kinetics.water_gas_shift", self.kinetics.water_gas_shift),
] {
if !(r.a_forward > 0.0) || !r.a_forward.is_finite() {
return bad(format!(
"{label}.a_forward must be finite and positive, got {}",
r.a_forward
));
}
if r.e_forward_j_mol < 0.0 || !r.e_forward_j_mol.is_finite() {
return bad(format!(
"{label}.e_forward_j_mol must be finite and non-negative, got {}",
r.e_forward_j_mol
));
}
}
if let Some(s) = &self.sweep {
const KNOWN: [&str; 4] = [
"temperature_k",
"pressure_pa",
"steam_to_carbon",
"volume_m3",
];
if !KNOWN.contains(&s.variable.as_str()) {
return bad(format!(
"sweep.variable {:?} is not one of {KNOWN:?}",
s.variable
));
}
if s.steps < 2 {
return bad(format!("sweep.steps must be at least 2, got {}", s.steps));
}
if !s.from.is_finite() || !s.to.is_finite() {
return bad("sweep bounds must be finite".into());
}
if !(s.from > 0.0) || !(s.to > 0.0) {
return bad("sweep bounds must be positive for every supported variable".into());
}
}
Ok(())
}
#[must_use]
pub fn base_case(&self) -> SmrCase {
SmrCase {
methane_feed: self.feed.methane_mol_s,
steam_to_carbon: self.feed.steam_to_carbon,
temperature: self.reactor.temperature_k,
pressure: self.reactor.pressure_pa,
volume: self.reactor.volume_m3,
forward: [
ForwardRate {
a: self.kinetics.reforming.a_forward,
e: self.kinetics.reforming.e_forward_j_mol,
},
ForwardRate {
a: self.kinetics.water_gas_shift.a_forward,
e: self.kinetics.water_gas_shift.e_forward_j_mol,
},
],
}
}
#[must_use]
pub fn cases(&self) -> Vec<(f64, SmrCase)> {
let base = self.base_case();
match &self.sweep {
None => vec![(f64::NAN, base)],
Some(s) => s
.values()
.into_iter()
.map(|v| {
let mut c = base.clone();
match s.variable.as_str() {
"temperature_k" => c.temperature = v,
"pressure_pa" => c.pressure = v,
"steam_to_carbon" => c.steam_to_carbon = v,
"volume_m3" => c.volume = v,
_ => unreachable!("validate rejects unknown sweep variables"),
}
(v, c)
})
.collect(),
}
}
}