use num_complex::Complex;
use crate::lu::{LuBackend, SparseLu};
use super::dae::residual;
use super::events::{DynamicsWarning, Event, EventError, EventKind, Relay, RelayAction};
use super::models::ModelJacobian;
use super::{DynamicSystem, DynamicsOptions, DynamicsReport, DynamicsStatus, Trajectory};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Rule {
Trapezoidal,
BackwardEuler,
}
impl Rule {
pub fn coefficients(self) -> (f64, f64) {
match self {
Rule::Trapezoidal => (0.5, 0.5),
Rule::BackwardEuler => (1.0, 0.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum StepOutcome {
Converged { iterations: usize },
NotConverged,
Singular,
}
struct Workspace {
scratch: Vec<ModelJacobian>,
values: Vec<f64>,
f_scratch: Vec<f64>,
res: Vec<f64>,
rhs: Vec<f64>,
newton_iterations: usize,
}
impl Workspace {
fn new(system: &DynamicSystem) -> Self {
let layout = system.pattern.layout();
Self {
scratch: (0..layout.n_devices())
.map(|d| ModelJacobian::zeros(layout.dev_len[d]))
.collect(),
values: Vec::with_capacity(system.pattern.nnz()),
f_scratch: vec![0.0; layout.n_diff],
res: vec![0.0; layout.n()],
rhs: vec![0.0; layout.n()],
newton_iterations: 0,
}
}
}
#[allow(clippy::too_many_arguments)]
fn newton(
system: &DynamicSystem,
solver: &mut SparseLu,
ws: &mut Workspace,
x_prev: &[f64],
f0: &[f64],
x: &mut [f64],
v: &mut [Complex<f64>],
ha: f64,
hb: f64,
pin_states: bool,
tol: f64,
max_newton: usize,
) -> StepOutcome {
let pattern = &system.pattern;
let layout = pattern.layout();
let n = layout.n();
for iteration in 1..=max_newton {
residual(
pattern,
&system.models,
x_prev,
f0,
x,
v,
&system.v_fixed,
ha,
hb,
&system.ybus,
&mut ws.f_scratch,
&mut ws.res,
);
let norm = ws.res.iter().fold(0.0f64, |m, r| m.max(r.abs()));
if norm < tol {
return StepOutcome::Converged {
iterations: iteration - 1,
};
}
pattern.fill(
&system.models,
x,
v,
ha,
&system.ybus,
&mut ws.scratch,
&mut ws.values,
);
for k in 0..n {
ws.rhs[k] = -ws.res[k];
}
if !solver.is_analyzed() && !solver.analyze(n, &pattern.to_triplets(&ws.values)) {
return StepOutcome::Singular;
}
let Some(dz) = solver.factor_and_solve_values(&ws.values, &ws.rhs) else {
return StepOutcome::Singular;
};
if !pin_states {
for i in 0..layout.n_diff {
x[i] += dz[i];
}
}
for i in 0..layout.n_bus {
v[i] += Complex::new(dz[layout.v_re(i)], dz[layout.v_im(i)]);
}
ws.newton_iterations += 1;
}
StepOutcome::NotConverged
}
fn apply(system: &mut DynamicSystem, kind: EventKind) -> Result<(), EventError> {
let n_bus = system.v0.len();
let n_branch = system.outaged.len();
if let Some(bus) = kind.bus().filter(|&b| b >= n_bus) {
return Err(EventError::BusOutOfRange { bus, n_bus });
}
match kind {
EventKind::BusFault { bus, y } => system.fault_y[bus] = y,
EventKind::ClearFault { bus } => system.fault_y[bus] = Complex::new(0.0, 0.0),
EventKind::BranchTrip { branch } | EventKind::BranchClose { branch } => {
if branch >= n_branch {
return Err(EventError::BranchOutOfRange { branch, n_branch });
}
system.outaged[branch] = matches!(kind, EventKind::BranchTrip { .. });
}
EventKind::UnitTrip { unit } | EventKind::UnitClose { unit } => {
let n_unit = system.models.len();
if unit >= n_unit {
return Err(EventError::UnitOutOfRange { unit, n_unit });
}
system.models[unit].set_connected(matches!(kind, EventKind::UnitClose { .. }));
}
EventKind::LoadStep { bus, ds } => {
let v_sq = system.v_fixed[bus].norm_sqr();
if v_sq != 0.0 {
system.load_y[bus] += -ds.conj() / v_sq;
}
}
}
Ok(())
}
pub fn integrate(system: &mut DynamicSystem, opts: &DynamicsOptions) -> DynamicsReport {
let layout = system.pattern.layout().clone();
let mut solver = SparseLu::new(opts.backend.lu_backend());
let mut ws = Workspace::new(system);
let mut f0 = vec![0.0; layout.n_diff];
let mut x = system.x0.clone();
let mut v = system.v0.clone();
let mut traj = Trajectory::new(system.observable_names());
let mut warnings: Vec<DynamicsWarning> = Vec::new();
let mut events: Vec<Event> = opts.events.clone();
events.sort_by(|a, b| {
a.time
.partial_cmp(&b.time)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut next_event = 0usize;
let mut events_applied = 0usize;
let eps = opts.step * 1e-9;
struct Pending {
relay: usize,
crossed_at: f64,
fires_at: f64,
}
let relays: Vec<Relay> = opts.relays.clone();
let mut pending: Vec<Pending> = Vec::new();
let mut spent = vec![false; relays.len()];
let mut relay_actions: Vec<RelayAction> = Vec::new();
let reference = x.clone();
let mut holding = vec![false; relays.len()];
for (i, relay) in relays.iter().enumerate() {
match system.watched(relay.watch, &x, &v, &reference) {
Some(value) => holding[i] = relay.trigger.holds(value),
None => {
warnings.push(DynamicsWarning::RelayDropped {
id: relay.id.clone(),
detail: format!("{:?} names nothing that exists", relay.watch),
});
spent[i] = true;
}
}
}
let mut t = 0.0;
let mut steps = 0usize;
let mut status = DynamicsStatus::Completed;
let mut damping_left = opts.damping_steps;
traj.push(t, &x, &v);
let fire = |system: &mut DynamicSystem,
events: &[Event],
next_event: &mut usize,
t: f64,
x: &mut Vec<f64>,
v: &mut Vec<Complex<f64>>,
solver: &mut SparseLu,
ws: &mut Workspace,
traj: &mut Trajectory,
warnings: &mut Vec<DynamicsWarning>,
events_applied: &mut usize|
-> Option<DynamicsStatus> {
let mut any = false;
while *next_event < events.len() && events[*next_event].time <= t + eps {
let event = events[*next_event];
*next_event += 1;
match apply(system, event.kind) {
Ok(()) => {
any = true;
*events_applied += 1;
}
Err(error) => warnings.push(DynamicsWarning::Skipped {
time: event.time,
error,
}),
}
}
if !any {
return None;
}
system.reassemble();
system.latch(x, v);
let x_prev = x.clone();
let zero = vec![0.0; x.len()];
match newton(
system,
solver,
ws,
&x_prev,
&zero,
x,
v,
0.0,
0.0,
true,
opts.tol,
opts.max_newton,
) {
StepOutcome::Converged { .. } => {}
StepOutcome::NotConverged => return Some(DynamicsStatus::NewtonFailed { time: t }),
StepOutcome::Singular => return Some(DynamicsStatus::Singular { time: t }),
}
traj.push(t, x, v);
for island in system.dead_islands() {
let warning = DynamicsWarning::DeadIsland {
buses: island,
time: t,
};
if !warnings.contains(&warning) {
warnings.push(warning);
}
}
None
};
if let Some(bad) = fire(
system,
&events,
&mut next_event,
t,
&mut x,
&mut v,
&mut solver,
&mut ws,
&mut traj,
&mut warnings,
&mut events_applied,
) {
status = bad;
} else {
if events_applied > 0 {
damping_left = opts.damping_steps;
}
while t < opts.end_time - eps {
let mut h = opts.step.min(opts.end_time - t);
let mut snap: Option<f64> = None;
let consider = |candidate: f64, h: &mut f64, snap: &mut Option<f64>| {
let dt = candidate - t;
if dt > eps && dt < *h {
*h = dt;
*snap = Some(candidate);
}
};
if next_event < events.len() {
consider(events[next_event].time, &mut h, &mut snap);
}
for p in &pending {
consider(p.fires_at, &mut h, &mut snap);
}
let rule = if damping_left > 0 {
Rule::BackwardEuler
} else {
Rule::Trapezoidal
};
let (a, b) = rule.coefficients();
system.latch(&x, &v);
system.derivatives_at(&x, &v, &mut f0);
let x_prev = x.clone();
let v_prev = v.clone();
macro_rules! attempt {
($size:expr) => {{
x.copy_from_slice(&x_prev);
v.copy_from_slice(&v_prev);
for i in 0..layout.n_diff {
x[i] = x_prev[i] + $size * f0[i];
}
newton(
system,
&mut solver,
&mut ws,
&x_prev,
&f0,
&mut x,
&mut v,
$size * a,
$size * b,
false,
opts.tol,
opts.max_newton,
)
}};
}
let outcome = attempt!(h);
match outcome {
StepOutcome::Converged { .. } => {}
StepOutcome::NotConverged => {
status = DynamicsStatus::NewtonFailed { time: t + h };
break;
}
StepOutcome::Singular => {
status = DynamicsStatus::Singular { time: t + h };
break;
}
}
let mut crossed: Option<(usize, f64)> = None;
for (i, relay) in relays.iter().enumerate() {
if spent[i] || holding[i] || pending.iter().any(|p| p.relay == i) {
continue;
}
let before = system
.watched(relay.watch, &x_prev, &v_prev, &reference)
.unwrap_or(0.0);
let after = system
.watched(relay.watch, &x, &v, &reference)
.unwrap_or(0.0);
if relay.trigger.holds(after) && !relay.trigger.holds(before) {
let mut bracket = crate::roots::Illinois {
lo: 0.0,
hi: 1.0,
f_lo: relay.trigger.residual(before),
f_hi: relay.trigger.residual(after),
};
for _ in 0..24 {
if bracket.width() * h < 1e-9 {
break;
}
let theta = bracket.next();
if !matches!(attempt!(theta * h), StepOutcome::Converged { .. }) {
break;
}
let value = system
.watched(relay.watch, &x, &v, &reference)
.unwrap_or(0.0);
bracket.update(theta, relay.trigger.residual(value));
}
let theta = bracket.hi.clamp(1e-9, 1.0);
if crossed.is_none_or(|(_, best)| theta < best) {
crossed = Some((i, theta));
}
}
}
if let Some((_, theta)) = crossed {
h *= theta;
snap = None;
if !matches!(attempt!(h), StepOutcome::Converged { .. }) {
status = DynamicsStatus::NewtonFailed { time: t + h };
break;
}
}
system.project(&mut x);
t = snap.unwrap_or(t + h);
steps += 1;
damping_left = damping_left.saturating_sub(1);
traj.push(t, &x, &v);
let _ = crossed;
let before = events_applied;
if let Some(bad) = fire(
system,
&events,
&mut next_event,
t,
&mut x,
&mut v,
&mut solver,
&mut ws,
&mut traj,
&mut warnings,
&mut events_applied,
) {
status = bad;
break;
}
if events_applied > before {
damping_left = opts.damping_steps;
}
for (i, relay) in relays.iter().enumerate() {
if spent[i] {
continue;
}
let Some(value) = system.watched(relay.watch, &x, &v, &reference) else {
continue;
};
let now = relay.trigger.holds(value);
if now && !holding[i] && !pending.iter().any(|p| p.relay == i) {
pending.push(Pending {
relay: i,
crossed_at: t,
fires_at: t + relay.delay,
});
}
holding[i] = now;
}
pending.retain(|p| holding[p.relay]);
let mut due: Vec<(usize, f64)> = Vec::new();
pending.retain(|p| {
if p.fires_at <= t + eps {
due.push((p.relay, p.crossed_at));
false
} else {
true
}
});
let mut halted = false;
for (i, crossed_at) in due {
let relay = &relays[i];
match apply(system, relay.action) {
Ok(()) => {
system.reassemble();
let x_frozen = x.clone();
let zero = vec![0.0; x.len()];
system.latch(&x, &v);
if !matches!(
newton(
system,
&mut solver,
&mut ws,
&x_frozen,
&zero,
&mut x,
&mut v,
0.0,
0.0,
true,
opts.tol,
opts.max_newton,
),
StepOutcome::Converged { .. }
) {
status = DynamicsStatus::NewtonFailed { time: t };
halted = true;
break;
}
traj.push(t, &x, &v);
relay_actions.push(RelayAction {
id: relay.id.clone(),
crossed_at,
fired_at: t,
action: relay.action,
});
damping_left = opts.damping_steps;
for island in system.dead_islands() {
let warning = DynamicsWarning::DeadIsland {
buses: island,
time: t,
};
if !warnings.contains(&warning) {
warnings.push(warning);
}
}
}
Err(error) => warnings.push(DynamicsWarning::Skipped { time: t, error }),
}
if !relay.repeating {
spent[i] = true;
}
}
if halted {
break;
}
}
}
system.x0 = x;
system.v0 = v;
DynamicsReport {
trajectory: traj,
status,
steps,
newton_iterations: ws.newton_iterations,
events_applied,
warnings,
relay_actions,
}
}
pub fn solve_algebraic(
system: &mut DynamicSystem,
backend: LuBackend,
tol: f64,
max_newton: usize,
) -> StepOutcome {
let mut solver = SparseLu::new(backend);
let mut ws = Workspace::new(system);
let x_prev = system.x0.clone();
let mut x = system.x0.clone();
let mut v = system.v0.clone();
let zero = vec![0.0; x.len()];
let outcome = newton(
system,
&mut solver,
&mut ws,
&x_prev,
&zero,
&mut x,
&mut v,
0.0,
0.0,
true,
tol,
max_newton,
);
if matches!(outcome, StepOutcome::Converged { .. }) {
system.x0 = x;
system.v0 = v;
}
outcome
}