Skip to main content

v0.7.0 Release Notes

EnerOS v0.7.0

Release Date: July 7, 2024 Codename: Equipment Git Tag: v0.7.0 Support Status: Internal Preview Total Crates: 6 Test Cases: 942

Version Overview

EnerOS v0.7.0 “Equipment” introduces the power equipment model library. This version releases the eneros-equipment crate, providing detailed mathematical models for four major equipment categories: transformers (two-winding, three-winding), transmission lines (PI equivalent circuit), switches (breakers, disconnectors), and loads (ZIP model). The topology model of v0.3.0 only contained the “connection relationships” of equipment, while v0.7.0 fills in the “physical characteristics” of equipment — transformers have not only turns ratios but also tap positions, no-load losses, and short-circuit impedance; lines have not only R/X/B but also temperature and frequency characteristics; loads have not only active and reactive power but also voltage-frequency static characteristics.

The equipment model library design follows the dual specifications of IEC 61970 CIM and China’s power industry standard DL/T 698. CIM provides the ontological framework for equipment classes, while DL/T 698 defines engineering value ranges and naming conventions for equipment parameters. EnerOS maps between the two: externally, it can export CIM-compatible RDF/JSON; internally, it uses Rust enums to express equipment types and traits to express equipment behavior. For example, all equipment implements the Equipment trait, providing impedance(), admittance(), status() and other methods; transformers additionally implement the TapChanger trait, providing tap adjustment capability.

An important design decision of v0.7.0 is the separation of “equipment models” from “equipment instances”. TransformerModel describes the parameter template of a certain transformer model (such as SZ11-31500/110), while TransformerInstance describes a specific deployed transformer (such as “Substation A’s No. 1 main transformer”). This separation allows the equipment library to build model libraries based on type test data provided by manufacturers, and instantiate at runtime based on actually installed equipment. The model library preset parameters for 50+ common models, covering voltage levels from 10kV to 500kV.

Key Data

MetricValueDescription
Equipment types4 major categoriesTransformer/Line/Switch/Load
Preset models52Covers common manufacturers
Equipment model parameters32Per device average
Transformer tap positions±8Standard configuration
ZIP load characteristic coefficients3Z/I/P ratios

New Features

1. Transformer Model

Two-Winding Transformer

// crates/eneros-equipment/src/transformer/two_winding.rs
use serde::{Deserialize, Serialize};
use eneros_topology::bus::BusId;

/// Two-winding transformer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TwoWindingTransformer {
    pub id: String,
    pub name: String,
    pub hv_bus: BusId,          // High-voltage side bus
    pub lv_bus: BusId,          // Low-voltage side bus
    pub rated_hv_kv: f64,       // High-voltage side rated voltage
    pub rated_lv_kv: f64,       // Low-voltage side rated voltage
    pub rated_mva: f64,         // Rated capacity
    /// Short-circuit impedance percentage (%)
    pub short_circuit_z_pct: f64,
    /// Short-circuit resistance percentage (%), usually = copper loss / (rated capacity * 10)
    pub short_circuit_r_pct: f64,
    /// No-load current percentage (%)
    pub no_load_i_pct: f64,
    /// No-load loss (kW)
    pub no_load_loss_kw: f64,
    /// Tap changer configuration
    pub tap_changer: TapChanger,
    /// Whether in service
    pub in_service: bool,
}

/// Tap changer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TapChanger {
    /// Turns ratio at nominal tap
    pub nominal_tap: f64,
    /// Step adjustment per tap (percentage)
    pub step_pct: f64,
    /// Maximum tap (positive)
    pub max_tap: i32,
    /// Minimum tap (negative)
    pub min_tap: i32,
    /// Current tap
    pub current_tap: i32,
}

impl TapChanger {
    /// Current actual turns ratio
    pub fn current_ratio(&self) -> f64 {
        self.nominal_tap * (1.0 + self.current_tap as f64 * self.step_pct / 100.0)
    }

    /// Raise tap
    pub fn raise(&mut self) -> Result<(), EquipmentError> {
        if self.current_tap >= self.max_tap {
            return Err(EquipmentError::TapAtMax);
        }
        self.current_tap += 1;
        Ok(())
    }

