Skip to main content

powerio_tx/network/
transformer_3w_serde.rs

1//! The historical transformer body and the fail-closed partial-winding record.
2//!
3//! Only nondefault winding states need a new representation. Nesting every
4//! historical required field makes older readers reject it rather than silently
5//! energizing a disconnected winding. This applies inside every IR document.
6
7use std::fmt;
8
9use serde::de::{Error as _, IgnoredAny, MapAccess, Visitor};
10use serde::ser::SerializeStruct;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13#[cfg(doc)]
14use super::Bus;
15use super::{Extras, Impedance, Transformer3W, WINDING_IN_SERVICE_EXTRA, Winding};
16
17const PARTIAL_TYPE: &str = "powerio.PartialTransformer3W";
18
19/// A three winding transformer with three terminal buses joined at a star point.
20///
21/// Series impedance is stored for winding pairs 1-2, 2-3, and 3-1. The record
22/// also retains star point voltage and per winding control data. PSS/E three
23/// winding records and PSLF tertiary winding records map to this type.
24/// [`to_star_expansion`](Transformer3W::to_star_expansion) turns it into the synthetic
25/// star bus plus three branches for a consumer that works in the bus-branch model;
26/// [`IndexedNetwork`](crate::IndexedNetwork) applies it before building any matrix,
27/// so a 3-winding transformer contributes to `Y_bus` and connectivity.
28#[derive(Deserialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30struct Transformer3WFields {
31    /// The three windings, in order (primary, secondary, tertiary).
32    windings: [Winding; 3],
33    /// Pairwise series impedance `[z12, z23, z31]` (primary-secondary,
34    /// secondary-tertiary, tertiary-primary), each per unit on the system base
35    /// with its declared MVA base.
36    z: [Impedance; 3],
37    /// Star-point voltage magnitude (p.u.) and angle (degrees), as solved.
38    star_vm: f64,
39    star_va: f64,
40    /// Magnetizing shunt referred to the star point (p.u. on the system base).
41    mag_g: f64,
42    mag_b: f64,
43    in_service: bool,
44    name: Option<String>,
45    /// Stable row identity; see [`Bus::uid`].
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    uid: Option<String>,
48    extras: Extras,
49    // Reserved structural fields must not be smuggled into a legacy body.
50    #[serde(default, rename = "type", deserialize_with = "reject_reserved")]
51    #[cfg_attr(feature = "schema", schemars(skip))]
52    _reserved_type: (),
53    #[serde(default, rename = "value", deserialize_with = "reject_reserved")]
54    #[cfg_attr(feature = "schema", schemars(skip))]
55    _reserved_value: (),
56}
57
58fn reject_reserved<'de, D: Deserializer<'de>>(_: D) -> Result<(), D::Error> {
59    Err(D::Error::custom(
60        "a transformer body cannot contain structural type/value fields",
61    ))
62}
63
64impl From<Transformer3WFields> for Transformer3W {
65    fn from(fields: Transformer3WFields) -> Self {
66        Self {
67            windings: fields.windings,
68            z: fields.z,
69            star_vm: fields.star_vm,
70            star_va: fields.star_va,
71            mag_g: fields.mag_g,
72            mag_b: fields.mag_b,
73            in_service: fields.in_service,
74            name: fields.name,
75            uid: fields.uid,
76            extras: fields.extras,
77        }
78    }
79}
80
81struct TransformerBody<'a>(&'a Transformer3W);
82
83impl Serialize for TransformerBody<'_> {
84    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
85        let t = self.0;
86        let mut body =
87            serializer.serialize_struct("Transformer3W", 9 + usize::from(t.uid.is_some()))?;
88        body.serialize_field("windings", &t.windings)?;
89        body.serialize_field("z", &t.z)?;
90        body.serialize_field("star_vm", &t.star_vm)?;
91        body.serialize_field("star_va", &t.star_va)?;
92        body.serialize_field("mag_g", &t.mag_g)?;
93        body.serialize_field("mag_b", &t.mag_b)?;
94        body.serialize_field("in_service", &t.in_service)?;
95        body.serialize_field("name", &t.name)?;
96        if let Some(uid) = &t.uid {
97            body.serialize_field("uid", uid)?;
98        }
99        body.serialize_field("extras", &t.extras)?;
100        body.end()
101    }
102}
103
104impl Serialize for Transformer3W {
105    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
106        if self.winding_in_service() == [true; 3] {
107            return TransformerBody(self).serialize(serializer);
108        }
109        let mut wrapper = serializer.serialize_struct("PartialTransformer3W", 2)?;
110        wrapper.serialize_field("type", PARTIAL_TYPE)?;
111        wrapper.serialize_field("value", &TransformerBody(self))?;
112        wrapper.end()
113    }
114}
115
116#[derive(Deserialize)]
117enum PartialType {
118    #[serde(rename = "powerio.PartialTransformer3W")]
119    Partial,
120}
121
122#[derive(Deserialize)]
123#[serde(field_identifier, rename_all = "snake_case")]
124enum Field {
125    Windings,
126    Z,
127    StarVm,
128    StarVa,
129    MagG,
130    MagB,
131    InService,
132    Name,
133    Uid,
134    Extras,
135    Type,
136    Value,
137    #[serde(other)]
138    Other,
139}
140
141impl<'de> Deserialize<'de> for Transformer3W {
142    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
143        struct TransformerVisitor;
144        impl<'de> Visitor<'de> for TransformerVisitor {
145            type Value = Transformer3W;
146
147            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148                formatter.write_str("a legacy transformer or powerio.PartialTransformer3W record")
149            }
150
151            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
152                let mut windings = None;
153                let mut z = None;
154                let mut star_vm = None;
155                let mut star_va = None;
156                let mut mag_g = None;
157                let mut mag_b = None;
158                let mut in_service = None;
159                let mut name = None;
160                let mut uid = None;
161                let mut extras = None;
162                let mut kind = None;
163                let mut value: Option<Transformer3WFields> = None;
164                let mut has_body = false;
165                // Decode directly, without buffering a JSON value or recursively
166                // decoding a Transformer3W. Winding/impedance arrays stay fixed
167                // at three elements and the wrapper admits exactly one body.
168                macro_rules! read_field {
169                    ($field:ident) => {{
170                        if $field.is_some() {
171                            return Err(A::Error::duplicate_field(stringify!($field)));
172                        }
173                        $field = Some(map.next_value()?);
174                        has_body = true;
175                    }};
176                }
177                while let Some(field) = map.next_key()? {
178                    match field {
179                        Field::Windings => read_field!(windings),
180                        Field::Z => read_field!(z),
181                        Field::StarVm => read_field!(star_vm),
182                        Field::StarVa => read_field!(star_va),
183                        Field::MagG => read_field!(mag_g),
184                        Field::MagB => read_field!(mag_b),
185                        Field::InService => read_field!(in_service),
186                        Field::Name => read_field!(name),
187                        Field::Uid => read_field!(uid),
188                        Field::Extras => read_field!(extras),
189                        Field::Type => {
190                            if kind.is_some() {
191                                return Err(A::Error::duplicate_field("type"));
192                            }
193                            kind = Some(map.next_value::<PartialType>()?);
194                        }
195                        Field::Value => {
196                            if value.is_some() {
197                                return Err(A::Error::duplicate_field("value"));
198                            }
199                            value = Some(map.next_value()?);
200                        }
201                        Field::Other => {
202                            map.next_value::<IgnoredAny>()?;
203                        }
204                    }
205                }
206                if kind.is_some() || value.is_some() {
207                    if has_body {
208                        return Err(A::Error::custom(
209                            "powerio.PartialTransformer3W requires all transformer fields inside value",
210                        ));
211                    }
212                    kind.ok_or_else(|| A::Error::missing_field("type"))?;
213                    let transformer =
214                        Transformer3W::from(value.ok_or_else(|| A::Error::missing_field("value"))?);
215                    let flags = transformer.extras.get(WINDING_IN_SERVICE_EXTRA)
216                        .and_then(|value| <[bool; 3]>::deserialize(value).ok())
217                        .ok_or_else(|| A::Error::custom("powerio.PartialTransformer3W requires three boolean winding_in_service flags"))?;
218                    if flags == [true; 3] {
219                        return Err(A::Error::custom(
220                            "powerio.PartialTransformer3W requires a nondefault winding state",
221                        ));
222                    }
223                    return Ok(transformer);
224                }
225                let transformer = Transformer3W {
226                    windings: windings.ok_or_else(|| A::Error::missing_field("windings"))?,
227                    z: z.ok_or_else(|| A::Error::missing_field("z"))?,
228                    star_vm: star_vm.ok_or_else(|| A::Error::missing_field("star_vm"))?,
229                    star_va: star_va.ok_or_else(|| A::Error::missing_field("star_va"))?,
230                    mag_g: mag_g.ok_or_else(|| A::Error::missing_field("mag_g"))?,
231                    mag_b: mag_b.ok_or_else(|| A::Error::missing_field("mag_b"))?,
232                    in_service: in_service.ok_or_else(|| A::Error::missing_field("in_service"))?,
233                    name: name.unwrap_or(None),
234                    uid: uid.unwrap_or(None),
235                    extras: extras.ok_or_else(|| A::Error::missing_field("extras"))?,
236                };
237                if transformer.winding_in_service() != [true; 3] {
238                    return Err(A::Error::custom(
239                        "nondefault winding states require a powerio.PartialTransformer3W record",
240                    ));
241                }
242                Ok(transformer)
243            }
244        }
245        deserializer.deserialize_map(TransformerVisitor)
246    }
247}
248
249#[cfg(feature = "schema")]
250#[derive(schemars::JsonSchema)]
251#[allow(dead_code)] // The payload is only used to generate the schema.
252#[serde(tag = "type", content = "value")]
253enum PartialTransformer3WWire {
254    #[serde(rename = "powerio.PartialTransformer3W")]
255    Partial(PartialTransformer3WFields),
256}
257
258#[cfg(feature = "schema")]
259struct PartialTransformer3WFields;
260
261#[cfg(feature = "schema")]
262impl schemars::JsonSchema for PartialTransformer3WFields {
263    fn schema_name() -> std::borrow::Cow<'static, str> {
264        "PartialTransformer3WFields".into()
265    }
266
267    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
268        let mut body = serde_json::to_value(Transformer3WFields::json_schema(generator))
269            .expect("transformer schema is serializable");
270        // Only the new alternative requires a well-formed nondefault state;
271        // the legacy body and all of its historical schemas stay untouched.
272        body["properties"]["extras"]["properties"] = serde_json::json!({
273            "winding_in_service": {
274                "type": "array", "minItems": 3, "maxItems": 3,
275                "items": {"type": "boolean"},
276                "not": {"const": [true, true, true]}
277            }
278        });
279        body["properties"]["extras"]["required"] = serde_json::json!(["winding_in_service"]);
280        serde_json::from_value(body).expect("an extended object remains a schema")
281    }
282}
283
284#[cfg(feature = "schema")]
285impl schemars::JsonSchema for Transformer3W {
286    fn schema_name() -> std::borrow::Cow<'static, str> {
287        "Transformer3W".into()
288    }
289
290    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
291        // Preserve the complete published body as the first alternative.
292        schemars::json_schema!({"anyOf": [
293            Transformer3WFields::json_schema(generator),
294            generator.subschema_for::<PartialTransformer3WWire>(),
295        ]})
296    }
297}