Skip to main content

Constraint as Law

Core Concepts

Constraint as Law

Constraint as Kernel Law is the safety cornerstone of EnerOS: physical constraints of the power system (voltage limit violations, branch overloads, frequency deviations, stability limits) are not optional validation steps at the application layer, but laws enforced by the operating system kernel. Any Agent decision that violates physical constraints is directly rejected in kernel mode, and when necessary, automatically projected to the nearest feasible solution, eliminating “infeasible decisions” at the architectural level.

Design Motivation

Defects of Traditional Constraint Validation

Traditional power software places constraint validation at the application layer, which poses serious security risks:

DefectDescriptionIncident Case
Validation omissionSome applications do not implement constraint validationEquipment damage from voltage overlimits
Sequence errorsValidation occurs after executionMisoperation already dispatched
Consistency issuesMultiple applications have conflicting constraintsDifferent applications overwrite each other
Performance overheadApplication-layer redundant computationIncreased decision latency
Non-auditableValidation logs are scatteredIncidents are hard to trace
No projectionRejection is the endDecisions cannot be executed

Constraint as Law Solution

EnerOS sinks constraint validation into the kernel, where all write operations must pass through the constraint engine:

Agent decision command


┌─────────────┐
│ Safety Gateway│ ← Real-time execution domain interception
└─────┬───────┘


┌─────────────┐     pass      ┌──────────┐
│ Constraint  │ ──────────→  │ Execute   │
│ Engine      │              │ Command   │
│ Validation  │              └──────────┘
└─────┬───────┘
      │ violated

┌─────────────┐     projectable ┌──────────┐
│ Projector   │ ──────────→  │ Projected │
│             │              │ Command   │
└─────┬───────┘              └──────────┘
      │ not projectable

┌─────────────┐
│ Reject +    │
│ Alert       │
└─────────────┘

Core Constraint Types

1. Voltage Constraints

Voltage must be within safe limits, otherwise equipment may be damaged or protection may misoperate:

use eneros_constraint::{Constraint, VoltageLimit};

// Network-wide voltage constraint
let voltage_constraint = Constraint::voltage()
    .name("network_voltage_safety")
    .min(0.95)                    // Minimum voltage 0.95 pu
    .max(1.05)                    // Maximum voltage 1.05 pu
    .buses(network.all_buses())   // Apply to all buses
    .severity(Severity::Critical) // Severity level
    .action(Action::ProjectAndWarn);

// Tight constraint (special buses are more strict)
let tight_voltage = Constraint::voltage()
    .name("critical_bus_voltage")
    .min(0.97)
    .max(1.03)
    .buses(vec![BusId(1), BusId(5)])
    .severity(Severity::Critical)
    .action(Action::Reject);

constraint_engine.register(voltage_constraint)?;
constraint_engine.register(tight_voltage)?;

2. Current/Thermal Limit Constraints

The current-carrying capacity of lines and transformers must not exceed thermal stability limits, otherwise they will overheat and be damaged:

use eneros_constraint::{Constraint, ThermalLimit};

// Single line thermal limit
let thermal_constraint = Constraint::thermal()
    .name("line_L-1_thermal_limit")
    .branch(BranchId::from("L-1"))
    .limit_mva(100.0)             // Long-term allowable 100 MVA
    .emergency_limit_mva(120.0)   // Emergency allowable 120 MVA (15 minutes)
    .emergency_duration(Duration::minutes(15))
    .severity(Severity::High)
    .action(Action::ProjectAndWarn);

// Transformer overload constraint
let transformer_constraint = Constraint::thermal()
    .name("transformer_T-1_overload")
    .branch(BranchId::from("T-1"))
    .limit_mva(63.0)
    .emergency_limit_mva(75.0)
    .emergency_duration(Duration::minutes(30))
    .severity(Severity::High);

constraint_engine.register(thermal_constraint)?;
constraint_engine.register(transformer_constraint)?;

3. Frequency Constraints

System frequency must be kept near the rated value, otherwise low-frequency load shedding or generator tripping will be triggered:

let frequency_constraint = Constraint::frequency()
    .name("system_frequency_safety")
    .min_hz(49.5)                 // Normal minimum 49.5 Hz
    .max_hz(50.5)                 // Normal maximum 50.5 Hz
    .emergency_min_hz(49.0)       // Emergency minimum 49.0 Hz
    .emergency_max_hz(51.0)       // Emergency maximum 51.0 Hz
    .rocof_hz_per_s(2.0)          // Rate of change of frequency limit 2 Hz/s
    .severity(Severity::Critical)
    .action(Action::Reject);

