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::{Generator, Network, SourceFormat};
19use crate::{Error, Result};
20
21pub fn parse_matpower(content: &str) -> Result<Network> {
23 parse_matpower_source(Arc::new(content.to_owned()), None)
25}
26
27pub fn parse_matpower_file(path: impl AsRef<Path>) -> Result<Network> {
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<Network> {
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<Network> {
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<Network> {
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(Network {
150 name: name.to_string(),
151 base_mva,
152 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
153 buses,
154 loads,
155 shunts,
156 branches,
157 switches: Vec::new(),
158 generators,
159 storage,
160 hvdc,
161 transformers_3w: Vec::new(),
162 areas: Vec::new(),
163 solver: None,
164 source_format: SourceFormat::Matpower,
165 source: None,
166 })
167}
168
169fn estimate_rows(assignment: &str) -> usize {
176 const MAX_ROW_HINT: usize = 1 << 20;
177 assignment
178 .bytes()
179 .filter(|&b| b == b';')
180 .count()
181 .min(MAX_ROW_HINT)
182}
183
184fn parse_rows<T>(
186 assignment: &str,
187 field: &str,
188 ctor: impl Fn(&[f64], usize) -> Result<T>,
189) -> Result<Vec<T>> {
190 let mut out = Vec::with_capacity(estimate_rows(assignment));
191 matlab::for_each_matrix_row(assignment, field, |row, i| {
192 out.push(ctor(row, i)?);
193 Ok(())
194 })?;
195 Ok(out)
196}
197
198fn parse_optional<'a, T>(
200 get: &impl Fn(&str) -> Option<&'a str>,
201 field: &str,
202 ctor: impl Fn(&[f64], usize) -> Result<T>,
203) -> Result<Vec<T>> {
204 match get(field) {
205 Some(raw) => parse_rows(raw, field, ctor),
206 None => Ok(Vec::new()),
207 }
208}
209
210fn parse_gens<'a>(get: &impl Fn(&str) -> Option<&'a str>) -> Result<Vec<Generator>> {
213 let Some(raw) = get("gen") else {
214 return Ok(Vec::new());
215 };
216 let mut gens = parse_rows(raw, "gen", rows::gen_row)?;
217
218 if let Some(craw) = get("gencost") {
221 let costs = parse_rows(craw, "gencost", rows::gencost_row)?;
222 let n = gens.len();
225 if costs.len() != n && costs.len() != 2 * n {
226 return Err(Error::GenCostCountMismatch {
227 gens: n,
228 gencost: costs.len(),
229 });
230 }
231 for (generator, cost) in gens.iter_mut().zip(costs) {
234 generator.cost = Some(cost);
235 }
236 }
237
238 Ok(gens)
239}