Skip to main content

gridoxide_core/
scenario.rs

1//! What a batch varies: per-bus overrides and branch outages against a
2//! shared template, and the errors a batch reports for them.
3//!
4//! Shared by the AC [`batch::BatchSolver`](crate::batch::BatchSolver) and the
5//! DC [`linear::batch::DcBatchSolver`](crate::linear::batch::DcBatchSolver).
6//! Kept apart from either so that `linear` need not depend on the AC batch
7//! solver, which itself sits above `linear`; `batch` re-exports everything
8//! here, so `gridoxide::batch::Scenario` and friends resolve as before.
9
10use std::fmt;
11
12use crate::types::Bus;
13
14/// A per-bus change applied on top of the batch's shared bus template.
15/// `None` fields leave the template's value alone.
16///
17/// Deliberately cannot change `bus_type`: switching `PV`↔`PQ` changes
18/// `n_unknowns`, which invalidates the cached symbolic factorization that
19/// makes batching worth doing at all. A caller that needs per-scenario bus
20/// types wants separate batches, one per type assignment.
21#[derive(Clone, Copy, Debug, Default, PartialEq)]
22pub struct BusOverride {
23    pub bus: usize,
24    pub p_spec: Option<f64>,
25    pub q_spec: Option<f64>,
26    /// Voltage setpoint for a `Slack`/`PV` bus. Ignored for `PQ` buses,
27    /// whose magnitude is an unknown the solver overwrites.
28    pub voltage_mag: Option<f64>,
29}
30
31impl BusOverride {
32    pub fn new(bus: usize) -> Self {
33        Self {
34            bus,
35            ..Default::default()
36        }
37    }
38
39    pub fn p(mut self, p_spec: f64) -> Self {
40        self.p_spec = Some(p_spec);
41        self
42    }
43
44    pub fn q(mut self, q_spec: f64) -> Self {
45        self.q_spec = Some(q_spec);
46        self
47    }
48
49    pub fn vm(mut self, voltage_mag: f64) -> Self {
50        self.voltage_mag = Some(voltage_mag);
51        self
52    }
53}
54
55/// One scenario in a batch: a set of bus-value overrides against the shared
56/// template.
57#[derive(Clone, Debug, Default, PartialEq)]
58pub struct Scenario {
59    pub bus_overrides: Vec<BusOverride>,
60    /// Flat branch indices to take out of service — lines first, then
61    /// transformers, the same space `branch_flow::branch_params` uses.
62    ///
63    /// Honored by
64    /// [`BatchSolver::solve_contingencies`](crate::batch::BatchSolver::solve_contingencies),
65    /// which takes the branch lists an outage needs.
66    /// [`BatchSolver::solve`](crate::batch::BatchSolver::solve) still rejects a non-empty
67    /// list with [`BatchError::OutagesUnsupported`] rather than ignoring it: it
68    /// is handed a finished Y-bus, in which the branches have already been
69    /// summed away and cannot be removed.
70    pub branch_outages: Vec<usize>,
71}
72
73impl Scenario {
74    pub fn new(bus_overrides: Vec<BusOverride>) -> Self {
75        Self {
76            bus_overrides,
77            branch_outages: Vec::new(),
78        }
79    }
80}
81
82/// Builds a scenario scaling every bus's `p_spec`/`q_spec` by `factor` —
83/// the standard time-series/QSTS and Monte Carlo shape, and what
84/// `scripts/bench/bench_batch.py` drives.
85///
86/// Buses with zero injection stay zero, so this touches only buses that
87/// actually carry load or generation.
88pub fn uniform_load_scaling(buses_template: &[Bus], factor: f64) -> Scenario {
89    let overrides = buses_template
90        .iter()
91        .filter(|b| b.p_spec != 0.0 || b.q_spec != 0.0)
92        .map(|b| {
93            BusOverride::new(b.idx)
94                .p(b.p_spec * factor)
95                .q(b.q_spec * factor)
96        })
97        .collect();
98    Scenario::new(overrides)
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum BatchError {
103    /// `Scenario::branch_outages` was non-empty on
104    /// [`BatchSolver::solve`](crate::batch::BatchSolver::solve), which cannot
105    /// honor it. See that field's doc comment, and use
106    /// [`BatchSolver::solve_contingencies`](crate::batch::BatchSolver::solve_contingencies)
107    /// instead.
108    OutagesUnsupported { scenario: usize },
109    /// A `BusOverride::bus` was not a valid index into the bus template.
110    BusOutOfRange {
111        scenario: usize,
112        bus: usize,
113        n_buses: usize,
114    },
115    /// A `Scenario::branch_outages` entry was not a valid flat branch index.
116    BranchOutOfRange {
117        scenario: usize,
118        branch: usize,
119        n_branches: usize,
120    },
121    /// `rayon` refused to build the requested thread pool.
122    ThreadPool(String),
123    /// The reduced susceptance matrix was singular, so no scenario in a
124    /// [`linear::batch::DcBatchSolver`](crate::linear::batch::DcBatchSolver)
125    /// batch can be solved.
126    ///
127    /// Unlike the Newton path — where a scenario that fails is reported *in*
128    /// its own result and never poisons the batch — this is a property of the
129    /// shared topology rather than of any one scenario, so it fails the whole
130    /// call.
131    DcSingular,
132}
133
134impl fmt::Display for BatchError {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            BatchError::OutagesUnsupported { scenario } => write!(
138                f,
139                "scenario {scenario} requests branch outages, which `solve` cannot honor from a \
140                 finished Y-bus — use `BatchSolver::solve_contingencies`, which takes the branch \
141                 lists"
142            ),
143            BatchError::BusOutOfRange {
144                scenario,
145                bus,
146                n_buses,
147            } => write!(
148                f,
149                "scenario {scenario} overrides bus {bus}, but the template has only {n_buses} bus(es)"
150            ),
151            BatchError::BranchOutOfRange {
152                scenario,
153                branch,
154                n_branches,
155            } => write!(
156                f,
157                "scenario {scenario} outages branch {branch}, but the network has only \
158                 {n_branches} branch(es)"
159            ),
160            BatchError::ThreadPool(e) => write!(f, "building the rayon thread pool failed: {e}"),
161            BatchError::DcSingular => write!(
162                f,
163                "the reduced susceptance matrix is singular, so no DC scenario can be solved"
164            ),
165        }
166    }
167}
168
169impl std::error::Error for BatchError {}