Skip to main content

powerio_prob/instance/
scuc_inputs.rs

1//! Source neutral inputs for an AC security constrained unit commitment calculation.
2//!
3//! These records describe the scheduling, reserve, and contingency data that
4//! do not belong on [`powerio_tx::BalancedNetwork`]. They use stable component
5//! identities and preserve the nested time structure of the source problem.
6//! Solver row numbers, packed arrays, and derived window memberships belong in
7//! solver preparation code, not in a calculation instance.
8
9use powerio_core::ComponentId;
10use serde::{Deserialize, Serialize};
11
12/// Whether a simple dispatchable device produces or consumes power.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum ScucDeviceKind {
18    Producer,
19    Consumer,
20}
21
22/// One downtime dependent startup cost adjustment.
23#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25#[non_exhaustive]
26pub struct ScucStartupCostAdjustment {
27    /// Cost adjustment in dollars.
28    pub cost: f64,
29    /// Maximum preceding down time for this adjustment, in hours.
30    pub maximum_down_time: f64,
31}
32
33/// A limit on how many times one device can start within a time window.
34#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
35#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
36#[non_exhaustive]
37pub struct ScucStartupLimit {
38    /// Window start, in hours from the beginning of the horizon.
39    pub start_time: f64,
40    /// Window end, in hours from the beginning of the horizon.
41    pub end_time: f64,
42    pub maximum_startups: u64,
43}
44
45/// An energy requirement over one time window.
46#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[non_exhaustive]
49pub struct ScucEnergyRequirement {
50    /// Window start, in hours from the beginning of the horizon.
51    pub start_time: f64,
52    /// Window end, in hours from the beginning of the horizon.
53    pub end_time: f64,
54    /// Energy bound in per unit, as defined by the GO Challenge 3 data model.
55    pub energy: f64,
56}
57
58/// Initial commitment duration data not carried by the electrical network.
59#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61#[non_exhaustive]
62pub struct ScucInitialCommitment {
63    /// Accumulated in service time before the first interval, in hours.
64    pub accumulated_up_time: f64,
65    /// Accumulated out of service time before the first interval, in hours.
66    pub accumulated_down_time: f64,
67}
68
69/// Active power ramp limits for one dispatchable device, in per unit per hour.
70#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[non_exhaustive]
73pub struct ScucRampLimits {
74    pub up: f64,
75    pub down: f64,
76    pub startup: f64,
77    pub shutdown: f64,
78}
79
80/// Reserve quantity limits for one dispatchable device, in per unit.
81#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
82#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83#[non_exhaustive]
84pub struct ScucReserveLimits {
85    pub regulation_up: f64,
86    pub regulation_down: f64,
87    pub synchronized: f64,
88    pub nonsynchronized: f64,
89    pub ramping_up_online: f64,
90    pub ramping_down_online: f64,
91    pub ramping_up_offline: f64,
92    pub ramping_down_offline: f64,
93}
94
95/// Additional active and reactive power capability relation for one device.
96#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[serde(rename_all = "snake_case", tag = "kind")]
99#[non_exhaustive]
100pub enum ScucReactiveCapability {
101    None,
102    Linear {
103        /// Reactive power at zero active power, in per unit.
104        reactive_power_at_zero_active_power: f64,
105        slope: f64,
106    },
107    Bounded {
108        /// Lower reactive power intercept at zero active power, in per unit.
109        reactive_power_at_zero_active_power_min: f64,
110        /// Upper reactive power intercept at zero active power, in per unit.
111        reactive_power_at_zero_active_power_max: f64,
112        slope_min: f64,
113        slope_max: f64,
114    },
115}
116
117/// One piecewise linear active energy cost block.
118#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120#[non_exhaustive]
121pub struct ScucEnergyCostBlock {
122    /// Marginal cost in $/(p.u. h).
123    pub marginal_cost: f64,
124    /// Active power width of the block, in per unit.
125    pub block_size: f64,
126}
127
128/// Reserve costs for one device and one interval, in $/(p.u. h).
129#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
130#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
131#[non_exhaustive]
132pub struct ScucReserveCosts {
133    pub regulation_up: f64,
134    pub regulation_down: f64,
135    pub synchronized: f64,
136    pub nonsynchronized: f64,
137    pub ramping_up_online: f64,
138    pub ramping_down_online: f64,
139    pub ramping_up_offline: f64,
140    pub ramping_down_offline: f64,
141    pub reactive_up: f64,
142    pub reactive_down: f64,
143}
144
145/// Time varying inputs for one dispatchable device and one interval.
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
148#[non_exhaustive]
149pub struct ScucDevicePeriod {
150    pub on_status_min: bool,
151    pub on_status_max: bool,
152    /// Active power lower bound in per unit on the instance system base.
153    pub active_power_min: f64,
154    /// Active power upper bound in per unit on the instance system base.
155    pub active_power_max: f64,
156    /// Reactive power lower bound in per unit on the instance system base.
157    pub reactive_power_min: f64,
158    /// Reactive power upper bound in per unit on the instance system base.
159    pub reactive_power_max: f64,
160    pub energy_cost_blocks: Vec<ScucEnergyCostBlock>,
161    pub reserve_costs: ScucReserveCosts,
162}
163
164/// Scheduling and cost inputs for one simple dispatchable device.
165#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
167#[non_exhaustive]
168pub struct ScucDevice {
169    /// The corresponding `generator` or `load` in the balanced network.
170    pub id: ComponentId,
171    pub kind: ScucDeviceKind,
172    /// Fixed operating cost in dollars.
173    pub on_cost: f64,
174    /// Base startup cost in dollars.
175    pub startup_cost: f64,
176    pub startup_cost_adjustments: Vec<ScucStartupCostAdjustment>,
177    /// Shutdown cost in dollars.
178    pub shutdown_cost: f64,
179    pub startup_limits: Vec<ScucStartupLimit>,
180    pub energy_upper_bounds: Vec<ScucEnergyRequirement>,
181    pub energy_lower_bounds: Vec<ScucEnergyRequirement>,
182    /// Minimum in service time after startup, in hours.
183    pub minimum_up_time: f64,
184    /// Minimum out of service time after shutdown, in hours.
185    pub minimum_down_time: f64,
186    pub ramp_limits: ScucRampLimits,
187    pub reserve_limits: ScucReserveLimits,
188    /// Commitment immediately before the first interval.
189    pub initial_on_status: bool,
190    pub initial_commitment: ScucInitialCommitment,
191    pub reactive_capability: ScucReactiveCapability,
192    /// Values in the same chronological order as [`ScucInputs::interval_durations`].
193    pub periods: Vec<ScucDevicePeriod>,
194}
195
196/// Discrete step limits for one shunt in the balanced network.
197#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
199#[non_exhaustive]
200pub struct ScucShunt {
201    pub id: ComponentId,
202    /// Conductance added by one step, in per unit on the instance system base.
203    pub conductance_per_step: f64,
204    /// Susceptance added by one step, in per unit on the instance system base.
205    pub susceptance_per_step: f64,
206    pub step_min: i64,
207    pub step_max: i64,
208    pub initial_step: i64,
209}
210
211/// Connection and disconnection costs for one switchable AC branch.
212#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
213#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
214#[non_exhaustive]
215pub struct ScucBranchSwitchingCost {
216    /// The corresponding `branch` or `transformer` in the balanced network.
217    pub id: ComponentId,
218    /// Connection cost in dollars.
219    pub connection_cost: f64,
220    /// Disconnection cost in dollars.
221    pub disconnection_cost: f64,
222}
223
224/// Tap ratio and phase shift bounds for one two winding transformer.
225#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
226#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
227#[non_exhaustive]
228pub struct ScucTransformerControl {
229    pub id: ComponentId,
230    /// Off nominal tap ratio lower bound in per unit.
231    pub tap_ratio_min: f64,
232    /// Off nominal tap ratio upper bound in per unit.
233    pub tap_ratio_max: f64,
234    /// Phase shift bounds in radians.
235    pub phase_shift_min: f64,
236    pub phase_shift_max: f64,
237}
238
239/// Active reserve requirements and violation costs for one zone.
240#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
242#[non_exhaustive]
243pub struct ScucActiveReserveZone {
244    pub id: ComponentId,
245    /// Buses assigned to this zone by stable source identity.
246    pub buses: Vec<ComponentId>,
247    pub regulation_up_requirement_fraction: f64,
248    pub regulation_down_requirement_fraction: f64,
249    pub synchronized_requirement_fraction: f64,
250    pub nonsynchronized_requirement_fraction: f64,
251    pub ramping_up_requirement: Vec<f64>,
252    pub ramping_down_requirement: Vec<f64>,
253    pub regulation_up_violation_cost: f64,
254    pub regulation_down_violation_cost: f64,
255    pub synchronized_violation_cost: f64,
256    pub nonsynchronized_violation_cost: f64,
257    pub ramping_up_violation_cost: f64,
258    pub ramping_down_violation_cost: f64,
259}
260
261/// Reactive reserve requirements and violation costs for one zone.
262#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264#[non_exhaustive]
265pub struct ScucReactiveReserveZone {
266    pub id: ComponentId,
267    /// Buses assigned to this zone by stable source identity.
268    pub buses: Vec<ComponentId>,
269    pub reactive_up_requirement: Vec<f64>,
270    pub reactive_down_requirement: Vec<f64>,
271    pub reactive_up_violation_cost: f64,
272    pub reactive_down_violation_cost: f64,
273}
274
275/// One named equipment outage.
276#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
277#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
278#[non_exhaustive]
279pub struct ScucContingency {
280    pub id: ComponentId,
281    /// Challenge 3 requires exactly one AC line, transformer, or DC line.
282    pub components: Vec<ComponentId>,
283}
284
285/// The four required violation costs from a Challenge 3 input/problem data file.
286#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
287#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
288#[non_exhaustive]
289pub struct ScucViolationCosts {
290    /// Active power balance violation cost in $/(p.u. h).
291    pub active_power_balance: f64,
292    /// Reactive power balance violation cost in $/(p.u. h).
293    pub reactive_power_balance: f64,
294    /// Branch thermal limit violation cost in $/(p.u. h).
295    pub branch_thermal_limit: f64,
296    /// Energy requirement violation cost in $/(p.u. h).
297    pub energy_requirement: f64,
298}
299
300/// Scheduling, reserve, and contingency inputs for an AC SCUC calculation.
301#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[non_exhaustive]
304pub struct ScucInputs {
305    /// Interval durations in chronological order, in hours.
306    pub interval_durations: Vec<f64>,
307    pub devices: Vec<ScucDevice>,
308    pub shunts: Vec<ScucShunt>,
309    pub branch_switching_costs: Vec<ScucBranchSwitchingCost>,
310    pub transformer_controls: Vec<ScucTransformerControl>,
311    pub active_reserve_zones: Vec<ScucActiveReserveZone>,
312    pub reactive_reserve_zones: Vec<ScucReactiveReserveZone>,
313    pub contingencies: Vec<ScucContingency>,
314    pub violation_costs: ScucViolationCosts,
315}
316
317impl ScucInputs {
318    /// Producing devices in source order.
319    pub fn producers(&self) -> impl Iterator<Item = &ScucDevice> {
320        self.devices
321            .iter()
322            .filter(|device| device.kind == ScucDeviceKind::Producer)
323    }
324
325    /// Consuming devices in source order.
326    pub fn consumers(&self) -> impl Iterator<Item = &ScucDevice> {
327        self.devices
328            .iter()
329            .filter(|device| device.kind == ScucDeviceKind::Consumer)
330    }
331
332    /// Find a dispatchable device by its source UID.
333    #[must_use]
334    pub fn device(&self, uid: &str) -> Option<&ScucDevice> {
335        self.devices
336            .iter()
337            .find(|device| device.id.local_id() == uid)
338    }
339
340    /// Find a shunt by its source UID.
341    #[must_use]
342    pub fn shunt(&self, uid: &str) -> Option<&ScucShunt> {
343        self.shunts.iter().find(|shunt| shunt.id.local_id() == uid)
344    }
345
346    /// Find a contingency by its source UID.
347    #[must_use]
348    pub fn contingency(&self, uid: &str) -> Option<&ScucContingency> {
349        self.contingencies
350            .iter()
351            .find(|contingency| contingency.id.local_id() == uid)
352    }
353
354    /// Interval durations in chronological order, in hours.
355    #[must_use]
356    pub fn interval_durations(&self) -> &[f64] {
357        &self.interval_durations
358    }
359}