powerio-dist 0.11.7

Multiconductor distribution network model and lossless converters for OpenDSS, PMD JSON, BMOPF JSON, and distribution CIM.
Documentation
//! Native static equivalent for signed bounded constant power.
use super::{Configuration, DistLoad, DistLoadVoltageModel, DssWriter, num};

pub(super) fn validate(load: &DistLoad) -> Result<(), String> {
    let DistLoadVoltageModel::BoundedConstantPower {
        v_min_pu,
        current_limit_pu,
        ..
    } = &load.voltage_model
    else {
        return Ok(());
    };
    load.voltage_model.power_scale(
        0,
        load.voltage_model
            .v_nom()
            .first()
            .copied()
            .unwrap_or(f64::NAN),
    )?;
    for phase in 0..load.p_nom.len() {
        load.voltage_model.power_scale(
            phase,
            load.voltage_model
                .v_nom()
                .get(phase)
                .copied()
                .unwrap_or(f64::NAN),
        )?;
    }
    let conductors = if load.configuration == Configuration::Delta && load.p_nom.len() == 3 {
        3
    } else {
        load.p_nom.len() + 1
    };
    if load.terminal_map.len() != conductors {
        return Err(format!(
            "load {}: bounded-power branches require complete conductor maps",
            load.name
        ));
    }
    if load.p_nom.is_empty()
        || load.p_nom.iter().any(|p| !p.is_finite())
        || load.q_nom.len() != load.p_nom.len()
        || load.q_nom.iter().any(|q| !q.is_finite())
        || load.p_nom.len() > 3
        || current_limit_pu.is_some_and(|limit| (limit * v_min_pu - 1.0).abs() > 1e-12)
    {
        return Err(format!(
            "load {}: native bounded-power equivalent requires one to three branches and a current limit equal to 1/v_min_pu",
            load.name
        ));
    }
    Ok(())
}

impl DssWriter {
    pub(super) fn write_static_storage(&mut self, load: &DistLoad) {
        let DistLoadVoltageModel::BoundedConstantPower {
            v_nom,
            v_min_pu,
            v_max_pu,
            current_limit_pu,
        } = &load.voltage_model
        else {
            unreachable!()
        };
        let phases = load.p_nom.len();
        // Single branch uses wye even across two hot conductors: native
        // one-phase delta has a different out-of-band voltage law.
        let conn = if load.configuration == Configuration::Delta && phases > 1 {
            "delta"
        } else {
            "wye"
        };
        let kv = v_nom[0] / 1000.0
            * if conn == "wye" && phases > 1 {
                3.0_f64.sqrt()
            } else {
                1.0
            };
        let kw = load.p_nom.iter().sum::<f64>() / 1000.0;
        let kvar = load.q_nom.iter().sum::<f64>() / 1000.0;
        let rating = (2.0 * kw.hypot(kvar)).max(1.0);
        let rated = kw.abs().max(1.0);
        let (state, idle, discharge) = if kw < 0.0 {
            ("discharging", 0.0, -kw / rated * 100.0)
        } else {
            ("idling", kw / rated * 100.0, 100.0)
        };
        self.check_name("storage", &load.name);
        for key in load
            .extras
            .keys()
            .filter(|key| !matches!(key.as_str(), "dss_original_element" | "phases"))
        {
            self.warn(&super::C::EMIT_DSS_EXTRAS_DROPPED, format!("load {}: extra {key} is not part of the typed bounded-power equivalent; omitted", load.name));
        }
        let bus = self.bus_ref(&load.bus, &load.terminal_map);
        self.line_out(&format!(
            "New Storage.{} phases={phases} bus1={} conn={conn} kv={} kwrated={} kva={} %idlingkw={} state={state} %discharge={} kvar={} vminpu={} vmaxpu={} limitcurrent={} model=1",
            load.name, bus, num(kv), num(rated), num(rating), num(idle), num(discharge), num(-kvar), num(*v_min_pu), num(*v_max_pu), if current_limit_pu.is_some() { "yes" } else { "no" }
        ));
    }
}