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:
| Defect | Description | Incident Case |
|---|---|---|
| Validation omission | Some applications do not implement constraint validation | Equipment damage from voltage overlimits |
| Sequence errors | Validation occurs after execution | Misoperation already dispatched |
| Consistency issues | Multiple applications have conflicting constraints | Different applications overwrite each other |
| Performance overhead | Application-layer redundant computation | Increased decision latency |
| Non-auditable | Validation logs are scattered | Incidents are hard to trace |
| No projection | Rejection is the end | Decisions 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 Type | Applicable Object | Key Parameters | Default Action | Severity |
|---|---|---|---|---|
| Voltage constraint | Bus | min, max (pu) | Project | Critical |
| Current constraint | Branch | limit_a (A) | Project | High |
| Thermal limit constraint | Branch | limit_mva, emergency_limit_mva | Project | High |
| Frequency constraint | System | min_hz, max_hz, rocof | Reject | Critical |
| Active power constraint | Generator | min_mw, max_mw, ramp_rate | Project | Medium |
| Reactive power constraint | Generator/SVC | min_mvar, max_mvar | Project | Medium |
| Phase angle difference constraint | Both ends of branch | max_angle_diff (degrees) | Project | High |
| Short-circuit capacity constraint | Bus | max_scc_mva | Reject | Critical |
| Stability constraint | System/Region | damping ratio, margin | Reject | Critical |
| Reserve constraint | System | min_reserve_mw | Reject | High |
| N-1 constraint | Any branch | Meet above constraints after any single component outage | Warn | High |
Severity Levels and Processing Actions
| Severity | Default Action | Description |
|---|---|---|
| Critical | Reject | Directly reject, record alert |
| High | ProjectAndWarn | Project to feasible solution, record alert |
| Medium | Project | Project to feasible solution |
| Low | Warn | Only 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
| Method | Algorithm | Complexity | Precision | Applicable Scenarios |
|---|---|---|---|---|
| QuadraticProgramming | Quadratic programming | O(n³) | High | Multi-constraint joint |
| ProjectionGradient | Projection gradient method | O(n²) | Medium | Large-scale problems |
| PenaltyMethod | Penalty function method | O(n²) | Medium | Nonlinear constraints |
| Heuristic | Heuristic | O(n log n) | Low | Real-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
| Operation | Latency | Throughput |
|---|---|---|
| Single constraint check | < 1μs | 10 million/sec |
| Multi-constraint check (10 constraints) | < 5μs | 200k/sec |
| Constraint projection (QP) | < 5ms | 200/sec |
| Constraint projection (heuristic) | < 100μs | 10k/sec |
| Constraint config loading | < 50ms | - |
| Audit log write | < 10μs | 100k/sec |
Comparison with Traditional Solutions
| Dimension | Traditional Constraint Validation | EnerOS Constraint as Law |
|---|---|---|
| Validation location | Application layer | Kernel mode |
| Enforceability | Optional | Mandatory |
| Validation omission | Easy to occur | Impossible |
| Constraint projection | Usually not supported | Built-in QP solver |
| Consistency | Multiple applications may conflict | Kernel unified |
| Audit | Scattered logs | Centralized audit |
| Performance | 1-5ms | < 10μs |
| Configuration | Hardcoded | TOML config + API |
| Dynamic update | Requires restart | Hot reload |
Limitations and Trade-offs
| Trade-off | Description | Mitigation Strategy |
|---|---|---|
| Kernel overhead | Every command needs validation | Constraint index acceleration, < 10μs |
| Projection may not be optimal | QP solver finds local optimum | Supports multiple projection method switching |
| Constraint conflicts | Multiple constraints may contradict | Conflict detection at startup |
| Configuration complexity | Large amount of constraint config | Provides standard templates and defaults |
| Real-time domain limitation | Projection solving is slower | Real-time domain uses heuristic projection |
Next Steps
- Power-Native First - Power-Native First design philosophy
- Safety Guard - Safety gateway details
- Real-Time Determinism - Constraint validation in real-time domain
- Agent-as-Grid-Node - Agent permission boundaries