mod conversions;
mod locate;
mod matlab;
mod rows;
mod tokens;
mod writer;
#[cfg(test)]
mod conversion_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
pub(crate) use writer::write_matpower;
pub(crate) use writer::write_matpower_conversion;
use self::locate::Assignment;
use crate::collect::Diagnostics;
use crate::diagnostics::codes::READ_MATPOWER_INDEXED_ASSIGNMENT;
use crate::network::{BalancedNetwork, BalancedNetworkTables, Generator, Hvdc, SourceFormat};
use crate::{Error, Result};
pub(crate) fn parse_matpower_source(
source: &str,
name_hint: Option<&str>,
warnings: &mut Diagnostics,
) -> Result<BalancedNetwork> {
let name = name_hint
.map(str::to_owned)
.or_else(|| matpower_function_name(source).map(str::to_owned))
.unwrap_or_else(|| "case".to_string());
parse_matpower_named(source, &name, warnings)
}
fn matpower_function_name(source: &str) -> Option<&str> {
for line in source.lines() {
let line = line.trim_start();
if !line.starts_with("function") {
continue;
}
let Some((_, rhs)) = line.split_once('=') else {
continue;
};
let rhs = rhs.trim_start();
let end = rhs
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.unwrap_or(rhs.len());
let starts_ident = rhs
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_');
if end > 0 && starts_ident {
return Some(&rhs[..end]);
}
}
None
}
fn parse_matpower_named(
source: &str,
name: &str,
warnings: &mut Diagnostics,
) -> Result<BalancedNetwork> {
let located = locate::locate_assignments(source);
let mut net = build_case(
name,
|field| {
located
.iter()
.find(|assignment| assignment.field == field)
.copied()
},
warnings,
)?;
if !indexed_assignment_spans(source).is_empty()
&& !conversions::apply(source, &located, &mut net)
{
warn_indexed_assignments(source, warnings);
}
net.check_references("MATPOWER")?;
Ok(net)
}
fn warn_indexed_assignments(source: &str, warnings: &mut Diagnostics) {
const LISTED: usize = 1_000;
let spans = indexed_assignment_spans(source);
if spans.len() > LISTED {
warnings.push(
&READ_MATPOWER_INDEXED_ASSIGNMENT,
format!(
"{} indexed assignments to mpc fields are not evaluated; the first {LISTED} are listed; no post-literal conversions were applied",
spans.len()
),
);
}
for (start, end, field) in spans.into_iter().take(LISTED) {
warnings.enter_record(start, end);
warnings.push(
&READ_MATPOWER_INDEXED_ASSIGNMENT,
format!(
"indexed assignment to mpc.{field} is not evaluated; no post-literal conversions were applied; parsed values may differ from MATLAB execution"
),
);
warnings.leave_record();
}
}
fn indexed_assignment_spans(source: &str) -> Vec<(usize, usize, &str)> {
let b = source.as_bytes();
let mut out = Vec::new();
let (mut i, mut line_start) = (0, 0);
let mut fresh = true;
let mut block_depth = 0usize;
let mut quote = None;
let (mut p, mut square, mut brace) = (0usize, 0usize, 0usize);
while i < b.len() {
if i == line_start {
let mut first = i;
while first < b.len() && matches!(b[first], b' ' | b'\t' | b'\r') {
first += 1;
}
let end = b[first..]
.iter()
.position(|x| *x == b'\n')
.map_or(b.len(), |n| first + n);
let delimiter = source[first..end].trim();
if block_depth > 0 {
match delimiter {
"%{" => block_depth += 1,
"%}" => block_depth -= 1,
_ => {}
}
i = end;
if i < b.len() {
i += 1;
line_start = i;
if p == 0 && square == 0 && brace == 0 {
fresh = true;
}
}
continue;
}
if delimiter == "%{" {
block_depth = 1;
i = end;
if i < b.len() {
i += 1;
line_start = i;
}
continue;
}
}
let c = b[i];
if let Some(q) = quote {
if c == q {
if b.get(i + 1) == Some(&q) {
i += 2;
continue;
}
quote = None;
}
i += 1;
continue;
}
if c == b'"' || (c == b'\'' && !is_transpose_quote(b, i, square > 0 || brace > 0)) {
quote = Some(c);
fresh = false;
i += 1;
continue;
}
if let Some(end) = matlab_continuation_end(b, i) {
i = end;
line_start = i;
continue;
}
if c == b'%' {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
continue;
}
if fresh
&& mpc_field_start(b, i).is_some()
&& let Some((lhs_end, field)) = indexed_assignment_at(source, i)
{
out.push((i, matlab_statement_end(source, i).max(lhs_end), field));
}
match c {
b'(' => p += 1,
b')' => p = p.saturating_sub(1),
b'[' => square += 1,
b']' => square = square.saturating_sub(1),
b'{' => brace += 1,
b'}' => brace = brace.saturating_sub(1),
_ => {}
}
if matches!(c, b',' | b';' | b'\n') && p == 0 && square == 0 && brace == 0 {
fresh = true;
} else if !c.is_ascii_whitespace() {
fresh = false;
}
if c == b'\n' {
line_start = i + 1;
}
i += 1;
}
out
}
fn is_transpose_quote(b: &[u8], at: usize, in_array: bool) -> bool {
if in_array && at > 0 && b[at - 1].is_ascii_whitespace() {
return false;
}
let mut j = at;
while j > 0 && b[j - 1].is_ascii_whitespace() {
j -= 1;
}
if j > 0 && b[j - 1] == b'.' {
j -= 1;
}
j > 0
&& matches!(
b[j - 1],
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b')' | b']' | b'}' | b'\''
)
}
fn matlab_continuation_end(b: &[u8], at: usize) -> Option<usize> {
(b.get(at..at + 3) == Some(b"...")).then(|| {
b[at..]
.iter()
.position(|c| *c == b'\n')
.map_or(b.len(), |offset| at + offset + 1)
})
}
fn skip_matlab_assignment_trivia(b: &[u8], mut at: usize) -> usize {
loop {
while at < b.len() && b[at].is_ascii_whitespace() {
at += 1;
}
if let Some(end) = matlab_continuation_end(b, at) {
at = end;
} else {
return at;
}
}
}
fn mpc_field_start(b: &[u8], start: usize) -> Option<usize> {
if b.get(start..start + 3) != Some(b"mpc") {
return None;
}
let dot = skip_matlab_assignment_trivia(b, start + 3);
if b.get(dot) != Some(&b'.') {
return None;
}
Some(skip_matlab_assignment_trivia(b, dot + 1))
}
fn indexed_assignment_at(source: &str, start: usize) -> Option<(usize, &str)> {
let b = source.as_bytes();
let mut i = mpc_field_start(b, start)?;
let field_start = i;
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
i += 1;
}
if i == field_start {
return None;
}
let field = &source[field_start..i];
i = skip_matlab_assignment_trivia(b, i);
if b.get(i) != Some(&b'(') {
return None;
}
i += 1;
let mut depth = 0usize;
let (mut square, mut brace) = (0usize, 0usize);
let mut quote = None;
while i < b.len() {
let c = b[i];
if let Some(q) = quote {
if c == q {
if b.get(i + 1) == Some(&q) {
i += 2;
continue;
}
quote = None;
}
i += 1;
continue;
}
if c == b'"' || (c == b'\'' && !is_transpose_quote(b, i, square > 0 || brace > 0)) {
quote = Some(c);
i += 1;
continue;
}
if let Some(end) = matlab_continuation_end(b, i) {
i = end;
continue;
}
if c == b'%' {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
continue;
}
match c {
b'\n' if square == 0 && brace == 0 => return None,
b'[' => square += 1,
b']' => square = square.saturating_sub(1),
b'{' => brace += 1,
b'}' => brace = brace.saturating_sub(1),
b'(' => depth += 1,
b')' if depth == 0 => {
let end = i + 1;
i = skip_matlab_assignment_trivia(b, end);
if matches!(b.get(i), Some(b'+' | b'-' | b'*' | b'/' | b'\\' | b'^'))
&& b.get(i + 1) == Some(&b'=')
&& b.get(i + 2) != Some(&b'=')
{
return Some((end, field));
}
if b.get(i) == Some(&b'=')
&& b.get(i + 1) != Some(&b'=')
&& !matches!(
i.checked_sub(1).and_then(|n| b.get(n)),
Some(b'=' | b'<' | b'>' | b'~')
)
{
return Some((end, field));
}
return None;
}
b')' => depth -= 1,
_ => {}
}
i += 1;
}
None
}
fn matlab_statement_end(source: &str, start: usize) -> usize {
let b = source.as_bytes();
let mut i = start;
let (mut p, mut square, mut brace) = (0usize, 0usize, 0usize);
let mut quote = None;
while i < b.len() {
let c = b[i];
if let Some(q) = quote {
if c == q {
if b.get(i + 1) == Some(&q) {
i += 2;
continue;
}
quote = None;
}
i += 1;
continue;
}
if c == b'"' || (c == b'\'' && !is_transpose_quote(b, i, square > 0 || brace > 0)) {
quote = Some(c);
i += 1;
continue;
}
if let Some(end) = matlab_continuation_end(b, i) {
i = end;
continue;
}
if c == b'%' {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
continue;
}
match c {
b'(' => p += 1,
b')' => p = p.saturating_sub(1),
b'[' => square += 1,
b']' => square = square.saturating_sub(1),
b'{' => brace += 1,
b'}' => brace = brace.saturating_sub(1),
b',' | b';' if p == 0 && square == 0 && brace == 0 => return i + 1,
b'\n' if p == 0 && square == 0 && brace == 0 => return i,
_ => {}
}
i += 1;
}
source.len()
}
fn build_case<'a>(
name: &str,
get: impl Fn(&str) -> Option<Assignment<'a>>,
warnings: &mut Diagnostics,
) -> Result<BalancedNetwork> {
let base_mva = match get("baseMVA") {
Some(assignment) => {
let (start, end) = assignment.range();
warnings.enter_record(start, end);
let value = matlab::scalar_from_assignment(assignment.text, "baseMVA")?;
warnings.leave_record();
value
}
None => None,
}
.ok_or(Error::MissingField("baseMVA"))?;
let bus = get("bus").ok_or(Error::MissingField("bus"))?;
let n_bus = estimate_rows(bus.text);
let mut buses = Vec::with_capacity(n_bus);
let mut loads = Vec::with_capacity(n_bus);
let mut shunts = Vec::with_capacity(n_bus);
matlab::for_each_matrix_row(bus.text, bus.start, "bus", warnings, |row, i| {
let (bus, load, shunt) = rows::bus_row(row, i)?;
buses.push(bus);
if let Some(l) = load {
loads.push(l);
}
if let Some(s) = shunt {
shunts.push(s);
}
Ok(())
})?;
let branches = parse_rows(
get("branch").ok_or(Error::MissingField("branch"))?,
"branch",
rows::branch_row,
warnings,
)?;
let generators = parse_gens(&get, warnings)?;
let storage = parse_optional(&get, "storage", rows::storage_row, warnings)?;
let areas = parse_optional(&get, "areas", rows::area_row, warnings)?;
let mut hvdc = parse_optional(&get, "dcline", rows::hvdc_row, warnings)?;
attach_dcline_costs(&get, &mut hvdc, warnings)?;
if let Some(assignment) = get("bus_name") {
let names = locate::parse_string_cell(assignment.text);
if names.len() == buses.len() {
for (bus, label) in buses.iter_mut().zip(names) {
bus.name = (!label.is_empty()).then_some(label);
}
}
}
Ok(BalancedNetwork::from_tables(BalancedNetworkTables {
name: name.to_string(),
base_mva,
base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
geo: None,
case_metadata: crate::network::CaseMetadata::default(),
detailed_connectivity: None,
generated_uids: std::sync::Arc::default(),
buses: buses.into(),
loads: loads.into(),
shunts: shunts.into(),
static_var_compensators: Vec::new().into(),
branches: branches.into(),
switches: Vec::new().into(),
generators: generators.into(),
storage: storage.into(),
hvdc: hvdc.into(),
transformers_3w: Vec::new().into(),
areas: areas.into(),
solver: None,
source_format: SourceFormat::Matpower,
}))
}
fn estimate_rows(assignment: &str) -> usize {
const MAX_ROW_HINT: usize = 1 << 20;
assignment
.bytes()
.filter(|&b| b == b';')
.count()
.min(MAX_ROW_HINT)
}
fn parse_rows<T>(
assignment: Assignment<'_>,
field: &str,
ctor: impl Fn(&[f64], usize) -> Result<T>,
warnings: &mut Diagnostics,
) -> Result<Vec<T>> {
let mut out = Vec::with_capacity(estimate_rows(assignment.text));
matlab::for_each_matrix_row(
assignment.text,
assignment.start,
field,
warnings,
|row, i| {
out.push(ctor(row, i)?);
Ok(())
},
)?;
Ok(out)
}
fn parse_optional<'a, T>(
get: &impl Fn(&str) -> Option<Assignment<'a>>,
field: &str,
ctor: impl Fn(&[f64], usize) -> Result<T>,
warnings: &mut Diagnostics,
) -> Result<Vec<T>> {
match get(field) {
Some(assignment) => parse_rows(assignment, field, ctor, warnings),
None => Ok(Vec::new()),
}
}
fn attach_dcline_costs<'a>(
get: &impl Fn(&str) -> Option<Assignment<'a>>,
hvdc: &mut [Hvdc],
warnings: &mut Diagnostics,
) -> Result<()> {
let Some(assignment) = get("dclinecost") else {
return Ok(());
};
let costs = parse_rows(assignment, "dclinecost", rows::gencost_row, warnings)?;
if costs.len() != hvdc.len() {
let (start, end) = assignment.range();
warnings.enter_record(start, end);
return Err(Error::DcLineCostCountMismatch {
dclines: hvdc.len(),
dclinecost: costs.len(),
});
}
for (line, cost) in hvdc.iter_mut().zip(costs) {
let zero =
cost.startup == 0.0 && cost.shutdown == 0.0 && cost.coeffs.iter().all(|c| *c == 0.0);
if !zero {
line.cost = Some(cost);
}
}
Ok(())
}
fn parse_gens<'a>(
get: &impl Fn(&str) -> Option<Assignment<'a>>,
warnings: &mut Diagnostics,
) -> Result<Vec<Generator>> {
let Some(assignment) = get("gen") else {
return Ok(Vec::new());
};
let mut gens = parse_rows(assignment, "gen", rows::gen_row, warnings)?;
if let Some(costs_assignment) = get("gencost") {
let costs = parse_rows(costs_assignment, "gencost", rows::gencost_row, warnings)?;
let n = gens.len();
if costs.len() != n && costs.len() != 2 * n {
let (start, end) = costs_assignment.range();
warnings.enter_record(start, end);
return Err(Error::GenCostCountMismatch {
gens: n,
gencost: costs.len(),
});
}
for (generator, cost) in gens.iter_mut().zip(costs) {
generator.cost = Some(cost);
}
}
Ok(gens)
}