    /// Lower tap
    pub fn lower(&mut self) -> Result<(), EquipmentError> {
        if self.current_tap <= self.min_tap {
            return Err(EquipmentError::TapAtMin);
        }
        self.current_tap -= 1;
        Ok(())
    }
}

impl TwoWindingTransformer {
    /// Calculate per-unit parameters (based on given base capacity)
    pub fn pi_parameters(&self, base_mva: f64) -> PiParameters {
        let tap = self.tap_changer.current_ratio();
        // Convert impedance from percentage to per-unit
        let z_pu = (self.short_circuit_z_pct / 100.0) * (base_mva / self.rated_mva);
        let r_pu = (self.short_circuit_r_pct / 100.0) * (base_mva / self.rated_mva);
        let x_pu = (z_pu * z_pu - r_pu * r_pu).sqrt();
        // Magnetizing admittance
        let b_half = (self.no_load_i_pct / 100.0) * (base_mva / self.rated_mva);
        PiParameters {
            r_pu,
            x_pu,
            b_half_pu: b_half / 2.0,
            tap_ratio: tap,
            phase_shift: 0.0,
        }
    }
}

Three-Winding Transformer

// crates/eneros-equipment/src/transformer/three_winding.rs
/// Three-winding transformer: equivalent to three two-winding transformers (star equivalent circuit)
pub struct ThreeWindingTransformer {
    pub id: String,
    pub hv_bus: BusId,      // High-voltage side
    pub mv_bus: BusId,      // Medium-voltage side
    pub lv_bus: BusId,      // Low-voltage side
    pub rated_hv_mva: f64,
    pub rated_mv_mva: f64,
    pub rated_lv_mva: f64,
    pub z_hm_pct: f64,  // High-Medium short-circuit impedance
    pub z_hl_pct: f64,  // High-Low short-circuit impedance
    pub z_ml_pct: f64,  // Medium-Low short-circuit impedance
    // ... three pairs of short-circuit parameters
}

impl ThreeWindingTransformer {
    /// Convert to three branches of star equivalent circuit
    pub fn to_star_equivalent(&self) -> [PiParameters; 3] {
        // Convert three-winding parameters to star equivalent
        let z_h = 0.5 * (self.z_hm_pct + self.z_hl_pct - self.z_ml_pct);
        let z_m = 0.5 * (self.z_hm_pct + self.z_ml_pct - self.z_hl_pct);
        let z_l = 0.5 * (self.z_hl_pct + self.z_ml_pct - self.z_hm_pct);
        // Return PI parameters for H/M/L branches
        [
            PiParameters::transformer(z_h / 100.0, 0.0, 1.0),
            PiParameters::transformer(z_m / 100.0, 0.0, 1.0),
            PiParameters::transformer(z_l / 100.0, 0.0, 1.0),
        ]
    }
}

2. Line Model (PI Equivalent Circuit)

// crates/eneros-equipment/src/line.rs
use eneros_topology::bus::BusId;

/// Transmission line (PI equivalent circuit)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransmissionLine {
    pub id: String,
    pub from_bus: BusId,
    pub to_bus: BusId,
    pub length_km: f64,
    /// Resistance per unit length (Ω/km)
    pub r_per_km: f64,
    /// Reactance per unit length (Ω/km)
    pub x_per_km: f64,
    /// Susceptance per unit length (S/km)
    pub b_per_km: f64,
    /// Conductance per unit length (S/km)
    pub g_per_km: f64,
    /// Number of parallel circuits (1, 2, 3, 4)
    pub num_circuits: u32,
    /// Rated current (A, single circuit)
    pub rated_current_a: f64,
    /// Resistance at 20°C (temperature correction reference)
    pub reference_temp_c: f64,
    /// Temperature coefficient of resistance (1/°C)
    pub temp_coefficient: f64,
    pub in_service: bool,
}

