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
| Metric | Value | Description |
|---|---|---|
| Equipment types | 4 major categories | Transformer/Line/Switch/Load |
| Preset models | 52 | Covers common manufacturers |
| Equipment model parameters | 32 | Per device average |
| Transformer tap positions | ±8 | Standard configuration |
| ZIP load characteristic coefficients | 3 | Z/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 Type | Voltage Dependency | Power-Voltage Relationship | Applicable Equipment |
|---|---|---|---|
| Constant impedance Z | V² | P ∝ V² | Heaters, incandescent lamps |
| Constant current I | V¹ | P ∝ V | General industrial loads |
| Constant power P | V⁰ | P = const | Motor drives (variable frequency) |
| Typical mix | - | 40%Z+30%I+30%P | Composite 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:Branchsupports associating detailed equipment modelseneros-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_parametersnot dividing by circuit count for parallel multi-circuit (#107) - Fixed
Switch::opennot returning an error when already open (#110)
Breaking Changes
eneros_topology::Branch:paramsfield changed toOption<PiParameters>, calculated by equipment model at runtime
Performance Improvements
| Operation | Time |
|---|---|
| Transformer PI parameter calculation | 240 ns |
| Line PI parameter calculation | 180 ns |
| ZIP load power calculation | 95 ns |
| Switch state transition | 32 ns |
| Model library loading (52 models) | 1.4 ms |
Contributors
| Contributor | Role | Commits |
|---|---|---|
| @eneros-foundation | Architect | 28 |
| @equipment-expert | Equipment Modeling Expert | 52 |
| @transformer-guru | Transformer Expert | 31 |
| @grid-rustacean | Rust Engineer | 22 |
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);