surge_network/network/load.rs
1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! Load representation.
3
4use serde::{Deserialize, Serialize};
5
6/// Load class for planning and demand categorization.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum LoadClass {
9 /// Residential customer load.
10 Residential,
11 /// Commercial customer load.
12 Commercial,
13 /// Industrial customer load.
14 Industrial,
15 /// Agricultural customer load (irrigation, processing).
16 Agricultural,
17 /// Data center load (high power factor, constant demand).
18 DataCenter,
19 /// Electric vehicle charging load.
20 EvCharging,
21 /// Uncategorized load.
22 Other,
23}
24
25/// Winding connection type for fault and unbalanced analysis.
26///
27/// Determines how the load's zero-sequence impedance participates in
28/// short-circuit calculations.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
30pub enum LoadConnection {
31 /// Wye-connected with grounded neutral (zero-sequence current path exists).
32 #[default]
33 WyeGrounded,
34 /// Wye-connected with ungrounded (floating) neutral.
35 WyeUngrounded,
36 /// Delta-connected (no zero-sequence current path).
37 Delta,
38}
39
40/// A load connected to a bus in the transmission network.
41///
42/// Supports ZIP (constant impedance / current / power) voltage dependence,
43/// frequency sensitivity, and composite load modeling (CMPLDW motor fractions).
44/// All power quantities are in MW/MVAr.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Load {
47 // --- identity ---
48 /// Bus number where the load is connected.
49 pub bus: u32,
50 /// Optional load identifier.
51 #[serde(default)]
52 pub id: String,
53 /// Load status (true = in service).
54 pub in_service: bool,
55 /// Whether this load conforms to system-wide scaling forecasts.
56 #[serde(default = "default_true")]
57 pub conforming: bool,
58
59 // --- steady-state injection ---
60 /// Real power demand in MW.
61 pub active_power_demand_mw: f64,
62 /// Reactive power demand in MVAr.
63 pub reactive_power_demand_mvar: f64,
64
65 // --- voltage dependence (ZIP) ---
66 /// Constant-impedance P fraction \[0,1\]. Default 0.
67 #[serde(default)]
68 pub zip_p_impedance_frac: f64,
69 /// Constant-current P fraction \[0,1\]. Default 0.
70 #[serde(default)]
71 pub zip_p_current_frac: f64,
72 /// Constant-power P fraction \[0,1\]. Default 1.
73 #[serde(default = "default_one")]
74 pub zip_p_power_frac: f64,
75 /// Constant-impedance Q fraction \[0,1\]. Default 0.
76 #[serde(default)]
77 pub zip_q_impedance_frac: f64,
78 /// Constant-current Q fraction \[0,1\]. Default 0.
79 #[serde(default)]
80 pub zip_q_current_frac: f64,
81 /// Constant-power Q fraction \[0,1\]. Default 1.
82 #[serde(default = "default_one")]
83 pub zip_q_power_frac: f64,
84
85 // --- frequency dependence ---
86 /// Active power frequency sensitivity (%P per Hz). Default 0.
87 #[serde(default)]
88 pub freq_sensitivity_p_pct_per_hz: f64,
89 /// Reactive power frequency sensitivity (%Q per Hz). Default 0.
90 #[serde(default)]
91 pub freq_sensitivity_q_pct_per_hz: f64,
92
93 // --- composition (CMPLDW bridge) ---
94 /// 3-phase large industrial motor fraction \[0,1\]. Default 0.
95 #[serde(default)]
96 pub frac_motor_a: f64,
97 /// 3-phase commercial motor fraction \[0,1\]. Default 0.
98 #[serde(default)]
99 pub frac_motor_b: f64,
100 /// 1-phase A/C compressor motor fraction \[0,1\]. Default 0.
101 #[serde(default)]
102 pub frac_motor_c: f64,
103 /// 1-phase other motor fraction \[0,1\]. Default 0.
104 #[serde(default)]
105 pub frac_motor_d: f64,
106 /// Power electronic load fraction \[0,1\]. Default 0.
107 #[serde(default)]
108 pub frac_electronic: f64,
109 /// Static (ZIP) load fraction \[0,1\]. Default 1.
110 #[serde(default = "default_one")]
111 pub frac_static: f64,
112
113 // --- classification ---
114 /// Load class for planning.
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub load_class: Option<LoadClass>,
117 /// Winding connection for fault/unbalanced analysis. Default WyeGrounded.
118 #[serde(default)]
119 pub connection: LoadConnection,
120 /// UFLS/UVLS shedding tier (1 = first shed, higher = later).
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub shedding_priority: Option<u32>,
123 /// Ownership entries (PSS/E OWNER field). Single-owner for loads.
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 pub owners: Vec<super::owner::OwnershipEntry>,
126}
127
128use crate::network::serde_defaults::{default_one, default_true};
129
130impl Default for Load {
131 fn default() -> Self {
132 Self {
133 bus: 0,
134 active_power_demand_mw: 0.0,
135 reactive_power_demand_mvar: 0.0,
136 in_service: true,
137 conforming: true,
138 id: String::new(),
139 zip_p_impedance_frac: 0.0,
140 zip_p_current_frac: 0.0,
141 zip_p_power_frac: 1.0,
142 zip_q_impedance_frac: 0.0,
143 zip_q_current_frac: 0.0,
144 zip_q_power_frac: 1.0,
145 freq_sensitivity_p_pct_per_hz: 0.0,
146 freq_sensitivity_q_pct_per_hz: 0.0,
147 frac_motor_a: 0.0,
148 frac_motor_b: 0.0,
149 frac_motor_c: 0.0,
150 frac_motor_d: 0.0,
151 frac_electronic: 0.0,
152 frac_static: 1.0,
153 load_class: None,
154 connection: LoadConnection::WyeGrounded,
155 shedding_priority: None,
156 owners: Vec::new(),
157 }
158 }
159}
160
161impl Load {
162 /// Create a load with the given bus, active power (MW), and reactive power (MVAr).
163 pub fn new(bus: u32, active_power_demand_mw: f64, reactive_power_demand_mvar: f64) -> Self {
164 Self {
165 bus,
166 active_power_demand_mw,
167 reactive_power_demand_mvar,
168 ..Default::default()
169 }
170 }
171}