impl TransmissionLine {
    /// Calculate PI parameters at a given temperature
    pub fn pi_parameters(&self, base_mva: f64, base_kv: f64, temp_c: f64) -> PiParameters {
        let z_base = base_kv * base_kv / base_mva;

        // Temperature correction
        let temp_factor = 1.0 + self.temp_coefficient * (temp_c - self.reference_temp_c);
        let r_total = self.r_per_km * self.length_km * temp_factor / z_base;
        let x_total = self.x_per_km * self.length_km / z_base;
        let b_total = self.b_per_km * self.length_km * z_base;

        // Parallel multi-circuit
        let n = self.num_circuits as f64;
        PiParameters {
            r_pu: r_total / n,
            x_pu: x_total / n,
            b_half_pu: b_total / (2.0 * n),
            tap_ratio: 1.0,
            phase_shift: 0.0,
        }
    }

    /// Calculate line thermal stability limit (A)
    pub fn thermal_limit_a(&self) -> f64 {
        self.rated_current_a * self.num_circuits as f64
    }
}

3. Switch Model

// crates/eneros-equipment/src/switch.rs
use eneros_topology::bus::BusId;

/// Switch type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SwitchKind {
    Breaker,        // Circuit breaker
    Disconnector,   // Disconnector
    LoadBreakSwitch,// Load break switch
    EarthingSwitch, // Earthing switch
    Fuse,           // Fuse
}

/// Switch state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SwitchState {
    Closed, // Closed
    Open,   // Open
    Tripped,// Tripped (fault)
}

/// Switch device
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Switch {
    pub id: String,
    pub from_bus: BusId,
    pub to_bus: BusId,
    pub kind: SwitchKind,
    pub state: SwitchState,
    /// Rated current (A)
    pub rated_current_a: f64,
    /// Rated breaking current (A, for breakers)
    pub breaking_capacity_a: f64,
    /// Operation count
    pub operation_count: u32,
    /// Last operation timestamp
    pub last_operated_at: Option<i64>,
}

impl Switch {
    pub fn new(id: impl Into<String>, from: BusId, to: BusId, kind: SwitchKind) -> Self {
        Self {
            id: id.into(),
            from_bus: from,
            to_bus: to,
            kind,
            state: SwitchState::Closed,
            rated_current_a: 1250.0,
            breaking_capacity_a: 25000.0,
            operation_count: 0,
            last_operated_at: None,
        }
    }

    /// Close
    pub fn close(&mut self) -> Result<(), EquipmentError> {
        if self.state == SwitchState::Closed {
            return Err(EquipmentError::AlreadyClosed);
        }
        self.state = SwitchState::Closed;
        self.operation_count += 1;
        self.last_operated_at = Some(chrono::Utc::now().timestamp());
        Ok(())
    }

    /// Open
    pub fn open(&mut self) -> Result<(), EquipmentError> {
        if self.state == SwitchState::Open {
            return Err(EquipmentError::AlreadyOpen);
        }
        self.state = SwitchState::Open;
        self.operation_count += 1;
        self.last_operated_at = Some(chrono::Utc::now().timestamp());
        Ok(())
    }

    /// Whether conducting
    pub fn is_conducting(&self) -> bool {
        self.state == SwitchState::Closed
    }
}

4. Load Model (ZIP)

The ZIP load model represents load as a weighted sum of constant impedance (Z), constant current (I), and constant power (P) components, and is the standard load model for power system steady-state analysis.

// crates/eneros-equipment/src/load.rs
use eneros_topology::bus::BusId;

/// ZIP load model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZipLoad {
    pub id: String,
    pub bus: BusId,
    /// Rated active power (MW)
    pub p_nominal_mw: f64,
    /// Rated reactive power (MVar)
    pub q_nominal_mvar: f64,
    /// Constant impedance ratio
    pub a_z: f64,
    /// Constant current ratio
    pub a_i: f64,
    /// Constant power ratio
    pub a_p: f64,
    /// Frequency static characteristic coefficient (active)
    pub kp_f: f64,
    /// Frequency static characteristic coefficient (reactive)
    pub kq_f: f64,
    pub in_service: bool,
}

