use std::ffi::{CStr, c_char, c_int};
use super::types::{
GridoxideBackend, GridoxideBus, GridoxideLine, GridoxideOptions, GridoxideShunt,
GridoxideTransformer,
};
use super::{
GridoxideIslandStatus, Status, guard, invalid, island_status_code, set_error, write_slice,
};
use crate::branch_flow::{BranchParams, Terminal, branch_params, bus_voltages, terminal_flow};
use crate::network::{ShuntAdm, YBusSparse, build_ybus, stamp_shunts};
use crate::outerloop::SlackDistribution;
use crate::solver::{IslandReport, IslandStatus, JacobianBackend, PersistentSolver, SolveStats};
use crate::types::{Bus, Line, Transformer};
pub struct GridoxidePowerFlow {
template: Vec<Bus>,
buses: Vec<Bus>,
ybus: YBusSparse,
branches: Vec<BranchParams>,
solver: PersistentSolver,
options: GridoxideOptions,
stats: Option<SolveStats>,
islands: Vec<IslandReport>,
slack_shift: Vec<f64>,
}
fn backend_of(backend: GridoxideBackend) -> Result<JacobianBackend, String> {
match backend {
GridoxideBackend::Scalar => Ok(JacobianBackend::Scalar),
GridoxideBackend::Block => Ok(JacobianBackend::Block),
GridoxideBackend::KluNative => Ok(JacobianBackend::KluNative),
#[cfg(feature = "klu")]
GridoxideBackend::Klu => Ok(JacobianBackend::Klu),
#[cfg(not(feature = "klu"))]
GridoxideBackend::Klu => {
Err("backend KLU needs gridoxide built with the `klu` feature".to_string())
}
#[cfg(feature = "pardiso")]
GridoxideBackend::Pardiso => Ok(JacobianBackend::Pardiso),
#[cfg(not(feature = "pardiso"))]
GridoxideBackend::Pardiso => {
Err("backend PARDISO needs gridoxide built with the `pardiso` feature".to_string())
}
}
}
impl GridoxidePowerFlow {
fn assemble(
buses: Vec<Bus>,
lines: Vec<Line>,
transformers: Vec<Transformer>,
shunts: Vec<ShuntAdm>,
options: GridoxideOptions,
) -> Result<Self, String> {
let backend = backend_of(options.backend)?;
let mut ybus = build_ybus(buses.len(), &lines, &transformers);
stamp_shunts(&mut ybus, &shunts);
let ybus = ybus.finish();
let branches = branch_params(&lines, &transformers);
Ok(Self {
buses: buses.clone(),
template: buses,
ybus,
branches,
solver: PersistentSolver::new(backend),
options,
stats: None,
islands: Vec::new(),
slack_shift: Vec::new(),
})
}
fn finish(&mut self, islands: Vec<IslandReport>, stats: SolveStats) -> Status {
self.islands = islands;
self.stats = Some(stats);
let worst = self.islands.iter().map(|i| i.status).find(|s| {
matches!(
s,
IslandStatus::Singular | IslandStatus::MaxIterationsReached
)
});
match worst {
Some(IslandStatus::Singular) => {
set_error("the Jacobian was singular; the returned state is not a solution");
Status::Singular
}
Some(IslandStatus::MaxIterationsReached) => {
set_error(format!(
"power flow did not converge within {} iterations (largest mismatch {:.3e})",
self.options.max_iterations,
self.stats
.as_ref()
.map(|s| s.final_mismatch())
.unwrap_or(f64::NAN)
));
Status::NotConverged
}
_ => Status::Ok,
}
}
}
unsafe fn handle_ref<'a>(
handle: *const GridoxidePowerFlow,
) -> Result<&'a GridoxidePowerFlow, Status> {
if handle.is_null() {
return Err(invalid("handle is null"));
}
Ok(unsafe { &*handle })
}
unsafe fn handle_mut<'a>(
handle: *mut GridoxidePowerFlow,
) -> Result<&'a mut GridoxidePowerFlow, Status> {
if handle.is_null() {
return Err(invalid("handle is null"));
}
Ok(unsafe { &mut *handle })
}
fn options_or_default(options: *const GridoxideOptions) -> GridoxideOptions {
if options.is_null() {
GridoxideOptions::default()
} else {
unsafe { *options }
}
}
fn build_from_pgm(
text: &str,
options: GridoxideOptions,
) -> Result<GridoxidePowerFlow, (Status, String)> {
let input: crate::pgm::PgmInput = serde_json::from_str(text)
.map_err(|e| (Status::Parse, format!("parsing PGM JSON: {e}")))?;
let id_to_idx = crate::pgm::node_id_to_idx(&input);
let shunts = crate::pgm::pgm_shunts_1ph(&input, &id_to_idx, options.s_base_va);
let (buses, lines, transformers) =
crate::pgm::pgm_to_buses_and_branches(input, options.s_base_va, options.frequency_hz);
GridoxidePowerFlow::assemble(buses, lines, transformers, shunts, options)
.map_err(|e| (Status::InvalidArgument, e))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_from_pgm_file(
path: *const c_char,
options: *const GridoxideOptions,
out: *mut *mut GridoxidePowerFlow,
) -> Status {
guard(|| {
if out.is_null() {
return invalid("out pointer is null");
}
if path.is_null() {
return invalid("path is null");
}
let path = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(p) => p,
Err(_) => return invalid("path is not valid UTF-8"),
};
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
set_error(format!("reading {path}: {e}"));
return Status::Io;
}
};
match build_from_pgm(&text, options_or_default(options)) {
Ok(model) => {
unsafe { *out = Box::into_raw(Box::new(model)) };
Status::Ok
}
Err((status, message)) => {
set_error(message);
status
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_from_pgm_string(
json: *const c_char,
len: usize,
options: *const GridoxideOptions,
out: *mut *mut GridoxidePowerFlow,
) -> Status {
guard(|| {
if out.is_null() {
return invalid("out pointer is null");
}
if json.is_null() {
return invalid("json pointer is null");
}
let bytes = unsafe { std::slice::from_raw_parts(json as *const u8, len) };
let text = match std::str::from_utf8(bytes) {
Ok(t) => t,
Err(_) => return invalid("json is not valid UTF-8"),
};
match build_from_pgm(text, options_or_default(options)) {
Ok(model) => {
unsafe { *out = Box::into_raw(Box::new(model)) };
Status::Ok
}
Err((status, message)) => {
set_error(message);
status
}
}
})
}
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn gridoxide_powerflow_from_arrays(
buses: *const GridoxideBus,
n_buses: usize,
lines: *const GridoxideLine,
n_lines: usize,
transformers: *const GridoxideTransformer,
n_transformers: usize,
shunts: *const GridoxideShunt,
n_shunts: usize,
options: *const GridoxideOptions,
out: *mut *mut GridoxidePowerFlow,
) -> Status {
guard(|| {
if out.is_null() {
return invalid("out pointer is null");
}
if n_buses == 0 {
return invalid("a network needs at least one bus");
}
if buses.is_null() {
return invalid("bus array is null but n_buses is nonzero");
}
if (n_lines > 0 && lines.is_null())
|| (n_transformers > 0 && transformers.is_null())
|| (n_shunts > 0 && shunts.is_null())
{
return invalid("an array is null but its count is nonzero");
}
let bus_slice = unsafe { std::slice::from_raw_parts(buses, n_buses) };
let line_slice = if n_lines == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(lines, n_lines) }
};
let tx_slice = if n_transformers == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(transformers, n_transformers) }
};
let shunt_slice = if n_shunts == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(shunts, n_shunts) }
};
for (k, line) in line_slice.iter().enumerate() {
if line.from >= n_buses || line.to >= n_buses {
return invalid(format!(
"line {k} joins buses {} and {}, but there are only {n_buses}",
line.from, line.to
));
}
}
for (k, tx) in tx_slice.iter().enumerate() {
if tx.from >= n_buses || tx.to >= n_buses {
return invalid(format!(
"transformer {k} joins buses {} and {}, but there are only {n_buses}",
tx.from, tx.to
));
}
}
for (k, shunt) in shunt_slice.iter().enumerate() {
if shunt.at >= n_buses {
return invalid(format!(
"shunt {k} sits at bus {}, but there are only {n_buses}",
shunt.at
));
}
}
let model = GridoxidePowerFlow::assemble(
bus_slice
.iter()
.enumerate()
.map(|(i, b)| b.to_bus(i))
.collect(),
line_slice.iter().map(|l| (*l).into()).collect(),
tx_slice.iter().map(|t| (*t).into()).collect(),
shunt_slice.iter().map(|s| (*s).into()).collect(),
options_or_default(options),
);
match model {
Ok(model) => {
unsafe { *out = Box::into_raw(Box::new(model)) };
Status::Ok
}
Err(message) => invalid(message),
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_free(handle: *mut GridoxidePowerFlow) {
if handle.is_null() {
return;
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
drop(unsafe { Box::from_raw(handle) });
}));
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_solve(handle: *mut GridoxidePowerFlow) -> Status {
guard(|| {
let model = match unsafe { handle_mut(handle) } {
Ok(m) => m,
Err(status) => return status,
};
model.buses.clone_from(&model.template);
model.slack_shift.clear();
let (islands, stats) = model.solver.solve_with_stats(
&mut model.buses,
&model.ybus,
model.options.tolerance,
model.options.max_iterations,
);
model.finish(islands, stats)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_solve_enforcing_q_limits(
handle: *mut GridoxidePowerFlow,
max_outer_iterations: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_mut(handle) } {
Ok(m) => m,
Err(status) => return status,
};
if max_outer_iterations == 0 {
return invalid("max_outer_iterations must be at least 1");
}
model.buses.clone_from(&model.template);
model.slack_shift.clear();
let backend = match backend_of(model.options.backend) {
Ok(b) => b,
Err(message) => return invalid(message),
};
let mut qlim = crate::outerloop::ReactiveLimits::new();
let islands = {
let mut ctx = crate::outerloop::SolveContext::new(&mut model.buses, &mut model.ybus);
let mut list: Vec<&mut dyn crate::outerloop::OuterLoop> = vec![&mut qlim];
crate::outerloop::solve_with_loops(
&mut ctx,
model.options.tolerance,
model.options.max_iterations,
backend,
&mut list,
max_outer_iterations,
)
.0
};
model.solver.reset();
let stats = stats_from_islands(&islands);
model.finish(islands, stats)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_solve_distributing_slack(
handle: *mut GridoxidePowerFlow,
factors: *const f64,
n_factors: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_mut(handle) } {
Ok(m) => m,
Err(status) => return status,
};
model.buses.clone_from(&model.template);
let distribution = if factors.is_null() {
SlackDistribution::uniform(&model.buses)
} else {
if n_factors != model.buses.len() {
return invalid(format!(
"factors holds {n_factors} weights, but this network has {} buses",
model.buses.len()
));
}
let weights = unsafe { std::slice::from_raw_parts(factors, n_factors) };
if weights.iter().any(|w| !w.is_finite() || *w < 0.0) {
return invalid("participation weights must be finite and non-negative");
}
SlackDistribution::from_weights(weights.to_vec())
};
let backend = match backend_of(model.options.backend) {
Ok(b) => b,
Err(message) => return invalid(message),
};
let cap = distribution.max_outer_iter;
let mut slack = crate::outerloop::DistributedSlack::new(distribution);
let islands = {
let mut ctx = crate::outerloop::SolveContext::new(&mut model.buses, &mut model.ybus);
let mut list: Vec<&mut dyn crate::outerloop::OuterLoop> = vec![&mut slack];
crate::outerloop::solve_with_loops(
&mut ctx,
model.options.tolerance,
model.options.max_iterations,
backend,
&mut list,
cap,
)
.0
};
let report = slack.into_report();
model.slack_shift = report.shift;
model.solver.reset();
let stats = stats_from_islands(&islands);
let status = model.finish(islands, stats);
if status == Status::Ok && !report.converged {
set_error(
"the slack-distribution outer loop hit its iteration cap with the slack still \
off its schedule",
);
return Status::NotConverged;
}
status
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_solve_with_controls(
handle: *mut GridoxidePowerFlow,
enforce_q_limits: i32,
distribute_slack: i32,
slack_factors: *const f64,
n_factors: usize,
max_outer_iterations: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_mut(handle) } {
Ok(m) => m,
Err(status) => return status,
};
if max_outer_iterations == 0 {
return invalid("max_outer_iterations must be at least 1");
}
model.buses.clone_from(&model.template);
model.slack_shift.clear();
let distribution = if distribute_slack == 0 {
None
} else if slack_factors.is_null() {
Some(crate::outerloop::SlackDistribution::uniform(&model.buses))
} else {
if n_factors != model.buses.len() {
return invalid(format!(
"factors holds {n_factors} weights, but this network has {} buses",
model.buses.len()
));
}
let weights = unsafe { std::slice::from_raw_parts(slack_factors, n_factors) };
if weights.iter().any(|w| !w.is_finite() || *w < 0.0) {
return invalid("participation weights must be finite and non-negative");
}
Some(crate::outerloop::SlackDistribution::from_weights(
weights.to_vec(),
))
};
let backend = match backend_of(model.options.backend) {
Ok(b) => b,
Err(message) => return invalid(message),
};
let mut slack = distribution.map(crate::outerloop::DistributedSlack::new);
let mut qlim = (enforce_q_limits != 0).then(crate::outerloop::ReactiveLimits::new);
let islands = {
let mut ctx = crate::outerloop::SolveContext::new(&mut model.buses, &mut model.ybus);
let mut list: Vec<&mut dyn crate::outerloop::OuterLoop> = Vec::new();
if let Some(l) = slack.as_mut() {
list.push(l);
}
if let Some(l) = qlim.as_mut() {
list.push(l);
}
crate::outerloop::solve_with_loops(
&mut ctx,
model.options.tolerance,
model.options.max_iterations,
backend,
&mut list,
max_outer_iterations,
)
.0
};
if let Some(l) = slack {
model.slack_shift = l.into_report().shift;
}
model.solver.reset();
let stats = stats_from_islands(&islands);
model.finish(islands, stats)
})
}
fn stats_from_islands(islands: &[IslandReport]) -> SolveStats {
SolveStats::from_loop(
if islands.iter().any(|i| i.status == IslandStatus::Singular) {
crate::solver::SolveStatus::Singular
} else if islands
.iter()
.any(|i| i.status == IslandStatus::MaxIterationsReached)
{
crate::solver::SolveStatus::MaxIterationsReached
} else {
crate::solver::SolveStatus::Converged
},
Vec::new(),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_bus_count(handle: *const GridoxidePowerFlow) -> usize {
unsafe { handle_ref(handle) }
.map(|m| m.buses.len())
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_branch_count(
handle: *const GridoxidePowerFlow,
) -> usize {
unsafe { handle_ref(handle) }
.map(|m| m.branches.len())
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_voltage_magnitude(
handle: *const GridoxidePowerFlow,
out: *mut f64,
len: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_ref(handle) } {
Ok(m) => m,
Err(status) => return status,
};
let values: Vec<f64> = model.buses.iter().map(|b| b.voltage_mag).collect();
unsafe { write_slice(&values, out, len, "voltage_magnitude") }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_voltage_angle(
handle: *const GridoxidePowerFlow,
out: *mut f64,
len: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_ref(handle) } {
Ok(m) => m,
Err(status) => return status,
};
let values: Vec<f64> = model.buses.iter().map(|b| b.voltage_ang).collect();
unsafe { write_slice(&values, out, len, "voltage_angle") }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_branch_flow(
handle: *const GridoxidePowerFlow,
terminal: c_int,
p_out: *mut f64,
q_out: *mut f64,
len: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_ref(handle) } {
Ok(m) => m,
Err(status) => return status,
};
if model.stats.is_none() {
return invalid("solve before reading branch flows");
}
let terminal = match terminal {
0 => Terminal::From,
1 => Terminal::To,
other => return invalid(format!("terminal must be 0 (from) or 1 (to), got {other}")),
};
if len != model.branches.len() {
return invalid(format!(
"branch_flow: buffer holds {len} entries, but this network has {} branches",
model.branches.len()
));
}
let v = bus_voltages(&model.buses);
let (mut p, mut q) = (Vec::with_capacity(len), Vec::with_capacity(len));
for branch in &model.branches {
let (bp, bq) = terminal_flow(branch, terminal, &v);
p.push(bp);
q.push(bq);
}
if !p_out.is_null() {
let status = unsafe { write_slice(&p, p_out, len, "branch_flow (P)") };
if status != Status::Ok {
return status;
}
}
if !q_out.is_null() {
let status = unsafe { write_slice(&q, q_out, len, "branch_flow (Q)") };
if status != Status::Ok {
return status;
}
}
Status::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_slack_shift(
handle: *const GridoxidePowerFlow,
out: *mut f64,
len: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_ref(handle) } {
Ok(m) => m,
Err(status) => return status,
};
if model.slack_shift.is_empty() {
set_error("the last solve did not distribute slack, so there is no shift to report");
return Status::NoAnswer;
}
unsafe { write_slice(&model.slack_shift, out, len, "slack_shift") }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_iterations(
handle: *const GridoxidePowerFlow,
) -> usize {
unsafe { handle_ref(handle) }
.ok()
.and_then(|m| m.stats.as_ref())
.map(|s| s.iterations())
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_max_mismatch(
handle: *const GridoxidePowerFlow,
) -> f64 {
unsafe { handle_ref(handle) }
.ok()
.and_then(|m| m.stats.as_ref())
.map(|s| s.final_mismatch())
.unwrap_or(f64::NAN)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_island_count(
handle: *const GridoxidePowerFlow,
) -> usize {
unsafe { handle_ref(handle) }
.map(|m| m.islands.len())
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_island_status(
handle: *const GridoxidePowerFlow,
island: usize,
out: *mut GridoxideIslandStatus,
) -> Status {
guard(|| {
let model = match unsafe { handle_ref(handle) } {
Ok(m) => m,
Err(status) => return status,
};
if out.is_null() {
return invalid("out pointer is null");
}
let Some(report) = model.islands.get(island) else {
return invalid(format!(
"island {island} is out of range; the last solve found {}",
model.islands.len()
));
};
unsafe { *out = island_status_code(report.status) };
Status::Ok
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_island_bus_count(
handle: *const GridoxidePowerFlow,
island: usize,
) -> usize {
unsafe { handle_ref(handle) }
.ok()
.and_then(|m| m.islands.get(island))
.map(|i| i.bus_indices.len())
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gridoxide_powerflow_island_buses(
handle: *const GridoxidePowerFlow,
island: usize,
out: *mut usize,
len: usize,
) -> Status {
guard(|| {
let model = match unsafe { handle_ref(handle) } {
Ok(m) => m,
Err(status) => return status,
};
if out.is_null() {
return invalid("out pointer is null");
}
let Some(report) = model.islands.get(island) else {
return invalid(format!(
"island {island} is out of range; the last solve found {}",
model.islands.len()
));
};
if len != report.bus_indices.len() {
return invalid(format!(
"island_buses: buffer holds {len} entries, but island {island} has {}",
report.bus_indices.len()
));
}
unsafe { std::ptr::copy_nonoverlapping(report.bus_indices.as_ptr(), out, len) };
Status::Ok
})
}