powerio/format/matpower/
mod.rs1mod locate;
4mod matlab;
5mod rows;
6mod tokens;
7mod writer;
8
9#[cfg(test)]
10mod tests;
11
12use std::path::Path;
13use std::sync::Arc;
14
15pub use writer::write_matpower;
16pub(crate) use writer::write_matpower_conversion;
17
18use crate::network::{BalancedNetwork, Generator, SourceFormat};
19use crate::{Error, Result};
20
21pub fn parse_matpower(content: &str) -> Result<BalancedNetwork> {
23 parse_matpower_source(Arc::new(content.to_owned()), None)
25}
26
27pub fn parse_matpower_file(path: impl AsRef<Path>) -> Result<BalancedNetwork> {
29 let path = path.as_ref();
30 let content = std::fs::read_to_string(path)?;
31 let name = path
32 .file_stem()
33 .and_then(|s| s.to_str())
34 .unwrap_or("case")
35 .to_string();
36 parse_matpower_named(Arc::new(content), &name)
39}
40
41pub(crate) fn parse_matpower_source(
45 source: Arc<String>,
46 name_hint: Option<&str>,
47) -> Result<BalancedNetwork> {
48 let name = name_hint
49 .map(str::to_owned)
50 .or_else(|| matpower_function_name(&source).map(str::to_owned))
51 .unwrap_or_else(|| "case".to_string());
52 parse_matpower_named(source, &name)
53}
54
55fn matpower_function_name(source: &str) -> Option<&str> {
56 for line in source.lines() {
57 let line = line.trim_start();
58 if !line.starts_with("function") {
59 continue;
60 }
61 let Some((_, rhs)) = line.split_once('=') else {
62 continue;
63 };
64 let rhs = rhs.trim_start();
65 let end = rhs
66 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
67 .unwrap_or(rhs.len());
68 let starts_ident = rhs
69 .as_bytes()
70 .first()
71 .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_');
72 if end > 0 && starts_ident {
73 return Some(&rhs[..end]);
74 }
75 }
76 None
77}
78
79fn parse_matpower_named(source: Arc<String>, name: &str) -> Result<BalancedNetwork> {
80 let mut net = {
84 let located = locate::locate_assignments(&source);
85 build_case(name, |field| {
86 located
87 .iter()
88 .find(|(f, _)| *f == field)
89 .map(|(_, full)| *full)
90 })?
91 };
92 net.source = Some(source);
93 net.check_references("MATPOWER")?;
97 Ok(net)
98}
99
100fn build_case<'a>(name: &str, get: impl Fn(&str) -> Option<&'a str>) -> Result<BalancedNetwork> {
106 let base_mva = get("baseMVA")
107 .and_then(|raw| matlab::scalar_from_assignment(raw, "baseMVA").transpose())
108 .transpose()?
109 .ok_or(Error::MissingField("baseMVA"))?;
110
111 let bus_raw = get("bus").ok_or(Error::MissingField("bus"))?;
112 let n_bus = estimate_rows(bus_raw);
113 let mut buses = Vec::with_capacity(n_bus);
114 let mut loads = Vec::with_capacity(n_bus);
115 let mut shunts = Vec::with_capacity(n_bus);
116 matlab::for_each_matrix_row(bus_raw, "bus", |row, i| {
117 let (bus, load, shunt) = rows::bus_row(row, i)?;
118 buses.push(bus);
119 if let Some(l) = load {
120 loads.push(l);
121 }
122 if let Some(s) = shunt {
123 shunts.push(s);
124 }
125 Ok(())
126 })?;
127
128 let branches = parse_rows(
129 get("branch").ok_or(Error::MissingField("branch"))?,
130 "branch",
131 rows::branch_row,
132 )?;
133
134 let generators = parse_gens(&get)?;
135 let storage = parse_optional(&get, "storage", rows::storage_row)?;
136 let hvdc = parse_optional(&get, "dcline", rows::hvdc_row)?;
137
138 if let Some(raw) = get("bus_name") {
141 let names = locate::parse_string_cell(raw);
142 if names.len() == buses.len() {
143 for (bus, label) in buses.iter_mut().zip(names) {
144 bus.name = Some(label);
145 }
146 }
147 }
148
149 Ok(BalancedNetwork {
150 name: name.to_string(),
151 base_mva,
152 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
153 geo: None,
154 buses,
155 loads,
156 shunts,
157 branches,
158 switches: Vec::new(),
159 generators,
160 storage,
161 hvdc,
162 transformers_3w: Vec::new(),
163 areas: Vec::new(),
164 solver: None,
165 source_format: SourceFormat::Matpower,
166 source: None,
167 })
168}
169
170fn estimate_rows(assignment: &str) -> usize {
177 const MAX_ROW_HINT: usize = 1 << 20;
178 assignment
179 .bytes()
180 .filter(|&b| b == b';')
181 .count()
182 .min(MAX_ROW_HINT)
183}
184
185fn parse_rows<T>(
187 assignment: &str,
188 field: &str,
189 ctor: impl Fn(&[f64], usize) -> Result<T>,
190) -> Result<Vec<T>> {
191 let mut out = Vec::with_capacity(estimate_rows(assignment));
192 matlab::for_each_matrix_row(assignment, field, |row, i| {
193 out.push(ctor(row, i)?);
194 Ok(())
195 })?;
196 Ok(out)
197}
198
199fn parse_optional<'a, T>(
201 get: &impl Fn(&str) -> Option<&'a str>,
202 field: &str,
203 ctor: impl Fn(&[f64], usize) -> Result<T>,
204) -> Result<Vec<T>> {
205 match get(field) {
206 Some(raw) => parse_rows(raw, field, ctor),
207 None => Ok(Vec::new()),
208 }
209}
210
211fn parse_gens<'a>(get: &impl Fn(&str) -> Option<&'a str>) -> Result<Vec<Generator>> {
214 let Some(raw) = get("gen") else {
215 return Ok(Vec::new());
216 };
217 let mut gens = parse_rows(raw, "gen", rows::gen_row)?;
218
219 if let Some(craw) = get("gencost") {
222 let costs = parse_rows(craw, "gencost", rows::gencost_row)?;
223 let n = gens.len();
226 if costs.len() != n && costs.len() != 2 * n {
227 return Err(Error::GenCostCountMismatch {
228 gens: n,
229 gencost: costs.len(),
230 });
231 }
232 for (generator, cost) in gens.iter_mut().zip(costs) {
235 generator.cost = Some(cost);
236 }
237 }
238
239 Ok(gens)
240}