Skip to main content

eneros-core

Crate Index

eneros-core

eneros-core is the foundational crate shared by all 56 EnerOS crates, defining unified types, an error model, and global configuration. All upper-layer crates depend directly or indirectly on eneros-core, ensuring cross-module type consistency, zero-copy transfer, and error propagation through the call chain. It is the root node of the EnerOS dependency graph, with no internal EnerOS dependencies.

Responsibilities

eneros-core handles the following five categories of foundational responsibilities:

  • Unified Identifier Types: Provides strongly typed IDs such as ElementId, BusId, BranchId, AgentId, avoiding runtime errors caused by mixing String / u64. All IDs are type-checked at compile time and cross-crate transfer is zero-cost.
  • Unified Error Enum: EnerOSError covers all error categories including kernel, IO, protocol, constraint, Agent, and authentication. Combined with thiserror to derive Display / From, the error chain propagates losslessly through the call stack.
  • Global Configuration: EnerOSConfig centrally manages configuration for network, Load Flow, gateway, time series, security, and other subsystems, supporting TOML file loading + environment variable overrides + runtime hot updates.
  • Physical Quantity Types: Strongly typed PerUnit, Voltage, Angle, Power with units and precision, avoiding catastrophic errors caused by unit confusion (one of the root causes of the 2003 US-Canada blackout).
  • General Utilities: Duration extensions, DateTime aliases, Arc-wrapped shared handles, and other infrastructure.

Key Types

Error Model

use thiserror::Error;

#[derive(Debug, Error)]
pub enum EnerOSError {
    #[error("invalid network: {0}")]
    InvalidNetwork(String),

    #[error("power flow divergence after {iterations} iterations, residual={residual:e}")]
    PowerFlowDivergence { iterations: usize, residual: f64 },

