Skip to main content

powerio_prob/scopf/
json.rs

1//! The Julia compatibility document.
2//!
3//! The conversion is structural: every struct that reaches the document classifies
4//! each of its fields as a 0-based internal index (renumbered to 1-based), a
5//! renamed field (Julia spells some names in Greek or uppercase), or a value
6//! passed through unchanged. The classification destructures the struct
7//! exhaustively, so a field added in `types.rs` fails to compile until it is
8//! classified here: a new index field cannot be silently missed, and a value
9//! field reusing an index name (`t`, `m`, `j_ln`, ...) in another struct is
10//! never bumped.
11
12use serde::Serialize;
13use serde_json::{Map, Value};
14
15use super::error::ScopfError;
16use super::types::{
17    ScopfAcContingencySurvivors, ScopfAcLineRow, ScopfAcLineSurvivorRow, ScopfActiveReserveRow,
18    ScopfActiveReserveSetRow, ScopfBusRow, ScopfDcContingencyFlowRow, ScopfDcLineRow,
19    ScopfDeviceRow, ScopfEnergyWindowMaxCsRow, ScopfEnergyWindowMaxPrRow,
20    ScopfEnergyWindowMinCsRow, ScopfEnergyWindowMinPrRow, ScopfEnergyWindowPeriodMaxCsRow,
21    ScopfEnergyWindowPeriodMaxPrRow, ScopfEnergyWindowPeriodMinCsRow,
22    ScopfEnergyWindowPeriodMinPrRow, ScopfEnergyWindows, ScopfFixedPhaseRow, ScopfFixedRatioRow,
23    ScopfLengths, ScopfPriceBlockRow, ScopfPriceBlocks, ScopfReactiveReserveRow,
24    ScopfReactiveReserveSetRow, ScopfShuntRow, ScopfStaticData, ScopfTransformerRow,
25    ScopfTransformerSurvivorRow, ScopfVariablePhaseRow, ScopfVariableRatioRow, ScopfViolationCost,
26};
27use super::{ScopfInstance, ScopfResult};
28
29pub const SCOPF_SCHEMA: &str = "powerio.scopf.julia";
30
31#[derive(Serialize)]
32struct Envelope {
33    schema: &'static str,
34    /// The powerio release that wrote this document; see [`powerio::version`].
35    #[serde(rename = "powerio_version")]
36    powerio_version: &'static str,
37    index_base: usize,
38    instance: Value,
39}
40
41/// One serialized object: the fields holding 0-based internal indices
42/// and the fields renamed in the document.
43trait SerializedFields: Serialize {
44    /// Serialized names of the fields holding 0-based internal indices.
45    /// External identity (`BusId`, `uid`) is never listed.
46    const INDEX_FIELDS: &'static [&'static str] = &[];
47    /// `(internal, document)` name pairs.
48    const RENAMED_FIELDS: &'static [(&'static str, &'static str)] = &[];
49}
50
51/// Classify every field of one struct that reaches the document. The generated function
52/// destructures the struct exhaustively, so this fails to compile whenever a
53/// field is added, removed, or renamed in `types.rs` without reclassifying it.
54macro_rules! serialized_fields {
55    ($row:ident {
56        index: [$($index:ident),* $(,)?],
57        values: [$($value:ident),* $(,)?]
58        $(, renamed: [$($from:ident => $to:literal),+ $(,)?])? $(,)?
59    }) => {
60        impl SerializedFields for $row {
61            const INDEX_FIELDS: &'static [&'static str] = &[$(stringify!($index)),*];
62            $(const RENAMED_FIELDS: &'static [(&'static str, &'static str)] =
63                &[$((stringify!($from), $to)),+];)?
64        }
65        const _: () = {
66            #[allow(dead_code)]
67            fn classified(row: $row) {
68                let $row { $($index: _,)* $($value: _,)* $($($from: _,)+)? } = row;
69            }
70        };
71    };
72}
73
74serialized_fields!(ScopfBusRow {
75    index: [],
76    values: [i, uid, v_min, v_max],
77});
78serialized_fields!(ScopfShuntRow {
79    index: [j_sh],
80    values: [uid, bus, g_sh, b_sh],
81});
82serialized_fields!(ScopfAcLineRow {
83    index: [j_ln],
84    values: [
85        uid, to_bus, fr_bus, c_su, c_sd, s_max, g_sr, b_sr, b_ch, g_fr, g_to, b_fr, b_to
86    ],
87});
88serialized_fields!(ScopfTransformerRow {
89    index: [j_xf],
90    values: [
91        uid, to_bus, fr_bus, c_su, c_sd, s_max, g_sr, b_sr, b_ch, g_fr, g_to, b_fr, b_to
92    ],
93});
94serialized_fields!(ScopfDcLineRow {
95    index: [j_dc],
96    values: [
97        uid, pdc_max, qdc_fr_min, qdc_to_min, qdc_fr_max, qdc_to_max, to_bus, fr_bus
98    ],
99});
100serialized_fields!(ScopfVariablePhaseRow {
101    index: [j_xf],
102    values: [phi_min, phi_max],
103});
104serialized_fields!(ScopfFixedPhaseRow {
105    index: [j_xf],
106    values: [phi_o],
107});
108serialized_fields!(ScopfVariableRatioRow {
109    index: [j_xf],
110    values: [tau_min, tau_max],
111});
112serialized_fields!(ScopfFixedRatioRow {
113    index: [j_xf],
114    values: [tau_o],
115});
116serialized_fields!(ScopfDeviceRow {
117    index: [],
118    values: [
119        bus,
120        uid,
121        c_on,
122        c_su,
123        c_sd,
124        p_ru,
125        p_rd,
126        p_ru_su,
127        p_rd_sd,
128        c_rgu,
129        c_rgd,
130        c_scr,
131        c_nsc,
132        c_rru_on,
133        c_rru_off,
134        c_rrd_on,
135        c_rrd_off,
136        c_qru,
137        c_qrd,
138        p_rgu_max,
139        p_rgd_max,
140        p_scr_max,
141        p_nsc_max,
142        p_rru_on_max,
143        p_rru_off_max,
144        p_rrd_on_max,
145        p_rrd_off_max,
146        p_0,
147        q_0,
148        p_max,
149        p_min,
150        q_max,
151        q_min,
152        sus,
153        q_bound_cap,
154        q_linear_cap,
155        beta_ub,
156        beta_lb,
157        q_0_ub,
158        q_0_lb,
159        beta,
160        q_p0
161    ],
162});
163serialized_fields!(ScopfActiveReserveRow {
164    index: [n_p],
165    values: [uid, c_rgu, c_rgd, c_scr, c_nsc, c_rru, c_rrd, p_rru_min, p_rrd_min],
166    renamed: [
167        sigma_rgu => "σ_rgu",
168        sigma_rgd => "σ_rgd",
169        sigma_scr => "σ_scr",
170        sigma_nsc => "σ_nsc",
171    ],
172});
173serialized_fields!(ScopfReactiveReserveRow {
174    index: [n_q],
175    values: [uid, c_qru, c_qrd, q_qru_min, q_qrd_min],
176});
177serialized_fields!(ScopfActiveReserveSetRow {
178    index: [n_p],
179    values: [i, uid],
180});
181serialized_fields!(ScopfReactiveReserveSetRow {
182    index: [n_q],
183    values: [i, uid],
184});
185serialized_fields!(ScopfLengths {
186    index: [],
187    values: [],
188    renamed: [
189        l_j_xf => "L_J_xf",
190        l_j_ln => "L_J_ln",
191        l_j_ac => "L_J_ac",
192        l_j_dc => "L_J_dc",
193        l_j_br => "L_J_br",
194        l_j_cs => "L_J_cs",
195        l_j_pr => "L_J_pr",
196        l_j_cspr => "L_J_cspr",
197        l_j_sh => "L_J_sh",
198        i => "I",
199        l_t => "L_T",
200        l_n_p => "L_N_p",
201        l_n_q => "L_N_q",
202        k => "K",
203    ],
204});
205serialized_fields!(ScopfViolationCost {
206    index: [],
207    values: [p_bus, q_bus, s, e],
208});
209serialized_fields!(ScopfEnergyWindowMaxPrRow {
210    index: [w_en_max_pr_ind],
211    values: [uid, a_en_max_start, a_en_max_end, e_max],
212});
213serialized_fields!(ScopfEnergyWindowMaxCsRow {
214    index: [w_en_max_cs_ind],
215    values: [uid, a_en_max_start, a_en_max_end, e_max],
216});
217serialized_fields!(ScopfEnergyWindowMinPrRow {
218    index: [w_en_min_pr_ind],
219    values: [uid, a_en_min_start, a_en_min_end, e_min],
220});
221serialized_fields!(ScopfEnergyWindowMinCsRow {
222    index: [w_en_min_cs_ind],
223    values: [uid, a_en_min_start, a_en_min_end, e_min],
224});
225serialized_fields!(ScopfEnergyWindowPeriodMaxPrRow {
226    index: [w_en_max_pr_ind, t],
227    values: [uid, dt],
228});
229serialized_fields!(ScopfEnergyWindowPeriodMaxCsRow {
230    index: [w_en_max_cs_ind, t],
231    values: [uid, dt],
232});
233serialized_fields!(ScopfEnergyWindowPeriodMinPrRow {
234    index: [w_en_min_pr_ind, t],
235    values: [uid, dt],
236});
237serialized_fields!(ScopfEnergyWindowPeriodMinCsRow {
238    index: [w_en_min_cs_ind, t],
239    values: [uid, dt],
240});
241serialized_fields!(ScopfPriceBlockRow {
242    index: [flat_k, t, m],
243    values: [uid, c_en, p_max],
244});
245serialized_fields!(ScopfAcLineSurvivorRow {
246    index: [ctg, j_ln],
247    values: [uid, to_bus, fr_bus, b_sr, s_max_ctg],
248});
249serialized_fields!(ScopfTransformerSurvivorRow {
250    index: [ctg, j_xf],
251    values: [uid, to_bus, fr_bus, b_sr, s_max_ctg],
252});
253serialized_fields!(ScopfDcContingencyFlowRow {
254    index: [flat_jtk_dc, ctg, j_dc, t],
255    values: [to_bus, fr_bus, dt],
256});
257
258/// Convert an internal instance to the 1-based Julia document.
259pub fn to_json_value(instance: &ScopfInstance) -> ScopfResult<Value> {
260    let ScopfInstance {
261        static_data,
262        lengths,
263        energy_windows,
264        price_blocks,
265        ac_contingency_survivors,
266        dc_contingency_flows,
267        violation_cost,
268        device_class_layout,
269    } = instance;
270    let mut fields = Map::new();
271    fields.insert("static".to_owned(), serialize_static(static_data)?);
272    fields.insert("lengths".to_owned(), serialize_object(lengths)?);
273    fields.insert(
274        "energy_windows".to_owned(),
275        serialize_energy_windows(energy_windows)?,
276    );
277    fields.insert(
278        "price_blocks".to_owned(),
279        serialize_price_blocks(price_blocks)?,
280    );
281    fields.insert(
282        "ac_contingency_survivors".to_owned(),
283        serialize_survivors(ac_contingency_survivors)?,
284    );
285    fields.insert(
286        "dc_contingency_flows".to_owned(),
287        serialize_rows(dc_contingency_flows)?,
288    );
289    fields.insert(
290        "violation_cost".to_owned(),
291        serialize_object(violation_cost)?,
292    );
293    fields.insert(
294        "device_class_layout".to_owned(),
295        serde_json::to_value(device_class_layout)?,
296    );
297    Ok(serde_json::to_value(Envelope {
298        schema: SCOPF_SCHEMA,
299        powerio_version: powerio::VERSION,
300        index_base: 1,
301        instance: Value::Object(fields),
302    })?)
303}
304
305/// Serialize an internal instance as the 1-based Julia document.
306pub fn to_json(instance: &ScopfInstance) -> ScopfResult<String> {
307    Ok(serde_json::to_string(&to_json_value(instance)?)?)
308}
309
310fn serialize_static(data: &ScopfStaticData) -> ScopfResult<Value> {
311    let ScopfStaticData {
312        bus,
313        shunt,
314        acl_branch,
315        acx_branch,
316        vpd,
317        fpd,
318        vwr,
319        fwr,
320        dc_branch,
321        prod,
322        cons,
323        active_reserve,
324        reactive_reserve,
325        active_reserve_set_pr,
326        active_reserve_set_cs,
327        reactive_reserve_set_pr,
328        reactive_reserve_set_cs,
329    } = data;
330    let mut object = Map::new();
331    object.insert("bus".to_owned(), serialize_rows(bus)?);
332    object.insert("shunt".to_owned(), serialize_rows(shunt)?);
333    object.insert("acl_branch".to_owned(), serialize_rows(acl_branch)?);
334    object.insert("acx_branch".to_owned(), serialize_rows(acx_branch)?);
335    object.insert("vpd".to_owned(), serialize_rows(vpd)?);
336    object.insert("fpd".to_owned(), serialize_rows(fpd)?);
337    object.insert("vwr".to_owned(), serialize_rows(vwr)?);
338    object.insert("fwr".to_owned(), serialize_rows(fwr)?);
339    object.insert("dc_branch".to_owned(), serialize_rows(dc_branch)?);
340    object.insert("prod".to_owned(), serialize_rows(prod)?);
341    object.insert("cons".to_owned(), serialize_rows(cons)?);
342    object.insert("active_reserve".to_owned(), serialize_rows(active_reserve)?);
343    object.insert(
344        "reactive_reserve".to_owned(),
345        serialize_rows(reactive_reserve)?,
346    );
347    object.insert(
348        "active_reserve_set_pr".to_owned(),
349        serialize_rows(active_reserve_set_pr)?,
350    );
351    object.insert(
352        "active_reserve_set_cs".to_owned(),
353        serialize_rows(active_reserve_set_cs)?,
354    );
355    object.insert(
356        "reactive_reserve_set_pr".to_owned(),
357        serialize_rows(reactive_reserve_set_pr)?,
358    );
359    object.insert(
360        "reactive_reserve_set_cs".to_owned(),
361        serialize_rows(reactive_reserve_set_cs)?,
362    );
363    Ok(Value::Object(object))
364}
365
366fn serialize_energy_windows(windows: &ScopfEnergyWindows) -> ScopfResult<Value> {
367    let ScopfEnergyWindows {
368        w_en_max_pr,
369        w_en_max_cs,
370        w_en_min_pr,
371        w_en_min_cs,
372        t_w_en_max_pr,
373        t_w_en_max_cs,
374        t_w_en_min_pr,
375        t_w_en_min_cs,
376    } = windows;
377    let mut object = Map::new();
378    object.insert("W_en_max_pr".to_owned(), serialize_rows(w_en_max_pr)?);
379    object.insert("W_en_max_cs".to_owned(), serialize_rows(w_en_max_cs)?);
380    object.insert("W_en_min_pr".to_owned(), serialize_rows(w_en_min_pr)?);
381    object.insert("W_en_min_cs".to_owned(), serialize_rows(w_en_min_cs)?);
382    object.insert("T_w_en_max_pr".to_owned(), serialize_rows(t_w_en_max_pr)?);
383    object.insert("T_w_en_max_cs".to_owned(), serialize_rows(t_w_en_max_cs)?);
384    object.insert("T_w_en_min_pr".to_owned(), serialize_rows(t_w_en_min_pr)?);
385    object.insert("T_w_en_min_cs".to_owned(), serialize_rows(t_w_en_min_cs)?);
386    Ok(Value::Object(object))
387}
388
389fn serialize_price_blocks(blocks: &ScopfPriceBlocks) -> ScopfResult<Value> {
390    let ScopfPriceBlocks { producer, consumer } = blocks;
391    let mut object = Map::new();
392    object.insert("producer".to_owned(), serialize_rows(producer)?);
393    object.insert("consumer".to_owned(), serialize_rows(consumer)?);
394    Ok(Value::Object(object))
395}
396
397fn serialize_survivors(survivors: &ScopfAcContingencySurvivors) -> ScopfResult<Value> {
398    let ScopfAcContingencySurvivors { ln, xf } = survivors;
399    let mut object = Map::new();
400    object.insert("ln".to_owned(), serialize_nested_rows(ln)?);
401    object.insert("xf".to_owned(), serialize_nested_rows(xf)?);
402    Ok(Value::Object(object))
403}
404
405fn serialize_rows<R: SerializedFields>(rows: &[R]) -> ScopfResult<Value> {
406    rows.iter()
407        .map(serialize_object)
408        .collect::<ScopfResult<Vec<_>>>()
409        .map(Value::from)
410}
411
412fn serialize_nested_rows<R: SerializedFields>(groups: &[Vec<R>]) -> ScopfResult<Value> {
413    groups
414        .iter()
415        .map(|group| serialize_rows(group))
416        .collect::<ScopfResult<Vec<_>>>()
417        .map(Value::from)
418}
419
420/// Serialize one struct, renumber its declared index fields, apply its
421/// renames. The declared fields always exist in the serialized object (the
422/// classification is compile-checked against the struct), so a miss here means
423/// a `serde` attribute changed the serialized name; fail loudly.
424fn serialize_object<R: SerializedFields>(row: &R) -> ScopfResult<Value> {
425    let mut value = serde_json::to_value(row)?;
426    let Some(object) = value.as_object_mut() else {
427        return Err(ScopfError::invalid(
428            "struct did not serialize to a JSON object",
429        ));
430    };
431    for &field in R::INDEX_FIELDS {
432        let index = object
433            .get_mut(field)
434            .ok_or_else(|| ScopfError::invalid(format!("index field `{field}` not serialized")))?;
435        let Some(zero_based) = index.as_u64() else {
436            return Err(ScopfError::invalid(format!(
437                "index field `{field}` is not an unsigned integer"
438            )));
439        };
440        *index = Value::from(zero_based + 1);
441    }
442    for &(from, to) in R::RENAMED_FIELDS {
443        let renamed = object
444            .remove(from)
445            .ok_or_else(|| ScopfError::invalid(format!("renamed field `{from}` not serialized")))?;
446        object.insert(to.to_owned(), renamed);
447    }
448    Ok(value)
449}