Physical Constraint Decision
EnerOS follows the “Constraint-as-Law” principle: any Agent decision must pass validation by the kernel constraint engine before taking effect. Commands that violate physical laws are intercepted directly and projected onto the nearest feasible solution. This mechanism ensures that Agent intelligence always operates within the physically feasible domain of the power system.
This is one of the fundamental characteristics that distinguish EnerOS from a general-purpose AgentOS. In a general AI Agent system, an “infeasible” decision output by a model may at worst cause a business failure; in a power system, however, a single switching operation that violates physical laws can trigger a large-scale blackout, equipment damage, or even personal injury. At the cost of microsecond-level overhead, the constraint engine locks all Agent decisions into the physically feasible domain.
Design Philosophy
“Constraint-as-Law” entails three layers of meaning:
- Mandatory enforcement: Constraint validation is not an optional step but a mandatory link in the kernel command channel; Agents cannot bypass it
- Physical feasibility: All constraints are based on power system physical laws (load flow equations, thermal stability limits, stability boundaries), not business rules
- Feasibility projection: Commands that violate constraints are not simply rejected; instead, they are projected onto the nearest feasible solution, preventing Agents from getting stuck in infinite loops
Constraint Engine Architecture
The constraint engine resides in kernel space and is divided into three layers: declaration layer (DSL), validation layer (synchronous), and projection layer (asynchronous).
┌──────────────────────────────────────────────────┐
│ Declaration Layer ConstraintBuilder (DSL) │
│ - Typed constraint constructors │
│ - Compile-time type checking │
└───────────────────┬──────────────────────────────┘
│ register()
┌───────────────────┴──────────────────────────────┐
│ Validation Layer ConstraintEngine.validate() (sync) │
│ - Single constraint < 5μs │
│ - Full constraint set < 200μs │
│ - Returns ValidationResult │
└───────────────────┬──────────────────────────────┘
│ if violated
┌───────────────────┴──────────────────────────────┐
│ Projection Layer ConstraintEngine.project() (async) │
│ - QP/LP solver │
│ - Weighted least squares │
│ - Returns the nearest feasible solution │
└──────────────────────────────────────────────────┘
Constraint Engine Initialization
use eneros_constraint::{ConstraintEngine, Constraint, ConstraintBuilder};
let mut engine = ConstraintEngine::new();
// Declare constraints
engine.register(ConstraintBuilder::voltage_limit(0.95, 1.05));
engine.register(ConstraintBuilder::thermal_limit(bus_id, 100.0)); // MW
engine.register(ConstraintBuilder::line_flow_limit(branch_id, 50.0));
engine.register(ConstraintBuilder::frequency_band(49.5, 50.5));
engine.register(ConstraintBuilder::power_balance(tolerance: 0.1));
engine.register(ConstraintBuilder::reserve_requirement(0.05)); // 5% of load
ConstraintBuilder Method Overview
| Method | Input | Meaning | Strictness |
|---|---|---|---|
| voltage_limit | min, max (p.u.) | Bus voltage magnitude upper/lower bounds | Hard constraint |
| thermal_limit | element_id, MVA | Element thermal stability limit | Hard constraint |
| line_flow_limit | branch_id, MW | Branch active power upper bound | Hard constraint |
| frequency_band | min, max (Hz) | Allowed system frequency range | Hard constraint |
| power_balance | tolerance (MW) | Total generation = total load + losses | Hard constraint |
| reserve_requirement | percent | Minimum reserve ratio | Soft constraint |
| phase_angle_diff | bus_a, bus_b, deg | Phase angle difference upper bound between two buses | Hard constraint |
| short_circuit_capacity | bus_id, MVA | Bus short-circuit capacity upper bound | Hard constraint |
| contingency | base, contingency, post | Constraints satisfied after N-1 contingency | Planning constraint |
| stability_margin | percent | Transient stability margin | Soft constraint |
Constraint DSL
Constraints use a typed DSL, avoiding the runtime risk of string parsing and supporting compile-time checks.
use eneros_constraint::{Constraint, Contingency};
// Voltage constraint
let v = Constraint::voltage()
.bus(1)
.min(0.95)
.max(1.05)
.build();
// Thermal stability constraint
let thermal = Constraint::thermal()
.element(branch_id)
.limit_mva(50.0)
.emergency_limit_mva(60.0)
.duration_secs(15 * 60)
.build();
// N-1 constraint
let n_minus_1 = Constraint::contingency()
.base_case(v.clone())
.contingency(Contingency::branch_out(5))
.post_contingency(v.clone())
.build();
// Frequency constraint
let freq = Constraint::frequency()
.min(49.5)
.max(50.5)
.rocof_max(0.5) // Hz/s rate of change of frequency
.build();
// Reserve constraint (soft constraint)
let reserve = Constraint::reserve()
.load_following(50.0) // MW
.spinning(100.0) // MW
.non_spinning(200.0) // MW
.severity(Severity::Soft)
.build();
Constraint Field Reference
| Field | Type | Meaning |
|---|---|---|
| id | ConstraintId | Unique constraint identifier |
| kind | ConstraintKind | Voltage/Thermal/Frequency/Reserve/N-1, etc. |
| scope | Scope | Scope: bus, branch, system |
| severity | Severity | Hard / Soft / Planning |
| penalty | f64 | Penalty factor for soft constraint violation |
| enabled | bool | Whether enabled |
| tags | Vec | User tags for grouping and querying |
Decision Validation
Commands produced by Agents must pass through validate before execution:
use eneros_constraint::{ValidationResult, DispatchDecision};
let decision = DispatchDecision::new()
.set_generator(gen_id, 80.0) // MW
.open_branch(branch_id)
.adjust_tap(xfmr_id, 5)
.set_load(bus_id, 50.0); // MW
match engine.validate(&decision, &network) {
ValidationResult::Ok => {
ctx.execute(decision).await?;
}
ValidationResult::Violated(violations) => {
log::warn!("Decision violates constraints: {:?}", violations);
// Project to the nearest feasible solution
let projected = engine.project(&decision, &network)?;
ctx.execute(projected).await?;
}
ValidationResult::Fatal(fatal) => {
log::error!("Fatal violation: {:?}", fatal);
ctx.emergency_block().await?;
}
}
ValidationResult Variants
| Variant | Meaning | Handling |
|---|---|---|
| Ok | All constraints satisfied | Execute directly |
| Violated | Soft/hard constraint violated, projectable | Call project() to project |
| Fatal | Fatal violation (e.g., frequency instability) | Emergency block, stop Agent |
Violation Structure
pub struct Violation {
pub constraint_id: ConstraintId,
pub kind: ConstraintKind,
pub element: ElementId,
pub actual: f64,
pub limit: f64,
pub deviation: f64,
pub severity: Severity,
pub message: String,
}
Feasibility Projection
When the original decision violates constraints, the projection layer projects it onto the nearest boundary of the constraint-feasible region based on a QP/LP solver, avoiding the situation where a simple rejection causes the Agent to get stuck in an infinite loop.
use eneros_constraint::ProjectionStrategy;
// Projection strategy: weighted least squares
let strategy = ProjectionStrategy::WeightedLeastSquares {
weights: weights_map,
max_iter: 100,
tolerance: 1e-6,
};
let projected = engine.project_with(&decision, &network, strategy)?;
// projected satisfies all hard constraints and is the minimum distance from the original decision
ProjectionStrategy Variants
| Strategy | Applicable Scenario | Solver | Typical Cost |
|---|---|---|---|
| WeightedLeastSquares | General dispatch decisions | QP (OSQP) | < 20ms |
| LeastSquares | Equal-weight projection | QP (OSQP) | < 15ms |
| LinearProgram | Pure linear constraints | LP (HiGHS) | < 5ms |
| MinShift | Minimum adjustment amount | MILP | < 50ms |
| PriorityBased | Priority-based adjustment | Heuristic | < 1ms |
Custom Projection Example
use eneros_constraint::{Projector, Decision};
struct EconomicProjector {
gen_costs: Vec<f64>,
}
impl Projector for EconomicProjector {
fn project(
&self,
decision: &Decision,
constraints: &[Constraint],
network: &NetworkGraph,
) -> ProjectResult {
// Adjust in ascending order of generation cost; reduce high-cost units first
let mut sorted_gens = decision.gen_changes.clone();
sorted_gens.sort_by(|a, b| {
self.gen_costs[a.gen_id as usize]
.partial_cmp(&self.gen_costs[b.gen_id as usize])
.unwrap()
});
let mut projected = decision.clone();
for c in constraints {
if c.is_violated(&projected, network) {
self.adjust_for(&mut projected, c, &sorted_gens)?;
}
}
Ok(projected)
}
}
let projected = engine.project_with_strategy(&decision, &network, EconomicProjector { gen_costs })?;
Constraint Type Matrix
| Category | Constraint | Strictness | Check Time |
|---|---|---|---|
| Voltage | V_min ≤ V_i ≤ V_max | Hard constraint | Real-time + before decision |
| Thermal stability | I_ij ≤ I_max | Hard constraint | Real-time + before decision |
| Frequency | f_min ≤ f ≤ f_max | Hard constraint | Real-time |
| Rate of change of frequency | df/dt | ≤ RoCoF_max | |
| Power balance | ΣP_gen = ΣP_load + Loss | Hard constraint | Before decision |
| Reserve | Reserve ≥ 5% Load | Soft constraint | Planning |
| N-1 | Above constraints satisfied after single contingency | Planning constraint | Planning |
| Short-circuit capacity | I_sc ≤ I_sc_max | Hard constraint | Planning |
| Transient stability | Stability margin ≥ threshold | Soft constraint | Online |
| Voltage stability | Margin ≥ threshold | Soft constraint | Online |
Kernel Mandatory Enforcement
As a kernel module, the constraint engine is hooked into the command execution channel. All ctx.execute(decision) calls automatically go through constraint validation, and Agents cannot bypass it:
use eneros_constraint::KernelHook;
// Mount the constraint hook at kernel startup
KernelHook::mount(&constraint_engine)?;
// After this, all ctx.execute() automatically call:
// 1. engine.validate(&decision, &network)
// 2. If Violated → automatically call project()
// 3. If Fatal → intercept and trigger emergency_block
N-1 Validation
N-1 safety check for critical scenarios:
use eneros_constraint::{ContingencySet, Contingency};
let contingencies = ContingencySet::new()
.add(Contingency::branch_out(5))
.add(Contingency::branch_out(12))
.add(Contingency::generator_out(gen_id))
.add(Contingency::bus_split(bus_id));
let report = engine.n_minus_1_check(&network, &contingencies)?;
for case in &report.cases {
if !case.feasible {
println!("Violation after contingency {:?}: {:?}", case.contingency, case.violations);
}
}
println!("Pass rate: {}/{}", report.passed, report.total);
Performance Metrics
| Operation | Latency | Note |
|---|---|---|
| Single constraint validation | < 5μs | Cache hit |
| Full constraint validation (1000 nodes) | < 200μs | Including load flow results |
| Full constraint validation (100k nodes) | < 5ms | Including load flow results |
| Feasibility projection (LP, 1000 variables) | < 5ms | HiGHS |
| Feasibility projection (QP, 1000 variables) | < 20ms | OSQP |
| N-1 check (100 contingencies) | < 50ms | Parallel |
| Constraint registration | < 10μs | - |
| Constraint hot update | < 100μs | Non-blocking validation |
Test environment: 4 cores / 8GB / Ubuntu 22.04, node count 1000.
Configuration Parameters
Constraint-related configuration in eneros.toml:
[constraint]
# Constraint validation strictness: strict / permissive / audit_only
mode = "strict"
# Whether to automatically project violating decisions
auto_project = true
# Default projection strategy: wls / ls / lp / min_shift / priority
default_projection = "wls"
# N-1 check parallelism
n_minus_1_parallelism = 4
# Soft constraint penalty factor (default)
soft_penalty = 1000.0
# Whether fatal violations trigger emergency block
fatal_emergency_block = true
# Constraint cache size
cache_size = 4096
| Parameter | Type | Default | Description |
|---|---|---|---|
| mode | enum | strict | Validation mode |
| auto_project | bool | true | Automatically project violating decisions |
| default_projection | enum | wls | Default projection strategy |
| n_minus_1_parallelism | u32 | 4 | N-1 check parallelism |
| soft_penalty | f64 | 1000.0 | Default soft constraint penalty factor |
| fatal_emergency_block | bool | true | Fatal violation triggers block |
| cache_size | u32 | 4096 | Number of constraint result cache entries |
Relationship with Other Capabilities
| Related Capability | Interaction |
|---|---|
| Grid Topology First-Class Citizen | Constraints are computed based on topology |
| Safety Guard | The constraint engine is the core component of the safety gateway |
| Digital Twin Engine | What-If simulation calls constraint validation |
| Multi-Agent Collaboration | Agent decisions must pass constraint validation |
| Real-Time Dual Execution Domain | Real-time domain constraint validation completes in microseconds |
| Equipment Model Library | Thermal stability limits come from equipment parameters |
| Time-Series Native Operations | Constraint violation events are ingested |
Related Documentation
- Grid Topology First-Class Citizen — Constraints are computed based on topology
- Safety Guard — The constraint engine is the core of the safety gateway
- Digital Twin Engine — What-If simulation calls constraint validation
- EnerOS Introduction — Constraint as Kernel Law design philosophy