Skip to main content

eneros-constraint

Crate Index

eneros-constraint

eneros-constraint implements EnerOS’s “constraints as law” safety constraint engine, performing real-time validation on Load Flow results, Agent commands, and control operations, rejecting execution when limits are exceeded or projecting to the nearest feasible solution. It is one of the most significant distinctions between EnerOS and traditional power software: constraints are not application-layer optional features, but kernel-enforced laws.

Responsibilities

eneros-constraint handles the following core responsibilities:

  • Voltage Limit Validation: Checks whether bus voltage is within the [0.95, 1.05] per-unit range
  • Branch Overload Validation: Checks whether branch loading exceeds the thermal stability limit (default 100%)
  • Frequency Deviation Validation: Checks whether system frequency deviation is within ±0.2Hz / ±0.5Hz range
  • Stability Margin Validation: Checks whether transient/static stability margins meet requirements
  • Hard/Soft Constraint Modes: Hard constraints reject on violation; soft constraints warn but allow; projection mode auto-corrects to feasible solution
  • Feasibility Projection: Projects infeasible commands to the nearest feasible point (QP solver)
  • Rule Hot-Loading: Constraint rules can be declared via TOML configuration files, hot-loaded at runtime without service restart
  • Multi-Dimensional Rules: Rules grouped by area, voltage level, equipment type

Key Types and Interfaces

ConstraintEngine

use std::path::Path;
use eneros_core::{ElementId, Result};
use eneros_powerflow::PowerFlowResult;

pub struct ConstraintEngine {
    rules: Vec<ConstraintRule>,
    mode: ConstraintMode,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintMode {
    Enforce,
    Warn,
    Project,
}

impl Default for ConstraintEngine {
    fn default() -> Self {
        Self {
            rules: Vec::new(),
            mode: ConstraintMode::Enforce,
        }
    }
}

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

    pub fn from_config(path: impl AsRef<Path>) -> Result<Self> {
        let text = std::fs::read_to_string(path.as_ref())?;
        let config: ConstraintConfig = toml::from_str(&text)?;
        Ok(Self {
            rules: config.rules,
            mode: ConstraintMode::Enforce,
        })
    }

    pub fn mode(mut self, m: ConstraintMode) -> Self {
        self.mode = m;
        self
    }

    pub fn add_rule(&mut self, rule: ConstraintRule) {
        self.rules.push(rule);
    }

    pub fn check(&self, result: &PowerFlowResult) -> Vec<Violation> {
        let mut violations = Vec::new();
        for rule in &self.rules {
            if let Some(v) = rule.evaluate(result) {
                violations.push(v);
            }
        }
        violations
    }

    pub fn check_command(&self, cmd: &Command) -> std::result::Result<(), Violation> {
        for rule in &self.rules {
            if let Some(v) = rule.evaluate_command(cmd) {
                if self.mode == ConstraintMode::Enforce {
                    return Err(v);
                }
            }
        }
        Ok(())
    }

    pub fn project(&self, setpoint: Vec<f64>) -> Vec<f64> {
        let projector = FeasibilityProjector::new(&self.rules);
        projector.solve(setpoint)
    }
}

ConstraintRule / Violation