constraint_engine.register(frequency_constraint)?;

4. Stability Constraints

Includes transient stability, static stability, voltage stability, and other dynamic constraints:

use eneros_constraint::{StabilityConstraint, StabilityType};

// Transient stability constraint
let transient = Constraint::stability()
    .name("transient_stability")
    .stability_type(StabilityType::Transient)
    .fault_clearing_time(Duration::milliseconds(100))
    .critical_clearing_time(Duration::milliseconds(150))
    .damping_ratio(0.05)          // Damping ratio ≥ 5%
    .severity(Severity::Critical)
    .action(Action::Reject);

// Voltage stability constraint
let voltage_stability = Constraint::stability()
    .name("voltage_stability_margin")
    .stability_type(StabilityType::Voltage)
    .min_margin_percent(5.0)      // Minimum stability margin 5%
    .severity(Severity::High)
    .action(Action::ProjectAndWarn);

constraint_engine.register(transient)?;
constraint_engine.register(voltage_stability)?;

Constraint Configuration Table

Complete Constraint Types Overview

Constraint TypeApplicable ObjectKey ParametersDefault ActionSeverity
Voltage constraintBusmin, max (pu)ProjectCritical
Current constraintBranchlimit_a (A)ProjectHigh
Thermal limit constraintBranchlimit_mva, emergency_limit_mvaProjectHigh
Frequency constraintSystemmin_hz, max_hz, rocofRejectCritical
Active power constraintGeneratormin_mw, max_mw, ramp_rateProjectMedium
Reactive power constraintGenerator/SVCmin_mvar, max_mvarProjectMedium
Phase angle difference constraintBoth ends of branchmax_angle_diff (degrees)ProjectHigh
Short-circuit capacity constraintBusmax_scc_mvaRejectCritical
Stability constraintSystem/Regiondamping ratio, marginRejectCritical
Reserve constraintSystemmin_reserve_mwRejectHigh
N-1 constraintAny branchMeet above constraints after any single component outageWarnHigh

Severity Levels and Processing Actions

SeverityDefault ActionDescription
CriticalRejectDirectly reject, record alert
HighProjectAndWarnProject to feasible solution, record alert
MediumProjectProject to feasible solution
LowWarnOnly record alert, allow execution

Constraint Engine Working Principle

Engine Architecture

┌──────────────────────────────────────────┐
│            Constraint Engine              │
├──────────────────────────────────────────┤
│  ┌─────────────┐  ┌──────────────────┐  │
│  │ Constraint   │  │ Constraint Index │  │
│  │ Registry     │  │ (by object)      │  │
│  └─────────────┘  └──────────────────┘  │
├──────────────────────────────────────────┤
│  ┌─────────────┐  ┌──────────────────┐  │
│  │ Validator    │  │ Projector        │  │
│  │              │  │                  │  │
│  └─────────────┘  └──────────────────┘  │
├──────────────────────────────────────────┤
│  ┌─────────────┐  ┌──────────────────┐  │
│  │ Audit Log    │  │ Alert Channel    │  │
│  └─────────────┘  └──────────────────┘  │
└──────────────────────────────────────────┘

Constraint Validation Flow

use eneros_constraint::{ConstraintEngine, CheckResult, Violation};

let engine = ConstraintEngine::new(&network);

// 1. Load constraints (can also be configured via eneros.toml)
engine.load_constraints("config/constraints.toml")?;

// 2. Validate command
let command = Command::SetGeneration { bus: 1, mw: 150.0 };
let result = engine.check(&command);

match result {
    CheckResult::Ok => {
        // Pass: command satisfies all constraints
        engine.execute(command).await?;
    }
    CheckResult::Violated(violations) => {
        // Violated: subsequent action depends on constraint's action
        for v in &violations {
            log::warn!("Constraint {} violated: current={:?}, limit={:?}",
                v.constraint_name, v.actual, v.limit);
        }

        match violations.first().action {
            Action::Reject => {
                return Err(EnerOSError::ConstraintRejected(violations));
            }
            Action::Project => {
                let feasible = engine.project(&command)?;
                engine.execute(feasible).await?;
            }
            Action::ProjectAndWarn => {
                let feasible = engine.project(&command)?;
                engine.alert(AlertLevel::Warn, &violations).await?;
                engine.execute(feasible).await?;
            }
            Action::Warn => {
                engine.alert(AlertLevel::Info, &violations).await?;
                engine.execute(command).await?;
            }
        }
    }
}