impl ZipLoad {
    pub fn new(bus: BusId, p_mw: f64, q_mvar: f64) -> Self {
        Self {
            id: format!("load_{}", bus),
            bus,
            p_nominal_mw: p_mw,
            q_nominal_mvar: q_mvar,
            a_z: 0.4,
            a_i: 0.3,
            a_p: 0.3,
            kp_f: 1.0,
            kq_f: -1.0,
            in_service: true,
        }
    }

    /// Calculate actual power at given voltage and frequency
    pub fn power_at(&self, v_pu: f64, freq_hz: f64) -> (f64, f64) {
        let df = (freq_hz - 50.0) / 50.0;
        let v2 = v_pu * v_pu;
        let v3 = v2 * v_pu;

        let p = self.p_nominal_mw * (
            self.a_z * v2 + self.a_i * v_pu + self.a_p
        ) * (1.0 + self.kp_f * df);

        let q = self.q_nominal_mvar * (
            self.a_z * v2 + self.a_i * v_pu + self.a_p
        ) * (1.0 + self.kq_f * df);

        (p, q)
    }
}

ZIP model characteristic comparison:

Load TypeVoltage DependencyPower-Voltage RelationshipApplicable Equipment
Constant impedance ZP ∝ V²Heaters, incandescent lamps
Constant current IP ∝ VGeneral industrial loads
Constant power PV⁰P = constMotor drives (variable frequency)
Typical mix-40%Z+30%I+30%PComposite load

5. Preset Equipment Model Library

// crates/eneros-equipment/src/catalog.rs
use crate::transformer::TwoWindingTransformer;

/// Preset transformer model library
pub fn transformer_catalog() -> Vec<TwoWindingTransformer> {
    vec![
        // SZ11-31500/110 (110kV/10kV, 31.5MVA)
        TwoWindingTransformer {
            id: "SZ11-31500/110".into(),
            name: "SZ11-31500/110 Three-phase double-winding on-load tap-changing".into(),
            rated_mva: 31.5,
            rated_hv_kv: 110.0,
            rated_lv_kv: 10.5,
            short_circuit_z_pct: 10.5,
            short_circuit_r_pct: 0.5,
            no_load_i_pct: 0.7,
            no_load_loss_kw: 29.0,
            // ...
        },
        // SZ11-63000/220
        // SZ11-150000/500
        // ...
    ]
}

Improvements

  • eneros-topology: Branch supports associating detailed equipment models
  • eneros-powerflow: Power flow calculation considers transformer tap ratio
  • Dependencies: Added chrono, uuid

Bug Fixes

  • Fixed base capacity conversion error in TwoWindingTransformer::pi_parameters (#103)
  • Fixed TransmissionLine::pi_parameters not dividing by circuit count for parallel multi-circuit (#107)
  • Fixed Switch::open not returning an error when already open (#110)

Breaking Changes

  • eneros_topology::Branch: params field changed to Option<PiParameters>, calculated by equipment model at runtime

Performance Improvements

OperationTime
Transformer PI parameter calculation240 ns
Line PI parameter calculation180 ns
ZIP load power calculation95 ns
Switch state transition32 ns
Model library loading (52 models)1.4 ms

Contributors

ContributorRoleCommits
@eneros-foundationArchitect28
@equipment-expertEquipment Modeling Expert52
@transformer-guruTransformer Expert31
@grid-rustaceanRust Engineer22

Upgrade Guide

New Dependencies

[dependencies]
eneros-equipment = { version = "0.7", path = "../eneros-equipment" }

Using Equipment Models

use eneros_equipment::transformer::TwoWindingTransformer;
use eneros_topology::bus::BusId;

let mut transformer = TwoWindingTransformer {
    id: "T1".into(),
    hv_bus: BusId(1),
    lv_bus: BusId(2),
    rated_hv_kv: 110.0,
    rated_lv_kv: 10.5,
    rated_mva: 31.5,
    short_circuit_z_pct: 10.5,
    // ...
};
transformer.tap_changer.raise()?;  // Raise one tap
let pi = transformer.pi_parameters(100.0);