Skip to main content

powerio_dist/model/
voltage_source.rs

1use serde::{Deserialize, Serialize, Serializer};
2
3use super::Extras;
4
5/// An ideal source prescribing terminal voltages relative to earth or one
6/// explicit terminal on the same bus. A reference terminal is a conductor,
7/// not an implicit grounding instruction.
8#[derive(Clone, Debug, PartialEq, Deserialize)]
9#[serde(try_from = "SourceWire")]
10#[non_exhaustive]
11pub struct VoltageSource {
12    pub name: String,
13    pub bus: String,
14    pub terminal_map: Vec<String>,
15    /// Volts per terminal, relative to `reference_terminal` or earth.
16    pub v_magnitude: Vec<f64>,
17    /// Radians per terminal, with the same reference as `v_magnitude`.
18    pub v_angle: Vec<f64>,
19    /// None preserves the historical earth-referenced source semantics.
20    /// Some names a declared terminal on `bus`; its voltage remains unknown
21    /// unless the rest of the circuit fixes it. Each phase current has an
22    /// equal opposite contribution at this reference terminal.
23    pub reference_terminal: Option<String>,
24    /// Energy cost rate in $/kWh, one entry per phase in terminal-map order.
25    pub energy_cost_rate: Option<Vec<f64>>,
26    pub extras: Extras,
27}
28
29impl VoltageSource {
30    #[must_use]
31    pub fn new(
32        name: impl Into<String>,
33        bus: impl Into<String>,
34        terminal_map: Vec<String>,
35        v_magnitude: Vec<f64>,
36        v_angle: Vec<f64>,
37    ) -> Self {
38        Self {
39            name: name.into(),
40            bus: bus.into(),
41            terminal_map,
42            v_magnitude,
43            v_angle,
44            reference_terminal: None,
45            energy_cost_rate: None,
46            extras: Extras::new(),
47        }
48    }
49
50    #[must_use]
51    pub fn with_reference_terminal(mut self, terminal: impl Into<String>) -> Self {
52        self.reference_terminal = Some(terminal.into());
53        self
54    }
55}
56
57// Keep the historical body exactly the same for earth-referenced sources.
58// A new source has a tagged wrapper, so a historical reader cannot accept it
59// while ignoring essential physics: its required name/bus/phasor fields are
60// absent at the wrapper level. This works recursively in every IR document.
61#[derive(Deserialize)]
62#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63struct SourceFields {
64    name: String,
65    bus: String,
66    terminal_map: Vec<String>,
67    /// Volts per terminal (0.0 on grounded terminals).
68    v_magnitude: Vec<f64>,
69    /// Radians per terminal.
70    v_angle: Vec<f64>,
71    /// Energy cost rate in $/kWh, one entry per phase in terminal-map order.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    energy_cost_rate: Option<Vec<f64>>,
74    extras: Extras,
75    #[serde(flatten)]
76    #[cfg_attr(feature = "schema", schemars(skip))]
77    unknown: std::collections::BTreeMap<String, serde_json::Value>,
78}
79
80#[derive(Deserialize)]
81struct ReferencedSourceFields {
82    #[serde(flatten)]
83    source: SourceFields,
84    reference_terminal: String,
85}
86
87#[cfg(feature = "schema")]
88impl schemars::JsonSchema for ReferencedSourceFields {
89    fn schema_name() -> std::borrow::Cow<'static, str> {
90        "ReferencedSourceFields".into()
91    }
92
93    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
94        let mut value = serde_json::to_value(SourceFields::json_schema(generator))
95            .expect("source schema is serializable");
96        value["properties"]["reference_terminal"] = serde_json::json!({
97            "type": "string", "description": "Declared reference terminal on the source bus; does not imply grounding."
98        });
99        value["properties"]["v_magnitude"]["description"] =
100            "Volts per terminal-to-reference difference, including at grounded phase terminals."
101                .into();
102        value["properties"]["v_angle"]["description"] =
103            "Radians per terminal-to-reference difference.".into();
104        value["required"]
105            .as_array_mut()
106            .expect("source schema has required fields")
107            .push("reference_terminal".into());
108        serde_json::from_value(value).expect("an extended object remains a schema")
109    }
110}
111
112#[derive(Deserialize)]
113#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
114#[serde(tag = "type", content = "value")]
115enum ReferencedSourceWire {
116    #[serde(rename = "powerio.ReferencedVoltageSource")]
117    Referenced(ReferencedSourceFields),
118}
119
120#[derive(Deserialize)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122#[serde(untagged)]
123enum SourceWire {
124    Referenced(ReferencedSourceWire),
125    Earth(SourceFields),
126}
127
128impl TryFrom<SourceWire> for VoltageSource {
129    type Error = String;
130
131    fn try_from(wire: SourceWire) -> Result<Self, Self::Error> {
132        let (fields, reference_terminal) = match wire {
133            SourceWire::Earth(fields) => {
134                if ["reference_terminal", "type", "value"]
135                    .iter()
136                    .any(|key| fields.unknown.contains_key(*key))
137                {
138                    return Err("source reference semantics require a powerio.ReferencedVoltageSource record".into());
139                }
140                (fields, None)
141            }
142            SourceWire::Referenced(ReferencedSourceWire::Referenced(fields)) => {
143                (fields.source, Some(fields.reference_terminal))
144            }
145        };
146        Ok(Self {
147            name: fields.name,
148            bus: fields.bus,
149            terminal_map: fields.terminal_map,
150            v_magnitude: fields.v_magnitude,
151            v_angle: fields.v_angle,
152            energy_cost_rate: fields.energy_cost_rate,
153            extras: fields.extras,
154            reference_terminal,
155        })
156    }
157}
158
159struct SourceBody<'a>(&'a VoltageSource);
160
161impl Serialize for SourceBody<'_> {
162    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163        use serde::ser::SerializeStruct;
164        let s = self.0;
165        let count = 6
166            + usize::from(s.energy_cost_rate.is_some())
167            + usize::from(s.reference_terminal.is_some());
168        let mut body = serializer.serialize_struct("VoltageSource", count)?;
169        body.serialize_field("name", &s.name)?;
170        body.serialize_field("bus", &s.bus)?;
171        body.serialize_field("terminal_map", &s.terminal_map)?;
172        body.serialize_field("v_magnitude", &s.v_magnitude)?;
173        body.serialize_field("v_angle", &s.v_angle)?;
174        if let Some(cost) = &s.energy_cost_rate {
175            body.serialize_field("energy_cost_rate", cost)?;
176        }
177        body.serialize_field("extras", &s.extras)?;
178        if let Some(reference) = &s.reference_terminal {
179            body.serialize_field("reference_terminal", reference)?;
180        }
181        body.end()
182    }
183}
184
185impl Serialize for VoltageSource {
186    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
187        use serde::ser::SerializeStruct;
188        if self.reference_terminal.is_none() {
189            return SourceBody(self).serialize(serializer);
190        }
191        let mut wrapper = serializer.serialize_struct("ReferencedVoltageSource", 2)?;
192        wrapper.serialize_field("type", "powerio.ReferencedVoltageSource")?;
193        wrapper.serialize_field("value", &SourceBody(self))?;
194        wrapper.end()
195    }
196}
197
198#[cfg(feature = "schema")]
199impl schemars::JsonSchema for VoltageSource {
200    fn schema_name() -> std::borrow::Cow<'static, str> {
201        "VoltageSource".into()
202    }
203    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
204        // Retain the published source schema verbatim as the first branch.
205        // The other branch has a distinct, mandatory structural type tag.
206        schemars::json_schema!({"anyOf": [
207            SourceFields::json_schema(generator),
208            generator.subschema_for::<ReferencedSourceWire>(),
209        ]})
210    }
211}