use std::collections::BTreeMap;
use std::fmt::Write as _;
use crate::format::Conversion;
use crate::network::{BusId, Network, SourceFormat};
#[must_use]
pub fn write_matpower(net: &Network) -> String {
match &net.source {
Some(text) if net.source_format == SourceFormat::Matpower => text.to_string(),
_ => canonical(net),
}
}
pub(crate) fn write_matpower_conversion(net: &Network) -> Conversion {
let text = write_matpower(net);
if net.source.is_some() && net.source_format == SourceFormat::Matpower {
return Conversion {
text,
warnings: Vec::new(),
};
}
let mut warnings = Vec::new();
if !net.hvdc.is_empty() {
warnings.push(format!(
"{} HVDC dcline(s) dropped: the canonical MATPOWER writer emits no `mpc.dcline` block",
net.hvdc.len()
));
}
if !net.transformers_3w.is_empty() {
warnings.push(format!(
"{} 3-winding transformer(s) dropped: the canonical MATPOWER writer emits no \
3-winding record (star-expand them into branches before writing to keep them)",
net.transformers_3w.len()
));
}
if net
.buses
.iter()
.any(|b| b.evhi.is_some() || b.evlo.is_some())
{
warnings.push(
"emergency voltage band(s) (EVHI/EVLO) dropped: this writer carries one voltage band"
.into(),
);
}
let with_caps = net.generators.iter().filter(|g| g.has_caps()).count();
if with_caps > 0 {
warnings.push(format!(
"generator capability/ramp columns dropped for {with_caps} generator(s): the canonical MATPOWER writer emits only the standard gen columns"
));
}
let with_cost = net.generators.iter().filter(|g| g.cost.is_some()).count();
if with_cost > 0 && with_cost < net.generators.len() {
warnings.push(format!(
"gen cost dropped: {with_cost} of {} generators carry cost data, but MATPOWER's `mpc.gencost` block is all-or-nothing",
net.generators.len()
));
}
let has_extras = net.buses.iter().any(|b| !b.extras.is_empty())
|| net.branches.iter().any(|b| !b.extras.is_empty())
|| net.loads.iter().any(|l| !l.extras.is_empty())
|| net.shunts.iter().any(|s| !s.extras.is_empty())
|| net.storage.iter().any(|s| !s.extras.is_empty())
|| net.hvdc.iter().any(|d| !d.extras.is_empty());
if has_extras {
warnings.push(
"source-format passthrough fields (extras) dropped: the canonical MATPOWER writer emits only named columns".to_string(),
);
}
Conversion { text, warnings }
}
#[allow(clippy::too_many_lines)] fn canonical(net: &Network) -> String {
let mut demand: BTreeMap<BusId, (f64, f64)> = BTreeMap::new();
for l in &net.loads {
let e = demand.entry(l.bus).or_default();
e.0 += l.p;
e.1 += l.q;
}
let mut shunt: BTreeMap<BusId, (f64, f64)> = BTreeMap::new();
for s in &net.shunts {
let e = shunt.entry(s.bus).or_default();
e.0 += s.g;
e.1 += s.b;
}
let mut s = String::new();
let _ = writeln!(s, "function mpc = {}", matlab_ident(&net.name));
let _ = writeln!(s, "mpc.version = '2';");
let _ = writeln!(s, "mpc.baseMVA = {};", net.base_mva);
let _ = writeln!(s, "mpc.bus = [");
for b in &net.buses {
let (pd, qd) = demand.get(&b.id).copied().unwrap_or((0.0, 0.0));
let (gs, bs) = shunt.get(&b.id).copied().unwrap_or((0.0, 0.0));
let _ = writeln!(
s,
"\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{};",
b.id,
b.kind as u8,
pd,
qd,
gs,
bs,
b.area,
b.vm,
b.va,
b.base_kv,
b.zone,
b.vmax,
b.vmin
);
}
let _ = writeln!(s, "];");
let _ = writeln!(s, "mpc.branch = [");
for br in &net.branches {
let _ = writeln!(
s,
"\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{};",
br.from,
br.to,
br.r,
br.x,
br.b,
br.rate_a,
br.rate_b,
br.rate_c,
br.tap,
br.shift,
f64::from(br.in_service),
br.angmin,
br.angmax
);
}
let _ = writeln!(s, "];");
if !net.generators.is_empty() {
let _ = writeln!(s, "mpc.gen = [");
for g in &net.generators {
let _ = writeln!(
s,
"\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{};",
g.bus,
g.pg,
g.qg,
g.qmax,
g.qmin,
g.vg,
g.mbase,
f64::from(g.in_service),
g.pmax,
g.pmin
);
}
let _ = writeln!(s, "];");
if net.generators.iter().all(|g| g.cost.is_some()) {
let _ = writeln!(s, "mpc.gencost = [");
let width = net
.generators
.iter()
.filter_map(|g| g.cost.as_ref())
.map(|c| c.coeffs.len())
.max()
.unwrap_or(0);
for g in &net.generators {
let c = g.cost.as_ref().expect("checked all gens have cost");
let _ = write!(
s,
"\t{}\t{}\t{}\t{}",
c.model, c.startup, c.shutdown, c.ncost
);
for j in 0..width {
let _ = write!(s, "\t{}", c.coeffs.get(j).copied().unwrap_or(0.0));
}
let _ = writeln!(s, ";");
}
let _ = writeln!(s, "];");
}
}
if !net.storage.is_empty() {
let _ = writeln!(s, "mpc.storage = [");
for st in &net.storage {
let _ = writeln!(
s,
"\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{};",
st.bus,
st.ps,
st.qs,
st.energy,
st.energy_rating,
st.charge_rating,
st.discharge_rating,
st.charge_efficiency,
st.discharge_efficiency,
st.thermal_rating,
st.qmin,
st.qmax,
st.r,
st.x,
st.p_loss,
st.q_loss,
f64::from(st.in_service)
);
}
let _ = writeln!(s, "];");
}
s
}
fn matlab_ident(name: &str) -> String {
let mut ident: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
if !ident.starts_with(|c: char| c.is_ascii_alphabetic()) {
ident.insert(0, 'c');
}
ident
}