1use crate::DistLoadVoltageModel;
4
5impl DistLoadVoltageModel {
6 #[expect(
32 clippy::too_many_lines,
33 reason = "keep all branches of the bounded voltage law together for comparison with its documented equations"
34 )]
35 pub fn power_scale(&self, phase: usize, voltage: f64) -> Result<[f64; 2], String> {
36 if !voltage.is_finite() || voltage < 0.0 {
37 return Err("load voltage magnitude must be finite and nonnegative".into());
38 }
39 if matches!(self, Self::ConstantPower { .. }) {
40 return Ok([1.0; 2]);
41 }
42 let nominal = *self
43 .v_nom()
44 .get(phase)
45 .ok_or("missing nominal load voltage")?;
46 if !nominal.is_finite() || nominal <= 0.0 {
47 return Err("nominal load voltage must be positive and finite".into());
48 }
49 let voltage_pu = voltage / nominal;
50 let at = |v: &[f64]| -> Result<f64, String> {
51 v.get(phase)
52 .copied()
53 .filter(|v| v.is_finite())
54 .ok_or_else(|| "missing or nonfinite load coefficient".into())
55 };
56 let value = match self {
57 Self::ConstantPower { .. } => unreachable!(),
58 Self::ConstantCurrent { .. } => [voltage_pu; 2],
59 Self::ConstantImpedance { .. } => [voltage_pu * voltage_pu; 2],
60 Self::Exponential {
61 gamma_p, gamma_q, ..
62 } => [voltage_pu.powf(at(gamma_p)?), voltage_pu.powf(at(gamma_q)?)],
63 Self::BoundedConstantPower {
64 v_min_pu,
65 v_max_pu,
66 current_limit_pu,
67 ..
68 } => {
69 if !v_min_pu.is_finite()
70 || *v_min_pu <= 0.0
71 || !v_max_pu.is_finite()
72 || *v_max_pu < *v_min_pu
73 || current_limit_pu.is_some_and(|v| !v.is_finite() || v <= 0.0)
74 {
75 return Err("constant-power bounds and current limit must be positive, finite and ordered".into());
76 }
77 let scale = if voltage_pu <= *v_min_pu {
78 (voltage_pu / v_min_pu).powi(2)
79 } else if voltage_pu > *v_max_pu {
80 (voltage_pu / v_max_pu).powi(2)
81 } else {
82 1.0
83 };
84 [current_limit_pu.map_or(scale, |limit| scale.min(voltage_pu * limit)); 2]
85 }
86 Self::BoundedExponential {
87 gamma_p,
88 gamma_q,
89 v_low_pu,
90 v_min_pu,
91 v_max_pu,
92 ..
93 } => bounded_exponential_scale(
94 voltage_pu,
95 [at(gamma_p)?, at(gamma_q)?],
96 [*v_low_pu, *v_min_pu, *v_max_pu],
97 )?,
98 Self::Zip {
99 alpha_z,
100 alpha_i,
101 alpha_p,
102 beta_z,
103 beta_i,
104 beta_p,
105 ..
106 }
107 | Self::BoundedZip {
108 alpha_z,
109 alpha_i,
110 alpha_p,
111 beta_z,
112 beta_i,
113 beta_p,
114 ..
115 } => {
116 let active = [at(alpha_z)?, at(alpha_i)?, at(alpha_p)?];
117 let reactive = [at(beta_z)?, at(beta_i)?, at(beta_p)?];
118 let (impedance_scale, current_scale, power_scale, dropout) =
119 if let Self::BoundedZip {
120 v_low_pu: low,
121 v_min_pu: min,
122 v_max_pu: max,
123 cutoff_pu: cutoff,
124 ..
125 } = self
126 {
127 if [low, min, max, cutoff]
128 .iter()
129 .any(|v| !v.is_finite() || **v < 0.0)
130 {
131 return Err(
132 "load voltage thresholds must be finite and nonnegative".into()
133 );
134 }
135 if voltage_pu <= *low {
136 return if (voltage_pu * voltage_pu).is_finite() {
139 Ok([voltage_pu * voltage_pu; 2])
140 } else {
141 Err("load power evaluation became nonfinite".into())
142 };
143 }
144 let (current_scale, power_scale) = if voltage_pu <= *min {
145 let weight = (voltage_pu - low) / (min - low);
146 (
147 voltage_pu * (low + (1.0 - low) * weight),
148 voltage_pu * (low + (1.0 / min - low) * weight),
149 )
150 } else if voltage_pu > *max {
151 (
152 voltage_pu * voltage_pu / if *max == 0.0 { 1.0 } else { *max },
153 voltage_pu * voltage_pu / if *max == 0.0 { 1.0 } else { max * max },
154 )
155 } else {
156 (voltage_pu, 1.0)
157 };
158 let dropout = if *cutoff > 0.0 {
159 f64::midpoint(1.0, (500.0 * (voltage_pu - cutoff)).tanh())
160 } else {
161 1.0
162 };
163 (voltage_pu * voltage_pu, current_scale, power_scale, dropout)
164 } else {
165 (voltage_pu * voltage_pu, voltage_pu, 1.0, 1.0)
166 };
167 [
168 active[0] * impedance_scale
169 + active[1] * current_scale
170 + active[2] * power_scale,
171 reactive[0] * impedance_scale
172 + reactive[1] * current_scale
173 + reactive[2] * power_scale,
174 ]
175 .map(|v| v * dropout)
176 }
177 };
178 if value.iter().any(|v| !v.is_finite()) {
179 Err("load power evaluation became nonfinite".into())
180 } else {
181 Ok(value)
182 }
183 }
184
185 #[must_use]
190 #[allow(clippy::float_cmp)] pub fn without_voltage_bounds(&self) -> Self {
192 if let Self::BoundedConstantPower { v_nom, .. } = self {
193 return Self::ConstantPower {
194 v_nom: v_nom.clone(),
195 };
196 }
197 if let Self::BoundedExponential {
198 v_nom,
199 gamma_p,
200 gamma_q,
201 ..
202 } = self
203 {
204 return Self::Exponential {
205 v_nom: v_nom.clone(),
206 gamma_p: gamma_p.clone(),
207 gamma_q: gamma_q.clone(),
208 };
209 }
210 let Self::BoundedZip {
211 v_nom,
212 alpha_z,
213 alpha_i,
214 alpha_p,
215 beta_z,
216 beta_i,
217 beta_p,
218 cutoff_pu,
219 ..
220 } = self
221 else {
222 return self.clone();
223 };
224 let all = |xs: &[f64], x| !xs.is_empty() && xs.iter().all(|v| *v == x);
225 if *cutoff_pu == 0.0 && all(alpha_z, 0.0) && all(beta_z, 0.0) {
226 if all(alpha_i, 0.0) && all(beta_i, 0.0) && all(alpha_p, 1.0) && all(beta_p, 1.0) {
227 return Self::ConstantPower {
228 v_nom: v_nom.clone(),
229 };
230 }
231 if all(alpha_p, 0.0) && all(beta_p, 0.0) && all(alpha_i, 1.0) && all(beta_i, 1.0) {
232 return Self::ConstantCurrent {
233 v_nom: v_nom.clone(),
234 };
235 }
236 }
237 Self::Zip {
238 v_nom: v_nom.clone(),
239 alpha_z: alpha_z.clone(),
240 alpha_i: alpha_i.clone(),
241 alpha_p: alpha_p.clone(),
242 beta_z: beta_z.clone(),
243 beta_i: beta_i.clone(),
244 beta_p: beta_p.clone(),
245 }
246 }
247}
248
249fn bounded_exponential_scale(
250 u: f64,
251 gamma: [f64; 2],
252 bounds: [f64; 3],
253) -> Result<[f64; 2], String> {
254 if bounds.iter().any(|v| !v.is_finite() || *v < 0.0) {
255 return Err("load voltage thresholds must be finite and nonnegative".into());
256 }
257 let [low, min, max] = bounds;
258 let scale = if u <= low {
260 u * u
261 } else if u <= min {
262 u * (low + (1.0 / min - low) * (u - low) / (min - low))
263 } else if u > max {
264 u * u / if max == 0.0 { 1.0 } else { max * max }
265 } else {
266 return Ok(gamma.map(|exponent| u.powf(exponent)));
267 };
268 Ok([scale; 2])
269}