1use std::collections::{BTreeMap, HashSet};
2
3use powerio::BusId;
4use powerio::format::goc3::Goc3Document;
5use serde_json::{Map, Value};
6
7use super::error::{ScopfError, ScopfResult};
8use super::goc3::{
9 Goc3Adapter, cost_cube, float_matrix, float_vec, initial_status, json_error, require_field,
10 require_num, require_str,
11};
12use super::types::{
13 ScopfAcContingencySurvivors, ScopfAcLineRow, ScopfAcLineSurvivorRow, ScopfActiveReserveRow,
14 ScopfActiveReserveSetRow, ScopfBusRow, ScopfCostRow, ScopfDcContingencyFlowRow, ScopfDcLineRow,
15 ScopfDeviceClassLayout, ScopfDeviceRow, ScopfEnergyWindowMaxCsRow, ScopfEnergyWindowMaxPrRow,
16 ScopfEnergyWindowMinCsRow, ScopfEnergyWindowMinPrRow, ScopfEnergyWindowPeriodMaxCsRow,
17 ScopfEnergyWindowPeriodMaxPrRow, ScopfEnergyWindowPeriodMinCsRow,
18 ScopfEnergyWindowPeriodMinPrRow, ScopfEnergyWindows, ScopfFixedPhaseRow, ScopfFixedRatioRow,
19 ScopfInstance, ScopfLengths, ScopfPriceBlockRow, ScopfPriceBlocks, ScopfReactiveReserveRow,
20 ScopfReactiveReserveSetRow, ScopfShuntRow, ScopfStaticData, ScopfStaticDataProjection,
21 ScopfTransformerRow, ScopfTransformerSurvivorRow, ScopfVariablePhaseRow, ScopfVariableRatioRow,
22 ScopfViolationCost,
23};
24
25type Result<T> = ScopfResult<T>;
26
27fn sdd_device_type(obj: &Map<String, Value>) -> &str {
31 obj.get("device_type")
32 .and_then(Value::as_str)
33 .unwrap_or("producer")
34}
35
36fn require_flag(obj: &Map<String, Value>, uid: &str, key: &str) -> Result<i64> {
41 let raw = obj.get(key).ok_or_else(|| {
42 json_error(format!(
43 "simple_dispatchable_device `{uid}` missing `{key}`"
44 ))
45 })?;
46 #[allow(clippy::float_cmp)]
49 let flag = raw.as_f64().and_then(|value| {
50 if value == 0.0 {
51 Some(0)
52 } else if value == 1.0 {
53 Some(1)
54 } else {
55 None
56 }
57 });
58 flag.ok_or_else(|| {
59 json_error(format!(
60 "simple_dispatchable_device `{uid}` `{key}` is not 0 or 1"
61 ))
62 })
63}
64
65fn validate_period_len(
66 kind: &str,
67 uid: &str,
68 field: &str,
69 actual: usize,
70 expected: usize,
71) -> Result<()> {
72 if actual == expected {
73 return Ok(());
74 }
75 Err(json_error(format!(
76 "{kind} `{uid}` `{field}` has {actual} periods; expected {expected}"
77 )))
78}
79
80impl Goc3Adapter {
81 fn cost_vector(&self, device_type: &str) -> Result<Vec<ScopfCostRow>> {
82 let mut rows = Vec::new();
83 for uid in self.sdd_order() {
84 let val = self.sdd.get(&uid)?;
85 if sdd_device_type(val) != device_type {
86 continue;
87 }
88 let ts_val = self.sdd_ts.get(&uid)?;
89 let bus = self.goc3_bus_id(require_str(val, "bus")?)?;
90 let cost = ts_val.get("cost").ok_or_else(|| {
91 json_error(format!(
92 "simple_dispatchable_device time series `{uid}` missing `cost`"
93 ))
94 })?;
95 let cost = cost_cube(cost)?;
96 validate_period_len(
97 "simple_dispatchable_device time series",
98 &uid,
99 "cost",
100 cost.len(),
101 self.dt.len(),
102 )?;
103 rows.push(ScopfCostRow { bus, uid, cost });
104 }
105 Ok(rows)
106 }
107
108 fn twt_variable_phase(&self) -> Result<Vec<ScopfVariablePhaseRow>> {
109 let mut rows = Vec::new();
110 for (j_xf, uid) in self.twt.uids().iter().enumerate() {
111 let val = self.twt.get(uid)?;
112 let (lb, ub) = (require_num(val, "ta_lb")?, require_num(val, "ta_ub")?);
113 if lb < ub {
114 rows.push(ScopfVariablePhaseRow {
115 j_xf,
116 phi_min: lb,
117 phi_max: ub,
118 });
119 }
120 }
121 rows.sort_by_key(|r| r.j_xf);
122 Ok(rows)
123 }
124
125 fn twt_fixed_phase(&self) -> Result<Vec<ScopfFixedPhaseRow>> {
126 let mut rows = Vec::new();
127 for (j_xf, uid) in self.twt.uids().iter().enumerate() {
128 let val = self.twt.get(uid)?;
129 let (lb, ub) = (require_num(val, "ta_lb")?, require_num(val, "ta_ub")?);
130 if lb >= ub {
131 let phi_o = require_num(initial_status(val)?, "ta")?;
132 rows.push(ScopfFixedPhaseRow { j_xf, phi_o });
133 }
134 }
135 rows.sort_by_key(|r| r.j_xf);
136 Ok(rows)
137 }
138
139 fn twt_variable_ratio(&self) -> Result<Vec<ScopfVariableRatioRow>> {
140 let mut rows = Vec::new();
141 for (j_xf, uid) in self.twt.uids().iter().enumerate() {
142 let val = self.twt.get(uid)?;
143 let (lb, ub) = (require_num(val, "tm_lb")?, require_num(val, "tm_ub")?);
144 if lb < ub {
145 rows.push(ScopfVariableRatioRow {
146 j_xf,
147 tau_min: lb,
148 tau_max: ub,
149 });
150 }
151 }
152 rows.sort_by_key(|r| r.j_xf);
153 Ok(rows)
154 }
155
156 fn twt_fixed_ratio(&self) -> Result<Vec<ScopfFixedRatioRow>> {
157 let mut rows = Vec::new();
158 for (j_xf, uid) in self.twt.uids().iter().enumerate() {
159 let val = self.twt.get(uid)?;
160 let (lb, ub) = (require_num(val, "tm_lb")?, require_num(val, "tm_ub")?);
161 if lb >= ub {
162 let tau_o = require_num(initial_status(val)?, "tm")?;
163 rows.push(ScopfFixedRatioRow { j_xf, tau_o });
164 }
165 }
166 rows.sort_by_key(|r| r.j_xf);
167 Ok(rows)
168 }
169
170 fn sdd_row(&self, uid: &str) -> Result<ScopfDeviceRow> {
171 const SDD: &str = "simple_dispatchable_device";
172 const SDD_TS: &str = "simple_dispatchable_device time series";
173 let val = self.sdd.get(uid)?;
174 let ts_val = self.sdd_ts.get(uid)?;
175 let initial = initial_status(val)?;
176 let ts = |key| require_field(ts_val, SDD_TS, uid, key);
177 let q_bound_cap = require_flag(val, uid, "q_bound_cap")?;
181 let q_linear_cap = require_flag(val, uid, "q_linear_cap")?;
182 if q_bound_cap == 1 && q_linear_cap == 1 {
183 return Err(json_error(format!(
184 "{SDD} `{uid}` sets both `q_bound_cap` and `q_linear_cap`; the two modes are mutually exclusive"
185 )));
186 }
187 let cap = |flag: i64, key: &str| -> Result<Option<f64>> {
188 if flag == 1 {
189 Ok(Some(require_num(val, key)?))
190 } else {
191 Ok(None)
192 }
193 };
194 let row = ScopfDeviceRow {
195 bus: self.goc3_bus_id(require_str(val, "bus")?)?,
196 uid: uid.to_owned(),
197 c_on: require_num(val, "on_cost")?,
198 c_su: require_num(val, "startup_cost")?,
199 c_sd: require_num(val, "shutdown_cost")?,
200 p_ru: require_num(val, "p_ramp_up_ub")?,
201 p_rd: require_num(val, "p_ramp_down_ub")?,
202 p_ru_su: require_num(val, "p_startup_ramp_ub")?,
203 p_rd_sd: require_num(val, "p_shutdown_ramp_ub")?,
204 c_rgu: float_vec(ts("p_reg_res_up_cost")?)?,
205 c_rgd: float_vec(ts("p_reg_res_down_cost")?)?,
206 c_scr: float_vec(ts("p_syn_res_cost")?)?,
207 c_nsc: float_vec(ts("p_nsyn_res_cost")?)?,
208 c_rru_on: float_vec(ts("p_ramp_res_up_online_cost")?)?,
209 c_rru_off: float_vec(ts("p_ramp_res_up_offline_cost")?)?,
210 c_rrd_on: float_vec(ts("p_ramp_res_down_online_cost")?)?,
211 c_rrd_off: float_vec(ts("p_ramp_res_down_offline_cost")?)?,
212 c_qru: float_vec(ts("q_res_up_cost")?)?,
213 c_qrd: float_vec(ts("q_res_down_cost")?)?,
214 p_rgu_max: require_num(val, "p_reg_res_up_ub")?,
215 p_rgd_max: require_num(val, "p_reg_res_down_ub")?,
216 p_scr_max: require_num(val, "p_syn_res_ub")?,
217 p_nsc_max: require_num(val, "p_nsyn_res_ub")?,
218 p_rru_on_max: require_num(val, "p_ramp_res_up_online_ub")?,
219 p_rru_off_max: require_num(val, "p_ramp_res_up_offline_ub")?,
220 p_rrd_on_max: require_num(val, "p_ramp_res_down_online_ub")?,
221 p_rrd_off_max: require_num(val, "p_ramp_res_down_offline_ub")?,
222 p_0: require_num(initial, "p")?,
223 q_0: require_num(initial, "q")?,
224 p_max: float_vec(ts("p_ub")?)?,
225 p_min: float_vec(ts("p_lb")?)?,
226 q_max: float_vec(ts("q_ub")?)?,
227 q_min: float_vec(ts("q_lb")?)?,
228 sus: float_matrix(require_field(val, SDD, uid, "startup_states")?)?,
229 q_bound_cap,
230 q_linear_cap,
231 beta_ub: cap(q_bound_cap, "beta_ub")?,
232 beta_lb: cap(q_bound_cap, "beta_lb")?,
233 q_0_ub: cap(q_bound_cap, "q_0_ub")?,
234 q_0_lb: cap(q_bound_cap, "q_0_lb")?,
235 beta: cap(q_linear_cap, "beta")?,
236 q_p0: cap(q_linear_cap, "q_0")?,
237 };
238 for (field, actual) in [
239 ("p_reg_res_up_cost", row.c_rgu.len()),
240 ("p_reg_res_down_cost", row.c_rgd.len()),
241 ("p_syn_res_cost", row.c_scr.len()),
242 ("p_nsyn_res_cost", row.c_nsc.len()),
243 ("p_ramp_res_up_online_cost", row.c_rru_on.len()),
244 ("p_ramp_res_up_offline_cost", row.c_rru_off.len()),
245 ("p_ramp_res_down_online_cost", row.c_rrd_on.len()),
246 ("p_ramp_res_down_offline_cost", row.c_rrd_off.len()),
247 ("q_res_up_cost", row.c_qru.len()),
248 ("q_res_down_cost", row.c_qrd.len()),
249 ("p_ub", row.p_max.len()),
250 ("p_lb", row.p_min.len()),
251 ("q_ub", row.q_max.len()),
252 ("q_lb", row.q_min.len()),
253 ] {
254 validate_period_len(SDD_TS, uid, field, actual, self.dt.len())?;
255 }
256 Ok(row)
257 }
258
259 fn sdd_rows(&self, device_type: &str) -> Result<Vec<ScopfDeviceRow>> {
260 let mut rows = Vec::new();
261 for uid in self.sdd_order() {
262 if sdd_device_type(self.sdd.get(&uid)?) == device_type {
263 rows.push(self.sdd_row(&uid)?);
264 }
265 }
266 Ok(rows)
267 }
268
269 fn reserve_set<R>(
279 &self,
280 ids: &[String],
281 uids_key: &str,
282 device_type: &str,
283 devices_by_bus: &BTreeMap<String, Vec<String>>,
284 bus_order: &[String],
285 mkrow: impl Fn(BusId, usize, String) -> R,
286 ) -> Result<Vec<R>> {
287 let mut rows = Vec::new();
288 for (zone_index, id) in ids.iter().enumerate() {
289 for bus_uid in bus_order {
290 let bus_obj = self.bus.get(bus_uid)?;
291 let member = bus_obj
292 .get(uids_key)
293 .and_then(Value::as_array)
294 .is_some_and(|zones| zones.iter().any(|z| z.as_str() == Some(id.as_str())));
295 if !member {
296 continue;
297 }
298 let Some(devices) = devices_by_bus.get(bus_uid) else {
299 continue;
300 };
301 for dev_uid in devices {
302 let device = self.sdd.get(dev_uid)?;
303 if sdd_device_type(device) == device_type {
304 rows.push(mkrow(
305 self.goc3_bus_id(bus_uid)?,
306 zone_index,
307 dev_uid.clone(),
308 ));
309 }
310 }
311 }
312 }
313 Ok(rows)
314 }
315}
316
317#[allow(clippy::too_many_lines, clippy::float_cmp)]
326fn build_static_projection(tables: &Goc3Adapter) -> Result<ScopfStaticDataProjection> {
327 let l_j_xf = tables.twt.uids().len();
328 let l_j_ln = tables.ac_line.uids().len();
329 let l_j_ac = l_j_ln + l_j_xf;
330 let l_j_dc = tables.dc_line.uids().len();
331 let l_j_br = l_j_ac + l_j_dc;
332 let l_j_cs = tables.sdd_ids_consumer.len();
333 let l_j_pr = tables.sdd_ids_producer.len();
334 let l_j_cspr = l_j_cs + l_j_pr;
335 let l_j_sh = tables.shunt.uids().len();
336 let i = tables.bus.uids().len();
337 let l_t = tables.dt.len();
338 let l_n_p = tables.azr.uids().len();
339 let l_n_q = tables.rzr.uids().len();
340 let k = tables.contingencies()?.len();
344
345 let lengths = ScopfLengths {
346 l_j_xf,
347 l_j_ln,
348 l_j_ac,
349 l_j_dc,
350 l_j_br,
351 l_j_cs,
352 l_j_pr,
353 l_j_cspr,
354 l_j_sh,
355 i,
356 l_t,
357 l_n_p,
358 l_n_q,
359 k,
360 };
361
362 let mut bus: Vec<ScopfBusRow> = tables
363 .bus
364 .uids()
365 .iter()
366 .map(|uid| {
367 let val = tables.bus.get(uid)?;
368 Ok(ScopfBusRow {
369 i: tables.goc3_bus_id(uid)?,
370 uid: uid.clone(),
371 v_min: require_num(val, "vm_lb")?,
372 v_max: require_num(val, "vm_ub")?,
373 })
374 })
375 .collect::<Result<_>>()?;
376 bus.sort_by_key(|r| r.i);
377
378 let shunt: Vec<ScopfShuntRow> = tables
379 .shunt
380 .uids()
381 .iter()
382 .enumerate()
383 .map(|(j_sh, uid)| {
384 let val = tables.shunt.get(uid)?;
385 Ok(ScopfShuntRow {
386 j_sh,
387 uid: uid.clone(),
388 bus: tables.goc3_bus_id(require_str(val, "bus")?)?,
389 g_sh: require_num(val, "gs")?,
390 b_sh: require_num(val, "bs")?,
391 })
392 })
393 .collect::<Result<_>>()?;
394 let mut acl_branch: Vec<ScopfAcLineRow> = tables
395 .ac_line
396 .uids()
397 .iter()
398 .enumerate()
399 .map(|(j_ln, uid)| {
400 let val = tables.ac_line.get(uid)?;
401 let (g_sr, b_sr, b_ch, g_fr, g_to, b_fr, b_to) = branch_admittance(uid, val)?;
402 Ok(ScopfAcLineRow {
403 j_ln,
404 uid: uid.clone(),
405 to_bus: tables.goc3_bus_id(require_str(val, "to_bus")?)?,
406 fr_bus: tables.goc3_bus_id(require_str(val, "fr_bus")?)?,
407 c_su: require_num(val, "connection_cost")?,
408 c_sd: require_num(val, "disconnection_cost")?,
409 s_max: require_num(val, "mva_ub_nom")?,
410 g_sr,
411 b_sr,
412 b_ch,
413 g_fr,
414 g_to,
415 b_fr,
416 b_to,
417 })
418 })
419 .collect::<Result<_>>()?;
420 acl_branch.sort_by_key(|r| r.j_ln);
421
422 let mut acx_branch: Vec<ScopfTransformerRow> = tables
423 .twt
424 .uids()
425 .iter()
426 .enumerate()
427 .map(|(j_xf, uid)| {
428 let val = tables.twt.get(uid)?;
429 let (g_sr, b_sr, b_ch, g_fr, g_to, b_fr, b_to) = branch_admittance(uid, val)?;
430 Ok(ScopfTransformerRow {
431 j_xf,
432 uid: uid.clone(),
433 to_bus: tables.goc3_bus_id(require_str(val, "to_bus")?)?,
434 fr_bus: tables.goc3_bus_id(require_str(val, "fr_bus")?)?,
435 c_su: require_num(val, "connection_cost")?,
436 c_sd: require_num(val, "disconnection_cost")?,
437 s_max: require_num(val, "mva_ub_nom")?,
438 g_sr,
439 b_sr,
440 b_ch,
441 g_fr,
442 g_to,
443 b_fr,
444 b_to,
445 })
446 })
447 .collect::<Result<_>>()?;
448 acx_branch.sort_by_key(|r| r.j_xf);
449
450 let mut dc_branch: Vec<ScopfDcLineRow> = tables
451 .dc_line
452 .uids()
453 .iter()
454 .enumerate()
455 .map(|(j_dc, uid)| {
456 let val = tables.dc_line.get(uid)?;
457 Ok(ScopfDcLineRow {
458 j_dc,
459 uid: uid.clone(),
460 pdc_max: require_num(val, "pdc_ub")?,
461 qdc_fr_min: require_num(val, "qdc_fr_lb")?,
462 qdc_to_min: require_num(val, "qdc_to_lb")?,
463 qdc_fr_max: require_num(val, "qdc_fr_ub")?,
464 qdc_to_max: require_num(val, "qdc_to_ub")?,
465 to_bus: tables.goc3_bus_id(require_str(val, "to_bus")?)?,
466 fr_bus: tables.goc3_bus_id(require_str(val, "fr_bus")?)?,
467 })
468 })
469 .collect::<Result<_>>()?;
470 dc_branch.sort_by_key(|r| r.j_dc);
471
472 let cost_vector_pr = tables.cost_vector("producer")?;
473 let cost_vector_cs = tables.cost_vector("consumer")?;
474 let prod = tables.sdd_rows("producer")?;
475 let cons = tables.sdd_rows("consumer")?;
476
477 let mut active_reserve: Vec<ScopfActiveReserveRow> = tables
478 .azr
479 .uids()
480 .iter()
481 .enumerate()
482 .map(|(n_p, uid)| {
483 let val = tables.azr.get(uid)?;
484 let ts_val = tables.azr_ts.get(uid)?;
485 let row = ScopfActiveReserveRow {
486 n_p,
487 uid: uid.clone(),
488 c_rgu: require_num(val, "REG_UP_vio_cost")?,
489 c_rgd: require_num(val, "REG_DOWN_vio_cost")?,
490 c_scr: require_num(val, "SYN_vio_cost")?,
491 c_nsc: require_num(val, "NSYN_vio_cost")?,
492 c_rru: require_num(val, "RAMPING_RESERVE_UP_vio_cost")?,
493 c_rrd: require_num(val, "RAMPING_RESERVE_DOWN_vio_cost")?,
494 sigma_rgu: require_num(val, "REG_UP")?,
495 sigma_rgd: require_num(val, "REG_DOWN")?,
496 sigma_scr: require_num(val, "SYN")?,
497 sigma_nsc: require_num(val, "NSYN")?,
498 p_rru_min: float_vec(ts_val.get("RAMPING_RESERVE_UP").ok_or_else(|| {
499 json_error(format!(
500 "active_zonal_reserve time series `{uid}` missing `RAMPING_RESERVE_UP`"
501 ))
502 })?)?,
503 p_rrd_min: float_vec(ts_val.get("RAMPING_RESERVE_DOWN").ok_or_else(|| {
504 json_error(format!(
505 "active_zonal_reserve time series `{uid}` missing `RAMPING_RESERVE_DOWN`"
506 ))
507 })?)?,
508 };
509 validate_period_len(
510 "active_zonal_reserve time series",
511 uid,
512 "RAMPING_RESERVE_UP",
513 row.p_rru_min.len(),
514 tables.dt.len(),
515 )?;
516 validate_period_len(
517 "active_zonal_reserve time series",
518 uid,
519 "RAMPING_RESERVE_DOWN",
520 row.p_rrd_min.len(),
521 tables.dt.len(),
522 )?;
523 Ok(row)
524 })
525 .collect::<Result<_>>()?;
526 active_reserve.sort_by_key(|r| r.n_p);
527
528 let mut reactive_reserve: Vec<ScopfReactiveReserveRow> = tables
529 .rzr
530 .uids()
531 .iter()
532 .enumerate()
533 .map(|(n_q, uid)| {
534 let val = tables.rzr.get(uid)?;
535 let ts_val = tables.rzr_ts.get(uid)?;
536 let row = ScopfReactiveReserveRow {
537 n_q,
538 uid: uid.clone(),
539 c_qru: require_num(val, "REACT_UP_vio_cost")?,
540 c_qrd: require_num(val, "REACT_DOWN_vio_cost")?,
541 q_qru_min: float_vec(ts_val.get("REACT_UP").ok_or_else(|| {
542 json_error(format!(
543 "reactive_zonal_reserve time series `{uid}` missing `REACT_UP`"
544 ))
545 })?)?,
546 q_qrd_min: float_vec(ts_val.get("REACT_DOWN").ok_or_else(|| {
547 json_error(format!(
548 "reactive_zonal_reserve time series `{uid}` missing `REACT_DOWN`"
549 ))
550 })?)?,
551 };
552 validate_period_len(
553 "reactive_zonal_reserve time series",
554 uid,
555 "REACT_UP",
556 row.q_qru_min.len(),
557 tables.dt.len(),
558 )?;
559 validate_period_len(
560 "reactive_zonal_reserve time series",
561 uid,
562 "REACT_DOWN",
563 row.q_qrd_min.len(),
564 tables.dt.len(),
565 )?;
566 Ok(row)
567 })
568 .collect::<Result<_>>()?;
569 reactive_reserve.sort_by_key(|r| r.n_q);
570
571 let devices_by_bus = tables.devices_by_bus()?;
572 let bus_order = tables.bus_order();
573 let active_reserve_set_pr = tables.reserve_set(
574 tables.azr.uids(),
575 "active_reserve_uids",
576 "producer",
577 &devices_by_bus,
578 &bus_order,
579 |i, n_p, uid| ScopfActiveReserveSetRow { i, n_p, uid },
580 )?;
581 let active_reserve_set_cs = tables.reserve_set(
582 tables.azr.uids(),
583 "active_reserve_uids",
584 "consumer",
585 &devices_by_bus,
586 &bus_order,
587 |i, n_p, uid| ScopfActiveReserveSetRow { i, n_p, uid },
588 )?;
589 let reactive_reserve_set_pr = tables.reserve_set(
590 tables.rzr.uids(),
591 "reactive_reserve_uids",
592 "producer",
593 &devices_by_bus,
594 &bus_order,
595 |i, n_q, uid| ScopfReactiveReserveSetRow { i, n_q, uid },
596 )?;
597 let reactive_reserve_set_cs = tables.reserve_set(
598 tables.rzr.uids(),
599 "reactive_reserve_uids",
600 "consumer",
601 &devices_by_bus,
602 &bus_order,
603 |i, n_q, uid| ScopfReactiveReserveSetRow { i, n_q, uid },
604 )?;
605
606 let static_data = ScopfStaticData {
607 bus,
608 shunt,
609 acl_branch,
610 acx_branch,
611 vpd: tables.twt_variable_phase()?,
612 fpd: tables.twt_fixed_phase()?,
613 vwr: tables.twt_variable_ratio()?,
614 fwr: tables.twt_fixed_ratio()?,
615 dc_branch,
616 prod,
617 cons,
618 active_reserve,
619 reactive_reserve,
620 active_reserve_set_pr,
621 active_reserve_set_cs,
622 reactive_reserve_set_pr,
623 reactive_reserve_set_cs,
624 };
625
626 Ok(ScopfStaticDataProjection {
627 static_data,
628 lengths,
629 cost_vector_pr,
630 cost_vector_cs,
631 })
632}
633
634fn interval_midpoints(dt: &[f64]) -> Vec<f64> {
635 let mut a_end = 0.0;
636 dt.iter()
637 .map(|d| {
638 let start = a_end;
639 a_end += d;
640 f64::midpoint(start, a_end)
641 })
642 .collect()
643}
644
645type EnergyWindowTuple = (usize, String, f64, f64, f64);
648type EnergyWindowPeriodTuple = (usize, String, usize, f64);
651
652fn sdd_windows(
662 tables: &Goc3Adapter,
663 a_mid: &[f64],
664 device_type: &str,
665 req_key: &str,
666 eps: f64,
667) -> Result<(Vec<EnergyWindowTuple>, Vec<EnergyWindowPeriodTuple>)> {
668 let mut windows = Vec::new();
669 let mut window_periods = Vec::new();
670 let mut ind = 0usize;
671 for uid in tables.sdd_order() {
672 let val = tables.sdd.get(&uid)?;
673 if sdd_device_type(val) != device_type {
674 continue;
675 }
676 let req = require_field(val, "simple_dispatchable_device", &uid, req_key)?
677 .as_array()
678 .ok_or_else(|| {
679 json_error(format!(
680 "simple_dispatchable_device `{uid}` `{req_key}` is not an array"
681 ))
682 })?;
683 for w in req {
684 let w = float_vec(w)?;
685 let [start, end, bound] = w[..] else {
686 return Err(json_error(format!(
687 "simple_dispatchable_device `{uid}` `{req_key}` window is not a 3-element array"
688 )));
689 };
690 windows.push((ind, uid.clone(), start, end, bound));
691 for (t0, &m) in a_mid.iter().enumerate() {
692 if start + eps < m && m <= end + eps {
693 window_periods.push((ind, uid.clone(), t0, tables.dt[t0]));
694 }
695 }
696 ind += 1;
697 }
698 }
699 Ok((windows, window_periods))
700}
701
702#[allow(clippy::too_many_lines)]
710fn build_energy_windows(tables: &Goc3Adapter) -> Result<ScopfEnergyWindows> {
711 const EPS_TIME: f64 = 1e-6;
712 let a_mid = interval_midpoints(&tables.dt);
713
714 let (max_pr, t_max_pr) = sdd_windows(tables, &a_mid, "producer", "energy_req_ub", EPS_TIME)?;
715 let (max_cs, t_max_cs) = sdd_windows(tables, &a_mid, "consumer", "energy_req_ub", EPS_TIME)?;
716 let (min_pr, t_min_pr) = sdd_windows(tables, &a_mid, "producer", "energy_req_lb", EPS_TIME)?;
717 let (min_cs, t_min_cs) = sdd_windows(tables, &a_mid, "consumer", "energy_req_lb", EPS_TIME)?;
718
719 let w_en_max_pr = max_pr
720 .into_iter()
721 .map(
722 |(w_en_max_pr_ind, uid, a_en_max_start, a_en_max_end, e_max)| {
723 ScopfEnergyWindowMaxPrRow {
724 w_en_max_pr_ind,
725 uid,
726 a_en_max_start,
727 a_en_max_end,
728 e_max,
729 }
730 },
731 )
732 .collect();
733 let w_en_max_cs = max_cs
734 .into_iter()
735 .map(
736 |(w_en_max_cs_ind, uid, a_en_max_start, a_en_max_end, e_max)| {
737 ScopfEnergyWindowMaxCsRow {
738 w_en_max_cs_ind,
739 uid,
740 a_en_max_start,
741 a_en_max_end,
742 e_max,
743 }
744 },
745 )
746 .collect();
747 let w_en_min_pr = min_pr
748 .into_iter()
749 .map(
750 |(w_en_min_pr_ind, uid, a_en_min_start, a_en_min_end, e_min)| {
751 ScopfEnergyWindowMinPrRow {
752 w_en_min_pr_ind,
753 uid,
754 a_en_min_start,
755 a_en_min_end,
756 e_min,
757 }
758 },
759 )
760 .collect();
761 let w_en_min_cs = min_cs
762 .into_iter()
763 .map(
764 |(w_en_min_cs_ind, uid, a_en_min_start, a_en_min_end, e_min)| {
765 ScopfEnergyWindowMinCsRow {
766 w_en_min_cs_ind,
767 uid,
768 a_en_min_start,
769 a_en_min_end,
770 e_min,
771 }
772 },
773 )
774 .collect();
775
776 let t_w_en_max_pr = t_max_pr
777 .into_iter()
778 .map(
779 |(w_en_max_pr_ind, uid, t, dt)| ScopfEnergyWindowPeriodMaxPrRow {
780 w_en_max_pr_ind,
781 uid,
782 t,
783 dt,
784 },
785 )
786 .collect();
787 let t_w_en_max_cs = t_max_cs
788 .into_iter()
789 .map(
790 |(w_en_max_cs_ind, uid, t, dt)| ScopfEnergyWindowPeriodMaxCsRow {
791 w_en_max_cs_ind,
792 uid,
793 t,
794 dt,
795 },
796 )
797 .collect();
798 let t_w_en_min_pr = t_min_pr
799 .into_iter()
800 .map(
801 |(w_en_min_pr_ind, uid, t, dt)| ScopfEnergyWindowPeriodMinPrRow {
802 w_en_min_pr_ind,
803 uid,
804 t,
805 dt,
806 },
807 )
808 .collect();
809 let t_w_en_min_cs = t_min_cs
810 .into_iter()
811 .map(
812 |(w_en_min_cs_ind, uid, t, dt)| ScopfEnergyWindowPeriodMinCsRow {
813 w_en_min_cs_ind,
814 uid,
815 t,
816 dt,
817 },
818 )
819 .collect();
820
821 Ok(ScopfEnergyWindows {
822 w_en_max_pr,
823 w_en_max_cs,
824 w_en_min_pr,
825 w_en_min_cs,
826 t_w_en_max_pr,
827 t_w_en_max_cs,
828 t_w_en_min_pr,
829 t_w_en_min_cs,
830 })
831}
832
833fn flatten_price_blocks(cost_vector: &[ScopfCostRow]) -> Vec<ScopfPriceBlockRow> {
834 let mut rows = Vec::new();
835 let mut flat_k = 0usize;
836 for pc in cost_vector {
837 for (t0, cost_t) in pc.cost.iter().enumerate() {
838 for (m0, cost_tm) in cost_t.iter().enumerate() {
839 let (c_en, p_max) = (cost_tm[0], cost_tm[1]);
840 rows.push(ScopfPriceBlockRow {
841 flat_k,
842 uid: pc.uid.clone(),
843 t: t0,
844 m: m0,
845 c_en,
846 p_max,
847 });
848 flat_k += 1;
849 }
850 }
851 }
852 rows
853}
854
855fn build_price_blocks(
861 cost_vector_pr: &[ScopfCostRow],
862 cost_vector_cs: &[ScopfCostRow],
863) -> ScopfPriceBlocks {
864 ScopfPriceBlocks {
865 producer: flatten_price_blocks(cost_vector_pr),
866 consumer: flatten_price_blocks(cost_vector_cs),
867 }
868}
869
870fn series_terms(r: f64, x: f64, uid: &str) -> Result<(f64, f64)> {
875 let denom = x * x + r * r;
876 if denom == 0.0 {
877 return Err(json_error(format!(
878 "branch `{uid}` has zero series impedance (r = x = 0)"
879 )));
880 }
881 if !denom.is_finite() {
882 return Err(json_error(format!(
883 "branch `{uid}` has non-finite series impedance"
884 )));
885 }
886 Ok((r / denom, -x / denom))
887}
888
889#[allow(clippy::type_complexity, clippy::float_cmp)]
897fn branch_admittance(
898 uid: &str,
899 val: &Map<String, Value>,
900) -> Result<(f64, f64, f64, f64, f64, f64, f64)> {
901 let (r, x) = (require_num(val, "r")?, require_num(val, "x")?);
902 let (g_sr, b_sr) = series_terms(r, x, uid)?;
903 let additional_shunt = require_num(val, "additional_shunt")? == 1.0;
904 let (g_fr, g_to, b_fr, b_to) = if additional_shunt {
905 (
906 require_num(val, "g_fr")?,
907 require_num(val, "g_to")?,
908 require_num(val, "b_fr")?,
909 require_num(val, "b_to")?,
910 )
911 } else {
912 (0.0, 0.0, 0.0, 0.0)
913 };
914 Ok((g_sr, b_sr, require_num(val, "b")?, g_fr, g_to, b_fr, b_to))
915}
916
917fn contingency_outages(ctg_idx: usize, ctg: &Value) -> Result<(usize, HashSet<&str>)> {
919 let ctg_obj = ctg
920 .as_object()
921 .ok_or_else(|| json_error("reliability.contingency item is not an object"))?;
922 let ctg_uid = require_str(ctg_obj, "uid")?;
923 let outaged = require_field(ctg_obj, "contingency", ctg_uid, "components")?
924 .as_array()
925 .ok_or_else(|| {
926 json_error(format!(
927 "contingency `{ctg_uid}` `components` is not an array"
928 ))
929 })?
930 .iter()
931 .map(|v| {
932 v.as_str()
933 .ok_or_else(|| json_error("component uid is not a string"))
934 })
935 .collect::<Result<_>>()?;
936 Ok((ctg_idx, outaged))
937}
938
939fn build_ac_contingency_survivors(tables: &Goc3Adapter) -> Result<ScopfAcContingencySurvivors> {
947 let contingencies = tables.contingencies()?;
948
949 let mut ln = Vec::with_capacity(contingencies.len());
950 let mut xf = Vec::with_capacity(contingencies.len());
951 for (ctg_idx, ctg) in contingencies.iter().enumerate() {
952 let (ctg_idx, outaged) = contingency_outages(ctg_idx, ctg)?;
953
954 let mut ln_rows = Vec::new();
955 for (j_ln, uid) in tables.ac_line.uids().iter().enumerate() {
956 if outaged.contains(uid.as_str()) {
957 continue;
958 }
959 let val = tables.ac_line.get(uid)?;
960 let (r, x) = (require_num(val, "r")?, require_num(val, "x")?);
961 let (_, b_sr) = series_terms(r, x, uid)?;
962 ln_rows.push(ScopfAcLineSurvivorRow {
963 ctg: ctg_idx,
964 j_ln,
965 uid: uid.clone(),
966 to_bus: tables.goc3_bus_id(require_str(val, "to_bus")?)?,
967 fr_bus: tables.goc3_bus_id(require_str(val, "fr_bus")?)?,
968 b_sr,
969 s_max_ctg: require_num(val, "mva_ub_em")?,
970 });
971 }
972 ln.push(ln_rows);
973
974 let mut xf_rows = Vec::new();
975 for (j_xf, uid) in tables.twt.uids().iter().enumerate() {
976 if outaged.contains(uid.as_str()) {
977 continue;
978 }
979 let val = tables.twt.get(uid)?;
980 let (r, x) = (require_num(val, "r")?, require_num(val, "x")?);
981 let (_, b_sr) = series_terms(r, x, uid)?;
982 xf_rows.push(ScopfTransformerSurvivorRow {
983 ctg: ctg_idx,
984 j_xf,
985 uid: uid.clone(),
986 to_bus: tables.goc3_bus_id(require_str(val, "to_bus")?)?,
987 fr_bus: tables.goc3_bus_id(require_str(val, "fr_bus")?)?,
988 b_sr,
989 s_max_ctg: require_num(val, "mva_ub_em")?,
990 });
991 }
992 xf.push(xf_rows);
993 }
994
995 Ok(ScopfAcContingencySurvivors { ln, xf })
996}
997
998fn build_dc_contingency_flows(tables: &Goc3Adapter) -> Result<Vec<ScopfDcContingencyFlowRow>> {
999 let contingencies = tables.contingencies()?;
1000 let mut rows = Vec::new();
1001 let mut flat_jtk_dc = 0usize;
1002 for (ctg_idx, ctg) in contingencies.iter().enumerate() {
1003 let (ctg_idx, outaged) = contingency_outages(ctg_idx, ctg)?;
1004
1005 for (t0, &dt) in tables.dt.iter().enumerate() {
1006 for (j_dc, uid) in tables.dc_line.uids().iter().enumerate() {
1007 if outaged.contains(uid.as_str()) {
1008 continue;
1009 }
1010 let val = tables.dc_line.get(uid)?;
1011 rows.push(ScopfDcContingencyFlowRow {
1012 flat_jtk_dc,
1013 ctg: ctg_idx,
1014 j_dc,
1015 to_bus: tables.goc3_bus_id(require_str(val, "to_bus")?)?,
1016 fr_bus: tables.goc3_bus_id(require_str(val, "fr_bus")?)?,
1017 t: t0,
1018 dt,
1019 });
1020 flat_jtk_dc += 1;
1021 }
1022 }
1023 }
1024 Ok(rows)
1025}
1026
1027fn build_violation_cost(tables: &Goc3Adapter) -> ScopfViolationCost {
1031 let price = |key: &str| tables.violation_cost.get(key).and_then(Value::as_f64);
1032 ScopfViolationCost {
1033 p_bus: price("p_bus_vio_cost"),
1034 q_bus: price("q_bus_vio_cost"),
1035 s: price("s_vio_cost"),
1036 e: price("e_vio_cost"),
1037 }
1038}
1039
1040fn device_class_blocks(tables: &Goc3Adapter) -> Result<ScopfDeviceClassLayout> {
1044 let mut runs: Vec<&str> = Vec::new();
1045 for uid in tables.sdd.uids() {
1046 let kind = sdd_device_type(tables.sdd.get(uid)?);
1047 let kind = if kind == "consumer" {
1048 "consumer"
1049 } else {
1050 "producer"
1051 };
1052 if runs.last() != Some(&kind) {
1053 runs.push(kind);
1054 }
1055 }
1056 if runs.len() > 2 {
1057 return Ok(ScopfDeviceClassLayout::Interleaved);
1058 }
1059 Ok(ScopfDeviceClassLayout::Contiguous {
1060 producers_first: runs.first() != Some(&"consumer"),
1061 })
1062}
1063
1064fn project_scopf_instance(tables: &Goc3Adapter) -> Result<ScopfInstance> {
1065 let ScopfStaticDataProjection {
1066 static_data,
1067 lengths,
1068 cost_vector_pr,
1069 cost_vector_cs,
1070 } = build_static_projection(tables)?;
1071 let device_class_layout = device_class_blocks(tables)?;
1072 Ok(ScopfInstance {
1073 static_data,
1074 lengths,
1075 energy_windows: build_energy_windows(tables)?,
1076 price_blocks: build_price_blocks(&cost_vector_pr, &cost_vector_cs),
1077 ac_contingency_survivors: build_ac_contingency_survivors(tables)?,
1078 dc_contingency_flows: build_dc_contingency_flows(tables)?,
1079 violation_cost: build_violation_cost(tables),
1080 device_class_layout,
1081 })
1082}
1083
1084fn build_scopf_instance(document: &Goc3Document) -> Result<ScopfInstance> {
1085 let tables = Goc3Adapter::from_document(document)?;
1086 project_scopf_instance(&tables)
1087}
1088
1089pub fn parse_scopf_str(text: &str, from: &str) -> Result<ScopfInstance> {
1091 if from != "goc3-json" {
1092 return Err(ScopfError::UnsupportedFormat(from.to_owned()));
1093 }
1094 let document = Goc3Document::parse(text)?;
1095 build_scopf_instance(&document)
1096}