    #[error("constraint violation: {0:?}")]
    ConstraintViolation(#[from] Violation),

    #[error("protocol error: {0}")]
    ProtocolError(String),

    #[error("unauthorized: {0}")]
    Unauthorized(String),

    #[error("not found: {0}")]
    NotFound(String),

    #[error("io error: {0}")]
    Io(#[from] std::io::Error),

    #[error("internal: {0}")]
    Internal(String),
}

pub type Result<T> = std::result::Result<T, EnerOSError>;

EnerOSError implements Send + Sync for safe cross-thread transfer; through #[from] it automatically converts io::Error and Violation, allowing callers to chain the error path with ?.

Identifier Types

use std::fmt;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ElementId(pub String);

impl ElementId {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

impl fmt::Display for ElementId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

pub type BusId = ElementId;
pub type BranchId = ElementId;
pub type AgentId = ElementId;
pub type TenantId = ElementId;

Using pub struct ElementId(String) instead of a bare String allows expressing intent in function signatures and prevents accidentally passing a BusId to a function expecting a BranchId.

Physical Quantity Types

use std::ops::{Add, Sub, Mul};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PerUnit(pub f64);

impl PerUnit {
    pub const fn new(v: f64) -> Self {
        Self(v)
    }

    pub fn clamp(&self, min: f64, max: f64) -> Self {
        Self(self.0.clamp(min, max))
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Voltage {
    pub magnitude: PerUnit,
    pub angle: Angle,
}

impl Voltage {
    pub fn new(magnitude: f64, angle_rad: f64) -> Self {
        Self {
            magnitude: PerUnit(magnitude),
            angle: Angle::from_radians(angle_rad),
        }
    }

    pub fn complex(&self) -> num_complex::Complex64 {
        num_complex::Complex64::from_polar(self.magnitude.0, self.angle.as_radians())
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Angle(f64);

impl Angle {
    pub fn from_radians(rad: f64) -> Self {
        Self(rad)
    }

    pub fn from_degrees(deg: f64) -> Self {
        Self(deg.to_radians())
    }

    pub fn as_radians(&self) -> f64 {
        self.0
    }

    pub fn as_degrees(&self) -> f64 {
        self.0.to_degrees()
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Power {
    pub active_mw: f64,
    pub reactive_mvar: f64,
}

Bus Type

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BusType {
    PQ,
    PV,
    Slack,
    Isolated,
}

impl BusType {
    pub fn is_slack(&self) -> bool {
        matches!(self, Self::Slack)
    }

    pub fn is_pq(&self) -> bool {
        matches!(self, Self::PQ)
    }
}

Global Configuration

use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnerOSConfig {
    pub network: NetworkConfig,
    pub powerflow: PowerFlowConfig,
    pub gateway: GatewayConfig,
    pub timeseries: TimeSeriesConfig,
    pub trust: TrustConfig,
    pub agent: AgentConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
    pub default_nominal_kv: f64,
    pub allow_isolated_buses: bool,
    pub max_buses: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PowerFlowConfig {
    pub method: String,
    pub tolerance: f64,
    pub max_iterations: usize,
    pub enable_cache: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayConfig {
    pub mode: String,
    pub rate_limit_per_second: u32,
    pub require_two_factor: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesConfig {
    pub storage_path: String,
    pub retention_days: u32,
    pub batch_size: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustConfig {
    pub ca_dir: String,
    pub cert_validity_days: u32,
    pub rotation_threshold_days: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
    pub max_concurrent: usize,
    pub default_tick_interval_ms: u64,
}

impl Default for EnerOSConfig {
    fn default() -> Self {
        Self {
            network: NetworkConfig {
                default_nominal_kv: 110.0,
                allow_isolated_buses: false,
                max_buses: 100_000,
            },
            powerflow: PowerFlowConfig {
                method: "newton_raphson".into(),
                tolerance: 1e-8,
                max_iterations: 50,
                enable_cache: true,
            },
            gateway: GatewayConfig {
                mode: "enforce".into(),
                rate_limit_per_second: 100,
                require_two_factor: true,
            },
            timeseries: TimeSeriesConfig {
                storage_path: "/var/lib/eneros/ts".into(),
                retention_days: 365,
                batch_size: 4096,
            },
            trust: TrustConfig {
                ca_dir: "/var/lib/eneros/ca".into(),
                cert_validity_days: 90,
                rotation_threshold_days: 30,
            },
            agent: AgentConfig {
                max_concurrent: 64,
                default_tick_interval_ms: 100,
            },
        }
    }
}

impl EnerOSConfig {
    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
        let text = std::fs::read_to_string(path.as_ref())?;
        let mut cfg: Self = toml::from_str(&text)?;
        cfg.apply_env_overrides();
        Ok(cfg)
    }

    pub fn apply_env_overrides(&mut self) {
        if let Ok(v) = std::env::var("ENEROS_POWERFLOW_TOLERANCE") {
            if let Ok(t) = v.parse::<f64>() {
                self.powerflow.tolerance = t;
            }
        }
        if let Ok(v) = std::env::var("ENEROS_GATEWAY_RATE_LIMIT") {
            if let Ok(r) = v.parse::<u32>() {
                self.gateway.rate_limit_per_second = r;
            }
        }
    }
}

Core API

MethodSignatureDescription
EnerOSConfig::loadfn load(path: impl AsRef<Path>) -> Result<Self>Load configuration from TOML file
EnerOSConfig::defaultfn default() -> SelfReturn default configuration
EnerOSConfig::apply_env_overridesfn apply_env_overrides(&mut self)Apply environment variable overrides
ElementId::newfn new(s: impl Into<String>) -> SelfConstruct identifier
ElementId::is_emptyfn is_empty(&self) -> boolCheck if empty
Voltage::newfn new(magnitude: f64, angle_rad: f64) -> SelfConstruct voltage phasor
Voltage::complexfn complex(&self) -> Complex64Convert to complex representation
Angle::from_degreesfn from_degrees(deg: f64) -> SelfConvert degrees to Angle
PerUnit::clampfn clamp(&self, min: f64, max: f64) -> SelfClamp per-unit value

Usage Examples

Load Configuration and Validate

use eneros_core::{EnerOSConfig, EnerOSError, ElementId, BusType};

fn main() -> Result<(), EnerOSError> {
    let config = EnerOSConfig::load("eneros.toml")?;
    println!("Load Flow method: {}", config.powerflow.method);
    println!("Convergence tolerance: {}", config.powerflow.tolerance);
    Ok(())
}

Unified Error Propagation

use eneros_core::{EnerOSError, ElementId};

fn validate(id: &ElementId) -> Result<(), EnerOSError> {
    if id.is_empty() {
        return Err(EnerOSError::InvalidNetwork("empty id".into()));
    }
    Ok(())
}

fn find_bus(id: &ElementId) -> Result<BusType, EnerOSError> {
    validate(id)?;
    Ok(BusType::PQ)
}

Physical Quantity Operations

use eneros_core::{Voltage, Angle, PerUnit, Power};

let v1 = Voltage::new(1.05, 0.0);
let v2 = Voltage::new(0.98, -0.12);

let diff = v1.magnitude.0 - v2.magnitude.0;
println!("Voltage difference: {:.4} pu", diff);

let angle = Angle::from_degrees(30.0);
println!("Angle: {:.4} rad / {:.4} deg", angle.as_radians(), angle.as_degrees());

let load = Power { active_mw: 50.0, reactive_mvar: 12.5 };
println!("Load: {:.2} MW + {:.2} Mvar", load.active_mw, load.reactive_mvar);

let pu = PerUnit::new(1.08).clamp(0.95, 1.05);
println!("After clamp: {:.4}", pu.0);

Configuration File Example (eneros.toml)

[network]
default_nominal_kv = 110.0
allow_isolated_buses = false
max_buses = 100000

[powerflow]
method = "newton_raphson"
tolerance = 1e-8
max_iterations = 50
enable_cache = true

[gateway]
mode = "enforce"
rate_limit_per_second = 100
require_two_factor = true

[timeseries]
storage_path = "/var/lib/eneros/ts"
retention_days = 365
batch_size = 4096

[trust]
ca_dir = "/var/lib/eneros/ca"
cert_validity_days = 90
rotation_threshold_days = 30

[agent]
max_concurrent = 64
default_tick_interval_ms = 100

Configuration Parameter Table

[network] Section

FieldTypeDefaultDescription
default_nominal_kvf64110.0Default nominal voltage (kV)
allow_isolated_busesboolfalseWhether to allow isolated buses
max_busesusize100000Maximum number of buses per network

[powerflow] Section

FieldTypeDefaultDescription
methodString”newton_raphson”Solver method
tolerancef641e-8Convergence tolerance (power residual)
max_iterationsusize50Maximum number of iterations
enable_cachebooltrueWhether to enable result caching

[gateway] Section

FieldTypeDefaultDescription
modeString”enforce”Gateway mode: enforce / warn / project
rate_limit_per_secondu32100Rate limit per second
require_two_factorbooltrueWhether control commands require two-factor confirmation

[timeseries] Section

FieldTypeDefaultDescription
storage_pathString”/var/lib/eneros/ts”SQLite storage directory
retention_daysu32365Data retention days
batch_sizeusize4096Batch write size

[trust] Section

FieldTypeDefaultDescription
ca_dirString”/var/lib/eneros/ca”CA private key and certificate directory
cert_validity_daysu3290Certificate validity period (days)
rotation_threshold_daysu3230Days before expiry to trigger rotation

[agent] Section

FieldTypeDefaultDescription
max_concurrentusize64Maximum number of concurrent Agents
default_tick_interval_msu64100Default tick interval (milliseconds)

Environment Variable Overrides

eneros-core supports overriding configuration file fields through environment variables, with the unified prefix ENEROS_:

Environment VariableOverride FieldType
ENEROS_POWERFLOW_TOLERANCEpowerflow.tolerancef64
ENEROS_GATEWAY_RATE_LIMITgateway.rate_limit_per_secondu32
ENEROS_TS_RETENTION_DAYStimeseries.retention_daysu32
ENEROS_AGENT_MAX_CONCURRENTagent.max_concurrentusize

Performance Metrics

OperationLatency
EnerOSConfig::load (typical config file)< 1ms
ElementId clone< 50ns
Voltage::complex conversion< 5ns
Error construction (with string allocation)< 200ns

Dependencies

Own Dependencies

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
toml = "0.8"
tracing = "0.1"
num-complex = "0.4"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }

Depended On By

All 55 other crates depend directly or indirectly on eneros-core, making it the root node of the EnerOS dependency graph. Typical dependency paths:

eneros-core

eneros-linalg → eneros-topology → eneros-powerflow → eneros-analysis
   ↓                                   ↓
eneros-timeseries → eneros-twin     eneros-constraint → eneros-gateway → eneros-api

No internal EnerOS dependencies; can be compiled and unit-tested independently.

Version and Compatibility

eneros-core VersionEnerOS VersionCompatibility Notes
0.47.x0.47.xCurrent stable version, stable API
0.46.x0.46.xCompatible with 0.45.x configuration files
0.45.x (LTS)0.45.xLTS version, security updates until 2028