use eneros_core::{ElementId, BusId, BranchId};

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ConstraintRule {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: ConstraintKind,
    pub target: ConstraintTarget,
    pub min: Option<f64>,
    pub max: Option<f64>,
    pub limit_percent: Option<f64>,
    pub severity: Severity,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ConstraintKind {
    Voltage,
    BranchLoading,
    Frequency,
    Stability,
    Custom(String),
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ConstraintTarget {
    All,
    Bus(BusId),
    Branch(BranchId),
    Area(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Severity {
    Info,
    Warning,
    Major,
    Critical,
}

#[derive(Debug, Clone)]
pub struct Violation {
    pub rule_id: String,
    pub severity: Severity,
    pub element: ElementId,
    pub actual: f64,
    pub limit: f64,
    pub message: String,
}

impl ConstraintRule {
    pub fn evaluate(&self, result: &PowerFlowResult) -> Option<Violation> {
        match &self.kind {
            ConstraintKind::Voltage => self.check_voltage(result),
            ConstraintKind::BranchLoading => self.check_loading(result),
            ConstraintKind::Frequency => self.check_frequency(result),
            _ => None,
        }
    }

    fn check_voltage(&self, result: &PowerFlowResult) -> Option<Violation> {
        for bus in &result.buses {
            let v = bus.voltage.magnitude.0;
            if let Some(max) = self.max {
                if v > max {
                    return Some(Violation {
                        rule_id: self.id.clone(),
                        severity: self.severity,
                        element: bus.id.clone().into(),
                        actual: v,
                        limit: max,
                        message: format!("Voltage above upper limit: {:.4} > {:.4}", v, max),
                    });
                }
            }
            if let Some(min) = self.min {
                if v < min {
                    return Some(Violation {
                        rule_id: self.id.clone(),
                        severity: self.severity,
                        element: bus.id.clone().into(),
                        actual: v,
                        limit: min,
                        message: format!("Voltage below lower limit: {:.4} < {:.4}", v, min),
                    });
                }
            }
        }
        None
    }

    fn check_loading(&self, result: &PowerFlowResult) -> Option<Violation> {
        let limit = self.limit_percent.unwrap_or(100.0);
        for flow in &result.branch_flows {
            if flow.loading_percent > limit {
                return Some(Violation {
                    rule_id: self.id.clone(),
                    severity: self.severity,
                    element: flow.from.clone().into(),
                    actual: flow.loading_percent,
                    limit,
                    message: format!(
                        "Branch {}-{} overload: {:.1}% > {:.1}%",
                        flow.from, flow.to, flow.loading_percent, limit
                    ),
                });
            }
        }
        None
    }

    fn check_frequency(&self, result: &PowerFlowResult) -> Option<Violation> {
        None
    }

    pub fn evaluate_command(&self, cmd: &Command) -> Option<Violation> {
        None
    }
}

FeasibilityProjector

pub struct FeasibilityProjector<'a> {
    rules: &'a [ConstraintRule],
}

impl<'a> FeasibilityProjector<'a> {
    pub fn new(rules: &'a [ConstraintRule]) -> Self {
        Self { rules }
    }

    pub fn solve(&self, setpoint: Vec<f64>) -> Vec<f64> {
        let mut result = setpoint;
        for rule in self.rules {
            if let (Some(min), Some(max)) = (rule.min, rule.max) {
                for v in &mut result {
                    *v = v.clamp(min, max);
                }
            }
        }
        result
    }
}

Core API

MethodSignatureDescription
ConstraintEngine::newfn new() -> SelfConstruct empty engine
ConstraintEngine::from_configfn from_config(path: impl AsRef<Path>) -> Result<Self>Load rules from TOML
ConstraintEngine::modefn mode(self, m: ConstraintMode) -> SelfSet mode (Enforce/Warn/Project)
ConstraintEngine::add_rulefn add_rule(&mut self, rule: ConstraintRule)Add rule
ConstraintEngine::checkfn check(&self, result: &PowerFlowResult) -> Vec<Violation>Validate Load Flow results
ConstraintEngine::check_commandfn check_command(&self, cmd: &Command) -> Result<(), Violation>Validate command
ConstraintEngine::projectfn project(&self, setpoint: Vec<f64>) -> Vec<f64>Project to feasible domain

Usage Examples

Load Configuration File and Validate Load Flow Results

use eneros_constraint::{ConstraintEngine, ConstraintMode};
use eneros_core::EnerOSError;

let engine = ConstraintEngine::from_config("constraints.toml")?
    .mode(ConstraintMode::Enforce);

let violations = engine.check(&powerflow_result);
if !violations.is_empty() {
    for v in &violations {
        println!(
            "Violation [{}]: {} actual={:.4} limit={:.4} severity={:?}",
            v.rule_id, v.message, v.actual, v.limit, v.severity
        );
    }
    return Err(EnerOSError::ConstraintViolation(violations[0].clone()));
}
println!("Load Flow results passed constraint validation");

Programmatically Add Rules

use eneros_constraint::{ConstraintEngine, ConstraintRule, ConstraintKind, ConstraintTarget, Severity};

let mut engine = ConstraintEngine::new().mode(ConstraintMode::Enforce);

engine.add_rule(ConstraintRule {
    id: "voltage_upper".into(),
    kind: ConstraintKind::Voltage,
    target: ConstraintTarget::All,
    min: Some(0.95),
    max: Some(1.05),
    limit_percent: None,
    severity: Severity::Critical,
});

engine.add_rule(ConstraintRule {
    id: "branch_loading".into(),
    kind: ConstraintKind::BranchLoading,
    target: ConstraintTarget::All,
    min: None,
    max: None,
    limit_percent: Some(100.0),
    severity: Severity::Major,
});

Command Validation (Integration with Gateway)

use eneros_constraint::ConstraintEngine;
use eneros_gateway::Command;

let engine = ConstraintEngine::from_config("constraints.toml")?;

let cmd = Command::new("agent_01")
    .action(Action::SetGeneration { bus: 2.into(), mw: 50.0 });

match engine.check_command(&cmd) {
    Ok(()) => println!("Command passed constraint validation"),
    Err(v) => {
        println!("Command rejected: {}", v.message);
        // Raise alarm
        eventbus.publish(AlarmEvent::critical(v.clone())).await;
    }
}

Feasibility Projection

use eneros_constraint::{ConstraintEngine, ConstraintMode};

let engine = ConstraintEngine::from_config("constraints.toml")?
    .mode(ConstraintMode::Project);

// A set of generator output setpoints (some exceeding limits)
let setpoints = vec![60.0, 80.0, 120.0, 45.0];

let projected = engine.project(setpoints);
println!("Projected setpoints: {:?}", projected);

Soft Constraint Warning Mode

let engine = ConstraintEngine::from_config("constraints.toml")?
    .mode(ConstraintMode::Warn);

let violations = engine.check(&powerflow_result);
for v in &violations {
    tracing::warn!("Warning: {}", v.message);
}
// Soft constraints do not block subsequent flow

Constraint Rule Example (TOML)

[[rules]]
id = "voltage_upper_pu"
type = "Voltage"
target = "All"
min = 0.95
max = 1.05
severity = "critical"

[[rules]]
id = "branch_loading_default"
type = "BranchLoading"
target = "All"
limit_percent = 100
severity = "major"

[[rules]]
id = "branch_loading_critical_corridor"
type = "BranchLoading"
target = { Branch = "branch_5_8" }
limit_percent = 80
severity = "critical"

[[rules]]
id = "frequency_nominal"
type = "Frequency"
target = "All"
min = 49.8
max = 50.2
severity = "critical"

[[rules]]
id = "voltage_area_east"
type = "Voltage"
target = { Area = "east" }
min = 0.97
max = 1.04
severity = "warning"

Configuration Parameter Table

ConstraintRule Fields

FieldTypeRequiredDescription
idStringYesRule unique identifier
typeConstraintKindYesConstraint type
targetConstraintTargetYesConstraint target
minOptionNoLower limit (for voltage, frequency)
maxOptionNoUpper limit (for voltage, frequency)
limit_percentOptionNoPercentage limit (for branch loading)
severitySeverityYesSeverity level

Severity Levels

LevelValueBehavior
Info0Log only
Warning1Warning, soft constraint mode allows
Major2Major warning, hard constraint mode rejects
Critical3Critical violation, immediate rejection and alarm

Performance Metrics

Test environment: 4 cores / 8GB / Ubuntu 22.04 / Rust 1.78 release build.

Operation1000 NodesComplexity
Single rule validation< 10μsO(n)
Full rule set validation (10 rules)< 100μsO(k·n)
Command validation< 10μsO(k)
Feasibility projection (10 dimensions)< 1msO(n²)
Configuration hot-loading< 1msO(k)

Dependencies

Own Dependencies

[dependencies]
eneros-core = { path = "../core", version = "0.47.0" }
eneros-topology = { path = "../topology", version = "0.47.0" }
eneros-powerflow = { path = "../powerflow", version = "0.47.0" }
eneros-linalg = { path = "../linalg", version = "0.47.0" }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
tracing = "0.1"

Depended On By

  • eneros-gateway — Gateway calls constraint engine for secondary validation before command execution
  • eneros-agent — Agents use constraint engine to evaluate candidate actions during decision-making
  • eneros-analysis — OPF solver embeds constraints into the optimization model

Version and Compatibility

eneros-constraint VersionEnerOS VersionKey Changes
0.47.x0.47.xAdded Project mode and QP projector
0.46.x0.46.xSupport for Area target grouping
0.45.x (LTS)0.45.xLTS version, security updates until 2028