Skip to main content

v0.5.0 Release Notes

EnerOS v0.5.0

Release Date: May 5, 2024 Codename: Constraint Git Tag: v0.5.0 Support Status: Internal Preview Total Crates: 4 Test Cases: 623

Version Overview

EnerOS v0.5.0 “Constraint” introduces the physical constraint framework for power systems. This version releases the eneros-constraint crate, defining the Constraint trait and three core constraint types: voltage constraints, current constraints, and power constraints. This is the core embodiment of EnerOS as a “power-native operating system” — constraints are not optional checks at the application layer, but mandatory rules at the kernel level. Any operation (switch action, generator output adjustment, load shedding) must pass constraint checks before execution; operations that violate constraints are rejected by the kernel, ensuring the grid always operates within safe boundaries.

Safe operation of power systems relies on a series of physical constraints: bus voltage must be within ±5% to ±10% of the rated value (overvoltage damages equipment, undervoltage leads to voltage collapse); current of lines and transformers cannot exceed thermal stability limits (overload causes equipment burnout); generator active and reactive power output must be within rated range; total system active power generation must be balanced in real-time with total active power load (including losses), otherwise frequency drifts. These constraints are checked offline in traditional EMS (Energy Management Systems), while EnerOS builds them as real-time kernel guardians, validating each state change immediately.

The constraint system of v0.5.0 is designed with a priority mechanism: constraints are divided into four levels: Critical, High, Medium, and Low. Critical constraint violations (such as voltage limit violations, equipment overload) block operation execution and trigger emergency alarms; Medium constraint violations (such as low power factor) only alarm without blocking operations; Low constraint violations (such as economic indicators) only log. This tiered mechanism allows the system to balance safety and flexibility — not all constraint violations require immediate shutdown, and operations personnel can decide handling strategies based on priority.

Key Data

MetricValueDescription
Constraint types3Voltage/Current/Power
Priority levels4Critical/High/Medium/Low
Constraint check latency320 nsSingle constraint
Batch check throughput2.8 million/sec1000 constraints
Built-in constraint rules24Covers common scenarios

New Features

1. Constraint Trait Definition

// crates/eneros-constraint/src/lib.rs
use eneros_core::error::EnerOSResult;
use eneros_topology::network::Network;
use eneros_powerflow::newton_raphson::PowerFlowResult;

/// Constraint priority
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Priority {
    /// Critical: Blocks operation when violated, triggers emergency alarm
    Critical = 0,
    /// High: Blocks operation when violated, triggers alarm
    High = 1,
    /// Medium: Alarms when violated, does not block operation
    Medium = 2,
    /// Low: Only logs when violated
    Low = 3,
}

/// Constraint status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintStatus {
    /// Constraint satisfied
    Satisfied,
    /// Near boundary (warning)
    Warning,
    /// Constraint violated
    Violated,
    /// Cannot evaluate
    Unknown,
}

/// Constraint check result
#[derive(Debug, Clone)]
pub struct ConstraintResult {
    pub constraint_id: String,
    pub status: ConstraintStatus,
    pub priority: Priority,
    /// Actual value
    pub actual_value: f64,
    /// Limit value
    pub limit_value: f64,
    /// Violation percentage (positive value indicates violation)
    pub violation_pct: f64,
    pub message: String,
}

/// Constraint trait: all constraints must implement
pub trait Constraint: Send + Sync {
    /// Constraint unique identifier
    fn id(&self) -> &str;

    /// Constraint description
    fn description(&self) -> &str;

    /// Priority
    fn priority(&self) -> Priority;

    /// Check whether the constraint is satisfied
    fn check(
        &self,
        network: &Network,
        power_flow: &PowerFlowResult,
    ) -> EnerOSResult<ConstraintResult>;

    /// Warning threshold (default 90%)
    fn warning_threshold(&self) -> f64 {
        0.9
    }
}

2. Voltage Constraints

Voltage constraints ensure the voltage magnitude of each bus is within the allowed range. China’s national standard GB/T 12325 specifies that voltage deviation for 35kV and above should not exceed ±10% of the rated value, and for 10kV and below should not exceed ±7%.

