Skip to main content

Physical Constraint Decision

Capabilities

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

MethodInputMeaningStrictness
voltage_limitmin, max (p.u.)Bus voltage magnitude upper/lower boundsHard constraint
thermal_limitelement_id, MVAElement thermal stability limitHard constraint
line_flow_limitbranch_id, MWBranch active power upper boundHard constraint
frequency_bandmin, max (Hz)Allowed system frequency rangeHard constraint
power_balancetolerance (MW)Total generation = total load + lossesHard constraint
reserve_requirementpercentMinimum reserve ratioSoft constraint
phase_angle_diffbus_a, bus_b, degPhase angle difference upper bound between two busesHard constraint
short_circuit_capacitybus_id, MVABus short-circuit capacity upper boundHard constraint
contingencybase, contingency, postConstraints satisfied after N-1 contingencyPlanning constraint
stability_marginpercentTransient stability marginSoft 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

FieldTypeMeaning
idConstraintIdUnique constraint identifier
kindConstraintKindVoltage/Thermal/Frequency/Reserve/N-1, etc.
scopeScopeScope: bus, branch, system
severitySeverityHard / Soft / Planning
penaltyf64Penalty factor for soft constraint violation
enabledboolWhether enabled
tagsVecUser 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

VariantMeaningHandling
OkAll constraints satisfiedExecute directly
ViolatedSoft/hard constraint violated, projectableCall project() to project
FatalFatal 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

StrategyApplicable ScenarioSolverTypical Cost
WeightedLeastSquaresGeneral dispatch decisionsQP (OSQP)< 20ms
LeastSquaresEqual-weight projectionQP (OSQP)< 15ms
LinearProgramPure linear constraintsLP (HiGHS)< 5ms
MinShiftMinimum adjustment amountMILP< 50ms
PriorityBasedPriority-based adjustmentHeuristic< 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

CategoryConstraintStrictnessCheck Time
VoltageV_min ≤ V_i ≤ V_maxHard constraintReal-time + before decision
Thermal stabilityI_ij ≤ I_maxHard constraintReal-time + before decision
Frequencyf_min ≤ f ≤ f_maxHard constraintReal-time
Rate of change of frequencydf/dt≤ RoCoF_max
Power balanceΣP_gen = ΣP_load + LossHard constraintBefore decision
ReserveReserve ≥ 5% LoadSoft constraintPlanning
N-1Above constraints satisfied after single contingencyPlanning constraintPlanning
Short-circuit capacityI_sc ≤ I_sc_maxHard constraintPlanning
Transient stabilityStability margin ≥ thresholdSoft constraintOnline
Voltage stabilityMargin ≥ thresholdSoft constraintOnline

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

OperationLatencyNote
Single constraint validation< 5μsCache hit
Full constraint validation (1000 nodes)< 200μsIncluding load flow results
Full constraint validation (100k nodes)< 5msIncluding load flow results
Feasibility projection (LP, 1000 variables)< 5msHiGHS
Feasibility projection (QP, 1000 variables)< 20msOSQP
N-1 check (100 contingencies)< 50msParallel
Constraint registration< 10μs-
Constraint hot update< 100μsNon-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
ParameterTypeDefaultDescription
modeenumstrictValidation mode
auto_projectbooltrueAutomatically project violating decisions
default_projectionenumwlsDefault projection strategy
n_minus_1_parallelismu324N-1 check parallelism
soft_penaltyf641000.0Default soft constraint penalty factor
fatal_emergency_blockbooltrueFatal violation triggers block
cache_sizeu324096Number of constraint result cache entries

Relationship with Other Capabilities

Related CapabilityInteraction
Grid Topology First-Class CitizenConstraints are computed based on topology
Safety GuardThe constraint engine is the core component of the safety gateway
Digital Twin EngineWhat-If simulation calls constraint validation
Multi-Agent CollaborationAgent decisions must pass constraint validation
Real-Time Dual Execution DomainReal-time domain constraint validation completes in microseconds
Equipment Model LibraryThermal stability limits come from equipment parameters
Time-Series Native OperationsConstraint violation events are ingested