use std::collections::HashMap;
use std::path::Path;
use super::json::{AvrSpec, GovSpec, MachineSpec, UnitSpec};
use super::models::Limits;
use super::models::avr::SexsParams;
use super::models::gov::Tgov1Params;
use super::models::machine::{GenClsParams, GenRoundParams};
#[derive(Clone, Debug, PartialEq)]
pub struct DyrRecord {
pub bus: i64,
pub model: String,
pub id: String,
pub params: Vec<f64>,
pub line: usize,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DyrDocument {
pub records: Vec<DyrRecord>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DyrError {
Io(String),
Malformed {
line: usize,
detail: String,
},
WrongArity {
line: usize,
model: String,
expected: usize,
found: usize,
},
BadParameter {
line: usize,
model: String,
detail: String,
},
MissingMachineData {
bus: i64,
id: String,
},
UnknownBus {
bus: i64,
},
OrphanedControl {
bus: i64,
id: String,
model: String,
},
}
impl std::fmt::Display for DyrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DyrError::Io(message) => write!(f, "{message}"),
DyrError::Malformed { line, detail } => write!(f, "line {line}: {detail}"),
DyrError::WrongArity {
line,
model,
expected,
found,
} => write!(
f,
"line {line}: {model} takes {expected} parameters, found {found}"
),
DyrError::BadParameter {
line,
model,
detail,
} => {
write!(f, "line {line}: {model}: {detail}")
}
DyrError::MissingMachineData { bus, id } => write!(
f,
"machine {id} at bus {bus} has no MBASE/ZSORCE supplied; a .dyr does not \
carry them (they are the .raw's MBASE and ZSORCE)"
),
DyrError::UnknownBus { bus } => {
write!(f, "bus number {bus} has no corresponding bus index")
}
DyrError::OrphanedControl { bus, id, model } => write!(
f,
"{model} at bus {bus} machine {id} has no GENCLS or GENROU record to attach to"
),
}
}
}
impl std::error::Error for DyrError {}
#[derive(Clone, Debug, PartialEq)]
pub enum DyrWarning {
UnsupportedModel {
bus: i64,
id: String,
model: String,
line: usize,
},
SaturationIgnored {
bus: i64,
id: String,
s10: f64,
s12: f64,
},
LimitsIgnored { bus: i64, id: String, model: String },
}
impl std::fmt::Display for DyrWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DyrWarning::UnsupportedModel {
bus,
id,
model,
line,
} => {
write!(
f,
"line {line}: {model} at bus {bus} machine {id} is not implemented; skipped"
)
}
DyrWarning::SaturationIgnored { bus, id, s10, s12 } => write!(
f,
"bus {bus} machine {id}: saturation S(1.0) = {s10}, S(1.2) = {s12} is not \
modelled and was dropped"
),
DyrWarning::LimitsIgnored { bus, id, model } => write!(
f,
"bus {bus} machine {id}: {model}'s output limits are not modelled and were dropped"
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MachineData {
pub mbase: f64,
pub ra: f64,
}
pub fn read(path: impl AsRef<Path>) -> Result<DyrDocument, DyrError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| DyrError::Io(format!("{}: {e}", path.display())))?;
parse(&text)
}
pub fn parse(text: &str) -> Result<DyrDocument, DyrError> {
let mut records = Vec::new();
let mut tokens: Vec<String> = Vec::new();
let mut start_line = 1usize;
let mut fresh = true;
for (index, raw) in text.lines().enumerate() {
let line_no = index + 1;
if fresh && raw.trim_start().starts_with('/') {
continue;
}
let mut rest = raw;
loop {
let (chunk, terminated, remainder) = split_at_terminator(rest);
if fresh && !chunk.trim().is_empty() {
start_line = line_no;
fresh = false;
}
tokens.extend(tokenize(chunk));
if !terminated {
break;
}
if !tokens.is_empty() {
records.push(build_record(&tokens, start_line)?);
tokens.clear();
}
fresh = true;
rest = remainder;
if rest.is_empty() {
break;
}
}
}
if !tokens.is_empty() {
records.push(build_record(&tokens, start_line)?);
}
Ok(DyrDocument { records })
}
fn split_at_terminator(line: &str) -> (&str, bool, &str) {
let mut in_quote = false;
for (i, c) in line.char_indices() {
match c {
'\'' => in_quote = !in_quote,
'/' if !in_quote => return (&line[..i], true, &line[i + 1..]),
_ => {}
}
}
(line, false, "")
}
fn tokenize(chunk: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
let mut in_quote = false;
for c in chunk.chars() {
match c {
'\'' => {
in_quote = !in_quote;
if !in_quote {
out.push(std::mem::take(&mut current));
}
}
_ if in_quote => current.push(c),
c if c.is_whitespace() || c == ',' => {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
out.push(current);
}
out
}
fn build_record(tokens: &[String], line: usize) -> Result<DyrRecord, DyrError> {
if tokens.len() < 3 {
return Err(DyrError::Malformed {
line,
detail: format!(
"expected `bus 'MODEL' id …`, found {} field(s)",
tokens.len()
),
});
}
let bus = tokens[0].parse::<i64>().map_err(|_| DyrError::Malformed {
line,
detail: format!("`{}` is not a bus number", tokens[0]),
})?;
let mut params = Vec::with_capacity(tokens.len() - 3);
for token in &tokens[3..] {
params.push(token.parse::<f64>().map_err(|_| DyrError::Malformed {
line,
detail: format!("`{token}` is not a number"),
})?);
}
Ok(DyrRecord {
bus,
model: tokens[1].trim().to_ascii_uppercase(),
id: tokens[2].trim().to_string(),
params,
line,
})
}
fn arity(record: &DyrRecord, expected: usize) -> Result<(), DyrError> {
if record.params.len() != expected {
return Err(DyrError::WrongArity {
line: record.line,
model: record.model.clone(),
expected,
found: record.params.len(),
});
}
Ok(())
}
impl DyrRecord {
pub fn is_machine(&self) -> bool {
matches!(self.model.as_str(), "GENCLS" | "GENROU")
}
fn as_gencls(&self, data: MachineData, xdp: f64) -> Result<MachineSpec, DyrError> {
arity(self, 2)?;
Ok(MachineSpec::GenCls(GenClsParams {
h: self.params[0],
d: self.params[1],
ra: data.ra,
xdp,
mbase: data.mbase,
}))
}
fn as_genrou(
&self,
data: MachineData,
warnings: &mut Vec<DyrWarning>,
) -> Result<MachineSpec, DyrError> {
arity(self, 14)?;
let p = &self.params;
let (s10, s12) = (p[12], p[13]);
if s10 != 0.0 || s12 != 0.0 {
warnings.push(DyrWarning::SaturationIgnored {
bus: self.bus,
id: self.id.clone(),
s10,
s12,
});
}
let xdpp = p[10];
let params = GenRoundParams {
td0p: p[0],
td0pp: p[1],
tq0p: p[2],
tq0pp: p[3],
h: p[4],
d: p[5],
xd: p[6],
xq: p[7],
xdp: p[8],
xqp: p[9],
xdpp,
xqpp: xdpp,
xl: p[11],
ra: data.ra,
mbase: data.mbase,
};
for (name, lo, hi) in [
("Xl < X''d", params.xl, params.xdpp),
("X''d < X'd", params.xdpp, params.xdp),
("X'd < Xd", params.xdp, params.xd),
("X''q < X'q", params.xqpp, params.xqp),
("X'q < Xq", params.xqp, params.xq),
] {
if hi <= lo {
return Err(DyrError::BadParameter {
line: self.line,
model: self.model.clone(),
detail: format!("{name} does not hold ({lo} vs {hi})"),
});
}
}
Ok(MachineSpec::GenRound(params))
}
fn as_sexs(&self, _warnings: &mut Vec<DyrWarning>) -> Result<AvrSpec, DyrError> {
arity(self, 6)?;
let p = &self.params;
Ok(AvrSpec::Sexs(SexsParams {
k: p[2],
ta: p[0] * p[1],
tb: p[1],
te: p[3],
limits: Limits {
min: Some(p[4]),
max: Some(p[5]),
},
}))
}
fn as_tgov1(&self, _warnings: &mut Vec<DyrWarning>) -> Result<GovSpec, DyrError> {
arity(self, 7)?;
let p = &self.params;
Ok(GovSpec::Tgov1(Tgov1Params {
r: p[0],
t1: p[1],
t2: p[4],
t3: p[5],
dt: p[6],
limits: Limits {
min: Some(p[3]),
max: Some(p[2]),
},
rate: Limits::NONE,
}))
}
}
pub fn to_units(
doc: &DyrDocument,
bus_index: &HashMap<i64, usize>,
machine_data: &HashMap<(i64, String), (MachineData, f64)>,
) -> Result<(Vec<UnitSpec>, Vec<DyrWarning>), DyrError> {
let mut warnings = Vec::new();
let mut units: Vec<UnitSpec> = Vec::new();
let mut index_of: HashMap<(i64, String), usize> = HashMap::new();
for record in doc.records.iter().filter(|r| r.is_machine()) {
let key = (record.bus, record.id.clone());
let &bus = bus_index
.get(&record.bus)
.ok_or(DyrError::UnknownBus { bus: record.bus })?;
let &(data, xdp) = machine_data
.get(&key)
.ok_or_else(|| DyrError::MissingMachineData {
bus: record.bus,
id: record.id.clone(),
})?;
let machine = match record.model.as_str() {
"GENCLS" => record.as_gencls(data, xdp)?,
"GENROU" => record.as_genrou(data, &mut warnings)?,
_ => unreachable!("is_machine covers exactly these"),
};
index_of.insert(key, units.len());
units.push(UnitSpec {
id: format!("{}_{}", record.bus, record.id),
bus,
p: None,
q: None,
machine,
avr: None,
gov: None,
pss: None,
});
}
for record in doc.records.iter().filter(|r| !r.is_machine()) {
let key = (record.bus, record.id.clone());
let owner = || {
index_of
.get(&key)
.copied()
.ok_or_else(|| DyrError::OrphanedControl {
bus: record.bus,
id: record.id.clone(),
model: record.model.clone(),
})
};
match record.model.as_str() {
"SEXS" => {
let i = owner()?;
units[i].avr = Some(record.as_sexs(&mut warnings)?);
}
"TGOV1" => {
let i = owner()?;
units[i].gov = Some(record.as_tgov1(&mut warnings)?);
}
other => warnings.push(DyrWarning::UnsupportedModel {
bus: record.bus,
id: record.id.clone(),
model: other.to_string(),
line: record.line,
}),
}
}
Ok((units, warnings))
}