use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShaftSegment {
pub name: String,
pub h_pu: f64,
pub d_self_pu: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShaftCoupling {
pub k_pu: f64,
pub d_mutual_pu: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SegmentTorqueSource {
Electrical,
GovernorStage(usize),
Fraction(f64),
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShaftModel {
pub segments: Vec<ShaftSegment>,
pub couplings: Vec<ShaftCoupling>,
pub torque_sources: Vec<SegmentTorqueSource>,
}
impl ShaftModel {
pub fn gen_segment_idx(&self) -> usize {
self.torque_sources
.iter()
.position(|s| matches!(s, SegmentTorqueSource::Electrical))
.expect("ShaftModel has no Electrical segment — call validate() first")
}
pub fn validate(&self) -> Result<(), String> {
let n = self.segments.len();
if n == 0 {
return Err("shaft must have at least one segment".into());
}
if self.couplings.len() != n - 1 {
return Err(format!(
"expected {} couplings for {} segments, got {}",
n - 1,
n,
self.couplings.len()
));
}
if self.torque_sources.len() != n {
return Err(format!(
"torque_sources length {} != segments length {}",
self.torque_sources.len(),
n
));
}
let n_elec = self
.torque_sources
.iter()
.filter(|s| matches!(s, SegmentTorqueSource::Electrical))
.count();
if n_elec != 1 {
return Err(format!(
"exactly one Electrical torque source required, found {}",
n_elec
));
}
for (i, seg) in self.segments.iter().enumerate() {
if seg.h_pu <= 0.0 {
return Err(format!(
"segment {} ({}) has H={} <= 0",
i, seg.name, seg.h_pu
));
}
}
for (i, c) in self.couplings.iter().enumerate() {
if c.k_pu <= 0.0 {
return Err(format!("coupling {} has K={} <= 0", i, c.k_pu));
}
}
let mut frac_sum = 0.0_f64;
let mut has_fractions = false;
for ts in &self.torque_sources {
if let SegmentTorqueSource::Fraction(f) = ts {
frac_sum += f;
has_fractions = true;
}
}
if has_fractions && (frac_sum - 1.0).abs() > 1e-6 {
return Err(format!(
"torque fractions sum to {}, expected 1.0",
frac_sum
));
}
Ok(())
}
}
pub fn ieee_fbm_shaft_model() -> ShaftModel {
let names = ["HP", "IP", "LPA", "LPB", "GEN", "EXC"];
let h_vals = [0.092595, 0.155589, 0.858670, 0.884215, 0.868495, 0.034216];
let k_vals = [19.652, 34.929, 52.038, 70.858, 2.822];
let segments = names
.iter()
.zip(h_vals.iter())
.map(|(&name, &h)| ShaftSegment {
name: name.to_string(),
h_pu: h,
d_self_pu: 0.0,
})
.collect();
let couplings = k_vals
.iter()
.map(|&k| ShaftCoupling {
k_pu: k,
d_mutual_pu: 0.0,
})
.collect();
let torque_sources = vec![
SegmentTorqueSource::Fraction(0.30), SegmentTorqueSource::Fraction(0.26), SegmentTorqueSource::Fraction(0.22), SegmentTorqueSource::Fraction(0.22), SegmentTorqueSource::Electrical, SegmentTorqueSource::None, ];
ShaftModel {
segments,
couplings,
torque_sources,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ieee_fbm_validates() {
let model = ieee_fbm_shaft_model();
model.validate().unwrap();
assert_eq!(model.gen_segment_idx(), 4);
}
#[test]
fn test_validate_no_electrical() {
let model = ShaftModel {
segments: vec![
ShaftSegment {
name: "A".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
ShaftSegment {
name: "B".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
],
couplings: vec![ShaftCoupling {
k_pu: 10.0,
d_mutual_pu: 0.0,
}],
torque_sources: vec![
SegmentTorqueSource::Fraction(1.0),
SegmentTorqueSource::None,
],
};
assert!(model.validate().unwrap_err().contains("Electrical"));
}
#[test]
fn test_validate_bad_fraction_sum() {
let model = ShaftModel {
segments: vec![
ShaftSegment {
name: "HP".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
ShaftSegment {
name: "GEN".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
],
couplings: vec![ShaftCoupling {
k_pu: 10.0,
d_mutual_pu: 0.0,
}],
torque_sources: vec![
SegmentTorqueSource::Fraction(0.5),
SegmentTorqueSource::Electrical,
],
};
assert!(model.validate().unwrap_err().contains("fractions sum"));
}
#[test]
fn test_validate_coupling_count() {
let model = ShaftModel {
segments: vec![
ShaftSegment {
name: "A".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
ShaftSegment {
name: "B".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
],
couplings: vec![], torque_sources: vec![
SegmentTorqueSource::Fraction(1.0),
SegmentTorqueSource::Electrical,
],
};
assert!(model.validate().unwrap_err().contains("couplings"));
}
#[test]
fn test_validate_zero_inertia() {
let model = ShaftModel {
segments: vec![
ShaftSegment {
name: "A".into(),
h_pu: 0.0,
d_self_pu: 0.0,
},
ShaftSegment {
name: "B".into(),
h_pu: 1.0,
d_self_pu: 0.0,
},
],
couplings: vec![ShaftCoupling {
k_pu: 10.0,
d_mutual_pu: 0.0,
}],
torque_sources: vec![
SegmentTorqueSource::Fraction(1.0),
SegmentTorqueSource::Electrical,
],
};
assert!(model.validate().unwrap_err().contains("H=0"));
}
}