use super::{EarthModel, Geometry, wire};
use crate::dss::{
line_history::Snapshot,
raw::{RawDss, RawProp},
};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, serde::Serialize)]
pub(in crate::dss) struct Conductor {
pub wire: Option<usize>,
pub x: f64,
pub height: f64,
pub units: Option<usize>,
}
#[derive(Clone, Debug)]
pub(in crate::dss) struct Definition {
pub conductors: Vec<Conductor>,
pub phases: usize,
pub reduce: bool,
pub normamps: f64,
pub emergamps: f64,
pub cable_allocations: usize,
cn_choices: Vec<bool>,
active: usize,
last_unit: usize,
}
impl Default for Definition {
fn default() -> Self {
Self {
conductors: Vec::new(),
phases: 0,
reduce: false,
normamps: 0.0,
emergamps: 0.0,
cable_allocations: 0,
cn_choices: Vec::new(),
active: 0,
last_unit: 5,
}
}
}
impl Definition {
fn resize(&mut self, n: usize) {
if self.cn_choices.iter().any(|v| *v) {
self.cable_allocations += 1;
}
self.cn_choices = vec![false; n];
self.conductors = vec![
Conductor {
wire: None,
x: 0.0,
height: 0.0,
units: None
};
n
];
self.active = 0;
self.last_unit = 5;
}
fn copy(&mut self, source: &Self) {
self.resize(source.conductors.len());
self.conductors.clone_from(&source.conductors);
self.phases = source.phases;
self.reduce = source.reduce;
self.normamps = source.normamps;
self.emergamps = source.emergamps;
}
fn bind(
&mut self,
index: usize,
point: Snapshot,
raw: &RawDss,
budget: &mut usize,
) -> Result<(), String> {
self.conductors
.get_mut(index)
.ok_or("invalid geometry conductor selector")?
.wire = Some(point.object);
if index == 0 {
let source = wire::resolve_with_budget(raw, point, budget)?;
if self.normamps == 0.0 && source.normamps > 0.0 {
self.normamps = source.normamps;
}
if self.emergamps == 0.0 && source.emergamps > 0.0 {
self.emergamps = source.emergamps;
}
}
Ok(())
}
fn bind_property(
&mut self,
raw: &RawDss,
object: usize,
offset: usize,
key: &str,
budget: &mut usize,
) -> Result<(), String> {
let refs = raw.line_history[&object]
.wires
.get(&offset)
.ok_or("missing WireData binding")?;
let single = matches!(key, "wire" | "cncable");
if key == "wire" && self.cn_choices.get(self.active) == Some(&true) {
return Err("replacing a CN cable with a bare phase wire is unqualified".into());
}
let start = if single {
self.active
} else if key == "wires" && self.cn_choices.get(self.active) == Some(&true) {
self.phases
} else {
0
};
let count = if single {
1
} else if key == "cncables" {
self.phases
} else {
self.conductors.len().saturating_sub(start)
};
if refs.len() != count || count == 0 {
return Err("geometry wire array has wrong length".into());
}
for (index, point) in refs.iter().enumerate() {
self.bind(
start + index,
point.ok_or("WireData did not exist at assignment")?,
raw,
budget,
)?;
}
if key == "wires" {
self.active = start + count - 1;
} else if key.starts_with("cn") {
let choice = self
.cn_choices
.get_mut(self.active)
.ok_or("invalid CN selector")?;
if !*choice {
self.cable_allocations += 1;
*choice = true;
}
}
Ok(())
}
fn apply(
&mut self,
raw: &RawDss,
object: usize,
offset: usize,
prop: &RawProp,
budget: &mut usize,
) -> Result<(), String> {
let key = prop
.name
.as_deref()
.ok_or("unnamed LineGeometry property")?;
let number = || -> Result<f64, String> {
let value = prop
.value
.to_f64(Some(&raw.vars))
.map_err(|e| e.to_string())?;
if value.is_finite() {
Ok(value)
} else {
Err(format!("nonfinite LineGeometry {key}"))
}
};
let count = |minimum: usize| -> Result<usize, String> {
let value = number()?;
if value.fract() != 0.0 || !(minimum as f64..=32.0).contains(&value) {
return Err(format!("invalid LineGeometry {key}"));
}
Ok(value as usize)
};
match key {
"nconds" => self.resize(count(1)?),
"nphases" => self.phases = count(0)?,
"cond" => {
self.active = count(1)? - 1;
self.conductors
.get_mut(self.active)
.ok_or("invalid geometry conductor selector")?
.units
.get_or_insert(self.last_unit);
}
"x" | "h" | "units" => {
let conductor = self
.conductors
.get_mut(self.active)
.ok_or("geometry has no active conductor")?;
match key {
"x" => conductor.x = number()?,
"h" => conductor.height = number()?,
_ => {
let units = crate::dss::linecode::units(&prop.value.text);
conductor.units = Some(units);
self.last_unit = units;
}
}
}
"wire" | "wires" | "cncable" | "cncables" => {
self.bind_property(raw, object, offset, key, budget)?;
}
"spacing" => {
let point = raw.line_history[&object]
.spacings
.get(&offset)
.ok_or("missing LineSpacing binding")?
.ok_or("LineSpacing did not exist at assignment")?;
let spacing = super::spacing::resolve_with_budget(raw, point, budget)?;
if spacing.x.len() != self.conductors.len() {
return Err("LineSpacing has the wrong number of conductors".into());
}
self.last_unit = spacing.units;
for (i, conductor) in self.conductors.iter_mut().enumerate() {
if !spacing.x[i].is_finite() || !spacing.height[i].is_finite() {
return Err("LineSpacing coordinates were not initialized".into());
}
conductor.x = spacing.x[i];
conductor.height = spacing.height[i];
conductor.units = Some(spacing.units);
}
}
"normamps" => self.normamps = number()?,
"emergamps" => self.emergamps = number()?,
"reduce" => self.reduce = prop.value.text.starts_with(['y', 'Y', 't', 'T']),
"seasons" | "ratings" | "linetype" => {}
_ => return Err(format!("unqualified LineGeometry property {key}")),
}
Ok(())
}
pub fn materialize(
&self,
raw: &RawDss,
earth_model: EarthModel,
rho: f64,
mut visible_wire: impl FnMut(usize) -> Snapshot,
) -> Result<Geometry, String> {
let mut budget = 1_usize << 22;
let wires = self
.conductors
.iter()
.map(|conductor| {
let object = conductor.wire.ok_or("geometry conductor has no WireData")?;
let state = wire::resolve_with_budget(raw, visible_wire(object), &mut budget)?;
let meters = wire::meters(conductor.units.unwrap_or(0));
Ok(state.at(conductor.x * meters, conductor.height * meters))
})
.collect::<Result<Vec<_>, String>>()?;
if self.cn_choices.iter().any(|v| *v) && !wires.iter().any(|w| w.cable.is_some()) {
return Err("replacing all CN cables with bare phase wires is unqualified".into());
}
Ok(Geometry {
wires,
phases: self.phases,
reduce: self.reduce,
reduction_order: None,
earth_model,
rho,
})
}
}
pub(super) fn resolve(raw: &RawDss, point: Snapshot) -> Result<Definition, String> {
let mut cache = HashMap::new();
let mut visiting = HashSet::new();
let mut pending = vec![(point, false)];
let mut budget = 1_usize << 22;
while let Some((point, ready)) = pending.pop() {
if cache.contains_key(&point) {
continue;
}
let object = raw
.objects
.get(point.object)
.ok_or("missing LineGeometry object")?;
if object.class != "linegeometry" {
return Err("expected LineGeometry history".into());
}
let history = raw
.line_history
.get(&point.object)
.ok_or("missing LineGeometry history")?;
let copies = history
.copies
.get(..point.copies)
.ok_or("invalid LineGeometry copy prefix")?;
budget = budget
.checked_sub(1)
.ok_or("LineGeometry evaluation work limit exceeded")?;
if ready {
let mut state = Definition::default();
let props = object
.props
.get(..point.properties)
.ok_or("invalid LineGeometry property prefix")?;
let mut offset = 0;
for copy in copies {
if copy.source.object == point.object {
return Err(
"native LineGeometry self-copy resets its own conductor data".into(),
);
}
for (i, prop) in props
.get(offset..copy.start)
.ok_or("invalid geometry copy span")?
.iter()
.enumerate()
{
state.apply(raw, point.object, offset + i, prop, &mut budget)?;
}
budget = budget
.checked_sub(copy.start - offset + 32)
.ok_or("LineGeometry evaluation work limit exceeded")?;
let source: &Definition = cache
.get(©.source)
.ok_or("unresolved LineGeometry copy")?;
if source.cn_choices.iter().any(|v| *v) {
return Err("pinned native engine cannot copy a CN LineGeometry".into());
}
state.copy(source);
offset = copy.end;
}
let remaining = props.get(offset..).ok_or("invalid geometry copy span")?;
budget = budget
.checked_sub(remaining.len())
.ok_or("LineGeometry evaluation work limit exceeded")?;
for (i, prop) in remaining.iter().enumerate() {
state.apply(raw, point.object, offset + i, prop, &mut budget)?;
}
cache.insert(point, state);
visiting.remove(&point);
} else {
if !visiting.insert(point) {
return Err("cyclic LineGeometry history".into());
}
pending.push((point, true));
for copy in copies.iter().rev() {
pending.push((copy.source, false));
}
}
}
cache
.remove(&point)
.ok_or_else(|| "missing resolved LineGeometry".into())
}
#[cfg(test)]
mod tests;