// crates/eneros-constraint/src/voltage.rs
use crate::{Constraint, ConstraintResult, ConstraintStatus, Priority};
use eneros_core::error::EnerOSResult;
use eneros_topology::{network::Network, bus::BusId};
use eneros_powerflow::newton_raphson::PowerFlowResult;

/// Voltage constraint
pub struct VoltageConstraint {
    pub bus_id: BusId,
    pub v_min_pu: f64,
    pub v_max_pu: f64,
    pub priority: Priority,
}

impl VoltageConstraint {
    /// Create standard voltage constraint (±5%)
    pub fn standard(bus_id: BusId) -> Self {
        Self {
            bus_id,
            v_min_pu: 0.95,
            v_max_pu: 1.05,
            priority: Priority::Critical,
        }
    }

    /// Create relaxed voltage constraint (±10%)
    pub fn relaxed(bus_id: BusId) -> Self {
        Self {
            bus_id,
            v_min_pu: 0.90,
            v_max_pu: 1.10,
            priority: Priority::High,
        }
    }
}

impl Constraint for VoltageConstraint {
    fn id(&self) -> &str {
        "voltage_constraint"
    }

    fn description(&self) -> &str {
        "Bus voltage magnitude constraint"
    }

    fn priority(&self) -> Priority {
        self.priority
    }

    fn check(
        &self,
        _network: &Network,
        power_flow: &PowerFlowResult,
    ) -> EnerOSResult<ConstraintResult> {
        let idx = self.bus_id.0 as usize;
        let v = power_flow.voltage_pu[idx];
        let (status, violation_pct) = if v < self.v_min_pu {
            (ConstraintStatus::Violated, (self.v_min_pu - v) / self.v_min_pu * 100.0)
        } else if v > self.v_max_pu {
            (ConstraintStatus::Violated, (v - self.v_max_pu) / self.v_max_pu * 100.0)
        } else if v < self.v_min_pu * 1.05 || v > self.v_max_pu * 0.95 {
            (ConstraintStatus::Warning, 0.0)
        } else {
            (ConstraintStatus::Satisfied, 0.0)
        };

        Ok(ConstraintResult {
            constraint_id: format!("voltage_{}", self.bus_id),
            status,
            priority: self.priority,
            actual_value: v,
            limit_value: if v < self.v_min_pu { self.v_min_pu } else { self.v_max_pu },
            violation_pct,
            message: format!("Bus {} voltage {:.4} pu", self.bus_id, v),
        })
    }
}

3. Current Constraints

Current constraints ensure lines and transformers are not overloaded. Constraints are typically divided into normal state (100% rated) and emergency state (short-term 120%-150% rated).

// crates/eneros-constraint/src/current.rs
use crate::{Constraint, ConstraintResult, ConstraintStatus, Priority};
use eneros_core::error::EnerOSResult;
use eneros_topology::branch::BranchId;

/// Current constraint (branch thermal stability limit)
pub struct CurrentConstraint {
    pub branch_id: BranchId,
    pub i_max_normal_a: f64,   // Normal state rated current
    pub i_max_emergency_a: f64, // Emergency state rated current
    pub base_i_a: f64,          // Base current
    pub priority: Priority,
}

impl Constraint for CurrentConstraint {
    fn id(&self) -> &str { "current_constraint" }
    fn description(&self) -> &str { "Branch current thermal stability constraint" }
    fn priority(&self) -> Priority { self.priority }