Constraint Projection Algorithm

Projection Principle

When a command violates constraints, EnerOS does not simply reject it, but calculates the nearest feasible command that satisfies all constraints. The projection problem is formalized as:

minimize   || x - x_orig ||_2
subject to g_i(x) ≤ 0,  i = 1, ..., m
           h_j(x) = 0,  j = 1, ..., n

Where x_orig is the original command, x is the projected feasible command, g_i are inequality constraints, and h_j are equality constraints.

Projector Implementation

use eneros_constraint::{FeasibilityProjector, ProjectionMethod};

let projector = FeasibilityProjector::new(&constraints)
    .method(ProjectionMethod::QuadraticProgramming) // QP solver
    .max_iter(100)
    .tolerance(1e-6)
    .timeout(Duration::milliseconds(10));

// Original command: generate 150 MW (violates max 100 MW constraint)
let original = Command::SetGeneration { bus: 1, mw: 150.0 };

// Project to feasible solution
let projected = projector.project(&original)?;

println!("Original: {:?}", original);
println!("Projected: {:?}", projected);
// Output:
// Original: SetGeneration { bus: 1, mw: 150.0 }
// Projected: SetGeneration { bus: 1, mw: 100.0 }

Multi-Constraint Joint Projection

In real scenarios, multiple constraints often need to be satisfied simultaneously. EnerOS uses a QP solver for joint projection:

// Consider simultaneously: voltage, thermal limit, reserve constraints
let command = Command::MultiDispatch {
    dispatches: vec![
        Dispatch { bus: 1, mw: 150.0 },
        Dispatch { bus: 2, mw: 80.0 },
        Dispatch { bus: 3, mw: 60.0 },
    ],
};

// Joint projection: adjust all generation outputs to satisfy all constraints
let projected = projector.project(&command)?;
// projected satisfies:
//   - Bus voltage ∈ [0.95, 1.05]
//   - Branch power ≤ thermal limit
//   - System reserve ≥ 50 MW
//   - Total generation = Total load + Network losses

Projection Method Comparison

MethodAlgorithmComplexityPrecisionApplicable Scenarios
QuadraticProgrammingQuadratic programmingO(n³)HighMulti-constraint joint
ProjectionGradientProjection gradient methodO(n²)MediumLarge-scale problems
PenaltyMethodPenalty function methodO(n²)MediumNonlinear constraints
HeuristicHeuristicO(n log n)LowReal-time domain fast projection

Violation Handling Mechanism

Three-Level Processing Strategy

Constraint violated

   ├── Critical → Reject (refuse execution) + Emergency alert + Audit record

   ├── High     → Project (project feasible solution) + Alert + Audit record

   └── Medium/Low → Project or Warn + Audit record

Complete Violation Handling Code

use eneros_constraint::{Violation, ViolationHandler, AlertLevel};

let handler = ViolationHandler::new()
    .enable_audit_log(true)         // Enable audit log
    .enable_alert_channel(true)     // Enable alert channel
    .alert_topic("grid/alerts")     // Alert topic
    .max_alert_rate(100);           // Maximum alert rate (messages/sec)

// Handle violations
async fn handle_violation(
    handler: &ViolationHandler,
    violations: &[Violation],
    command: &Command,
) -> Result<Command, EnerOSError> {
    // 1. Record audit log (mandatory)
    handler.audit(AuditRecord {
        timestamp: now!(),
        command: command.clone(),
        violations: violations.to_vec(),
        source: source_agent_id(),
    }).await?;

    // 2. Handle based on severity
    let max_severity = violations.iter()
        .map(|v| v.severity)
        .max()
        .unwrap_or(Severity::Low);

    match max_severity {
        Severity::Critical => {
            // Emergency alert
            handler.alert(AlertLevel::Emergency, violations).await?;
            Err(EnerOSError::ConstraintRejected(violations.to_vec()))
        }
        Severity::High => {
            // High-level alert + projection
            handler.alert(AlertLevel::High, violations).await?;
            let projected = handler.project(command)?;
            Ok(projected)
        }
        _ => {
            // Medium/Low: project or just alert
            let projected = handler.project(command)?;
            Ok(projected)
        }
    }
}

Constraint Configuration File