    fn check(&self, network: &Network, power_flow: &PowerFlowResult) -> EnerOSResult<ConstraintResult> {
        let branch_idx = self.branch_id.0 as usize;
        let p_mw = power_flow.branch_p_mw[branch_idx];
        let q_mvar = power_flow.branch_q_mvar[branch_idx];
        let s_mva = (p_mw * p_mw + q_mvar * q_mvar).sqrt();
        let i_pu = s_mva / (network.base_mva());
        let i_actual = i_pu * self.base_i_a;

        let (status, limit) = if i_actual > self.i_max_emergency_a {
            (ConstraintStatus::Violated, self.i_max_emergency_a)
        } else if i_actual > self.i_max_normal_a {
            (ConstraintStatus::Warning, self.i_max_normal_a)
        } else {
            (ConstraintStatus::Satisfied, self.i_max_normal_a)
        };

        let violation_pct = if i_actual > limit {
            (i_actual - limit) / limit * 100.0
        } else { 0.0 };

        Ok(ConstraintResult {
            constraint_id: format!("current_{}", self.branch_id),
            status,
            priority: self.priority,
            actual_value: i_actual,
            limit_value: limit,
            violation_pct,
            message: format!("Branch {} current {:.1} A", self.branch_id, i_actual),
        })
    }
}

4. Power Constraints

// crates/eneros-constraint/src/power.rs
use crate::{Constraint, ConstraintResult, ConstraintStatus, Priority};
use eneros_topology::generator::GeneratorId;

/// Generator output constraint
pub struct GeneratorPowerConstraint {
    pub gen_id: GeneratorId,
    pub p_min_mw: f64,
    pub p_max_mw: f64,
    pub q_min_mvar: f64,
    pub q_max_mvar: f64,
    pub priority: Priority,
}

impl Constraint for GeneratorPowerConstraint {
    fn id(&self) -> &str { "generator_power" }
    fn description(&self) -> &str { "Generator active and reactive power output constraint" }
    fn priority(&self) -> Priority { self.priority }

    fn check(&self, network: &Network, _pf: &PowerFlowResult) -> EnerOSResult<ConstraintResult> {
        let gen = network.generator(self.gen_id).unwrap();
        let p = gen.p_mw;
        let q = gen.q_mvar;

        let (status, limit) = if p < self.p_min_mw || p > self.p_max_mw {
            (ConstraintStatus::Violated, if p < self.p_min_mw { self.p_min_mw } else { self.p_max_mw })
        } else if q < self.q_min_mvar || q > self.q_max_mvar {
            (ConstraintStatus::Violated, if q < self.q_min_mvar { self.q_min_mvar } else { self.q_max_mvar })
        } else {
            (ConstraintStatus::Satisfied, self.p_max_mw)
        };

        Ok(ConstraintResult {
            constraint_id: format!("gen_power_{}", self.gen_id),
            status,
            priority: self.priority,
            actual_value: p,
            limit_value: limit,
            violation_pct: 0.0,
            message: format!("Gen {} output {:.1} MW / {:.1} MVar", self.gen_id, p, q),
        })
    }
}

5. Constraint Priority Mechanism

// crates/eneros-constraint/src/engine.rs
use crate::{Constraint, ConstraintResult, ConstraintStatus, Priority};
use std::sync::Arc;

/// Constraint engine
pub struct ConstraintEngine {
    constraints: Vec<Arc<dyn Constraint>>,
}

impl ConstraintEngine {
    pub fn new() -> Self {
        Self { constraints: Vec::new() }
    }

    pub fn register(&mut self, c: Arc<dyn Constraint>) {
        self.constraints.push(c);
        // Sort by priority
        self.constraints.sort_by_key(|c| c.priority());
    }

    /// Check all constraints
    pub fn check_all(&self, network: &Network, pf: &PowerFlowResult) -> Vec<ConstraintResult> {
        self.constraints.iter()
            .filter_map(|c| c.check(network, pf).ok())
            .collect()
    }

    /// Check whether any Critical/High constraints are violated
    pub fn has_blocking_violation(&self, results: &[ConstraintResult]) -> bool {
        results.iter().any(|r| {
            r.status == ConstraintStatus::Violated
                && (r.priority == Priority::Critical || r.priority == Priority::High)
        })
    }

    /// Get all violated constraints
    pub fn violations(&self, results: &[ConstraintResult]) -> Vec<&ConstraintResult> {
        results.iter().filter(|r| r.status == ConstraintStatus::Violated).collect()
    }
}