Constraints can be bulk loaded via TOML configuration file:

# config/constraints.toml

[[voltage]]
name = "network_voltage_safety"
min = 0.95
max = 1.05
buses = "all"               # or ["bus_1", "bus_2"]
severity = "critical"
action = "project_and_warn"

[[voltage]]
name = "critical_bus_voltage"
min = 0.97
max = 1.03
buses = ["bus_1", "bus_5"]
severity = "critical"
action = "reject"

[[thermal]]
name = "line_L-1_thermal_limit"
branch = "L-1"
limit_mva = 100.0
emergency_limit_mva = 120.0
emergency_duration_min = 15
severity = "high"
action = "project_and_warn"

[[frequency]]
name = "system_frequency_safety"
min_hz = 49.5
max_hz = 50.5
emergency_min_hz = 49.0
emergency_max_hz = 51.0
rocof_hz_per_s = 2.0
severity = "critical"
action = "reject"

[[stability]]
name = "transient_stability"
type = "transient"
fault_clearing_ms = 100
critical_clearing_ms = 150
damping_ratio = 0.05
severity = "critical"
action = "reject"

[[reserve]]
name = "system_reserve"
min_reserve_mw = 50.0
spinning_percent = 5.0
severity = "high"
action = "reject"
use eneros_constraint::ConstraintEngine;

let engine = ConstraintEngine::new(&network);
engine.load_constraints("config/constraints.toml")?;

// After startup, constraints automatically validate all write operations
engine.start()?;

Integration with Agents

Constraint Protection for Agent Decisions

use eneros_constraint::{ConstraintEngine, CheckResult};

impl Agent for DispatchAgent {
    async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
        // 1. Agent generates decision command
        let command = self.optimize_dispatch().await?;

        // 2. Submit to kernel, constraint engine automatically validates
        let result = ctx.syscall(ExecuteCommand(command)).await?;

        match result {
            ExecuteResult::Executed => {
                log::info!("Dispatch command executed");
            }
            ExecuteResult::Projected(projected) => {
                log::warn!("Dispatch command was projected: {:?}", projected);
                self.learn_from_projection(projected); // Online learning
            }
            ExecuteResult::Rejected(reason) => {
                log::error!("Dispatch command rejected: {}", reason);
                self.fallback_strategy().await?;
            }
        }

        Ok(())
    }
}

Constraint Query API

Agents can proactively query constraints to avoid generating infeasible commands:

impl Agent for DispatchAgent {
    async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
        // 1. Query current constraints
        let constraints = ctx.syscall(GetConstraints {
            object: ConstraintObject::Bus(self.bus_id),
        }).await?;

        // 2. Optimize within constraints
        let voltage_limit = constraints.voltage_limit(self.bus_id);
        let generation_limit = constraints.generation_limit(self.bus_id);

        let target_mw = self.optimize_within(
            generation_limit.min_mw,
            generation_limit.max_mw,
        )?;

        // 3. Submit command (will not violate constraints)
        ctx.syscall(SetGeneration { bus: self.bus_id, mw: target_mw }).await?;

        Ok(())
    }
}

Performance Metrics

OperationLatencyThroughput
Single constraint check< 1μs10 million/sec
Multi-constraint check (10 constraints)< 5μs200k/sec
Constraint projection (QP)< 5ms200/sec
Constraint projection (heuristic)< 100μs10k/sec
Constraint config loading< 50ms-
Audit log write< 10μs100k/sec

Comparison with Traditional Solutions

DimensionTraditional Constraint ValidationEnerOS Constraint as Law
Validation locationApplication layerKernel mode
EnforceabilityOptionalMandatory
Validation omissionEasy to occurImpossible
Constraint projectionUsually not supportedBuilt-in QP solver
ConsistencyMultiple applications may conflictKernel unified
AuditScattered logsCentralized audit
Performance1-5ms< 10μs
ConfigurationHardcodedTOML config + API
Dynamic updateRequires restartHot reload

Limitations and Trade-offs

Trade-offDescriptionMitigation Strategy
Kernel overheadEvery command needs validationConstraint index acceleration, < 10μs
Projection may not be optimalQP solver finds local optimumSupports multiple projection method switching
Constraint conflictsMultiple constraints may contradictConflict detection at startup
Configuration complexityLarge amount of constraint configProvides standard templates and defaults
Real-time domain limitationProjection solving is slowerReal-time domain uses heuristic projection

Next Steps