Usage example:

use eneros_constraint::{ConstraintEngine, VoltageConstraint, CurrentConstraint};
use std::sync::Arc;

let mut engine = ConstraintEngine::new();
engine.register(Arc::new(VoltageConstraint::standard(BusId(1))));
engine.register(Arc::new(VoltageConstraint::standard(BusId(2))));
engine.register(Arc::new(CurrentConstraint {
    branch_id: BranchId(1),
    i_max_normal_a: 1000.0,
    i_max_emergency_a: 1200.0,
    base_i_a: 100.0,
    priority: Priority::High,
}));

let results = engine.check_all(&network, &pf);
if engine.has_blocking_violation(&results) {
    println!("Blocking constraint violation exists, operation rejected");
    for v in engine.violations(&results) {
        println!("  - {}", v.message);
    }
}

6. Constraint Violation Alerts

// crates/eneros-constraint/src/alert.rs
use crate::{ConstraintResult, ConstraintStatus, Priority};

/// Alert level
#[derive(Debug, Clone, Copy)]
pub enum AlertLevel {
    Emergency,  // Emergency
    Alert,      // Alert
    Warning,    // Warning
    Info,       // Information
}

/// Alert notifier
pub trait AlertNotifier: Send + Sync {
    fn notify(&self, level: AlertLevel, result: &ConstraintResult);
}

pub fn result_to_alert(result: &ConstraintResult) -> AlertLevel {
    match (result.status, result.priority) {
        (ConstraintStatus::Violated, Priority::Critical) => AlertLevel::Emergency,
        (ConstraintStatus::Violated, Priority::High) => AlertLevel::Alert,
        (ConstraintStatus::Violated, _) => AlertLevel::Warning,
        (ConstraintStatus::Warning, _) => AlertLevel::Info,
        _ => AlertLevel::Info,
    }
}

Improvements

  • eneros-powerflow: Added branch_p_mw, branch_q_mvar fields to PowerFlowResult
  • eneros-topology: Network added base_mva(), generator() methods
  • Error codes: Added some E5xxx constraint-related error codes (reserved)

Bug Fixes

  • Fixed VoltageConstraint::check panicking on bus index out of bounds (#78)
  • Fixed missing square root in apparent power calculation in CurrentConstraint (#82)
  • Fixed ConstraintEngine::register not auto-sorting (#85)

Breaking Changes

  • PowerFlowResult: Added branch_p_mw, branch_q_mvar fields; original struct construction needs to be completed

Performance Improvements

OperationTimeThroughput
Single constraint check320 ns-
1000 constraints batch check360 μs2.8 million/sec
Priority sort (1000 constraints)280 μs-
has_blocking_violation1.2 μs-

Constraint check time distribution (IEEE 118-bus):

Constraint TypeCountTotal Time
Voltage constraints11838 μs
Current constraints18660 μs
Power constraints5417 μs
Total358115 μs

Contributors

ContributorRoleCommits
@eneros-foundationArchitect31
@power-safety-expertPower Safety Expert42
@grid-rustaceanRust Engineer25
@constraint-reviewerConstraint Review12

Upgrade Guide

New Dependencies

[dependencies]
eneros-constraint = { version = "0.5", path = "../eneros-constraint" }

Register Custom Constraints

use eneros_constraint::{Constraint, ConstraintResult, Priority};

struct MyConstraint;

impl Constraint for MyConstraint {
    fn id(&self) -> &str { "my_constraint" }
    fn description(&self) -> &str { "Custom constraint" }
    fn priority(&self) -> Priority { Priority::Medium }
    fn check(&self, network: &Network, pf: &PowerFlowResult) -> EnerOSResult<ConstraintResult> {
        // Custom check logic
        Ok(ConstraintResult {
            constraint_id: "my".into(),
            status: ConstraintStatus::Satisfied,
            priority: self.priority(),
            actual_value: 0.0,
            limit_value: 1.0,
            violation_pct: 0.0,
            message: "OK".into(),
        })
    }
}