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
| Method | Signature | Description |
|---|
EnerOSConfig::load | fn load(path: impl AsRef<Path>) -> Result<Self> | Load configuration from TOML file |
EnerOSConfig::default | fn default() -> Self | Return default configuration |
EnerOSConfig::apply_env_overrides | fn apply_env_overrides(&mut self) | Apply environment variable overrides |
ElementId::new | fn new(s: impl Into<String>) -> Self | Construct identifier |
ElementId::is_empty | fn is_empty(&self) -> bool | Check if empty |
Voltage::new | fn new(magnitude: f64, angle_rad: f64) -> Self | Construct voltage phasor |
Voltage::complex | fn complex(&self) -> Complex64 | Convert to complex representation |
Angle::from_degrees | fn from_degrees(deg: f64) -> Self | Convert degrees to Angle |
PerUnit::clamp | fn clamp(&self, min: f64, max: f64) -> Self | Clamp 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
| Field | Type | Default | Description |
|---|
default_nominal_kv | f64 | 110.0 | Default nominal voltage (kV) |
allow_isolated_buses | bool | false | Whether to allow isolated buses |
max_buses | usize | 100000 | Maximum number of buses per network |
[powerflow] Section
| Field | Type | Default | Description |
|---|
method | String | ”newton_raphson” | Solver method |
tolerance | f64 | 1e-8 | Convergence tolerance (power residual) |
max_iterations | usize | 50 | Maximum number of iterations |
enable_cache | bool | true | Whether to enable result caching |
[gateway] Section
| Field | Type | Default | Description |
|---|
mode | String | ”enforce” | Gateway mode: enforce / warn / project |
rate_limit_per_second | u32 | 100 | Rate limit per second |
require_two_factor | bool | true | Whether control commands require two-factor confirmation |
[timeseries] Section
| Field | Type | Default | Description |
|---|
storage_path | String | ”/var/lib/eneros/ts” | SQLite storage directory |
retention_days | u32 | 365 | Data retention days |
batch_size | usize | 4096 | Batch write size |
[trust] Section
| Field | Type | Default | Description |
|---|
ca_dir | String | ”/var/lib/eneros/ca” | CA private key and certificate directory |
cert_validity_days | u32 | 90 | Certificate validity period (days) |
rotation_threshold_days | u32 | 30 | Days before expiry to trigger rotation |
[agent] Section
| Field | Type | Default | Description |
|---|
max_concurrent | usize | 64 | Maximum number of concurrent Agents |
default_tick_interval_ms | u64 | 100 | Default tick interval (milliseconds) |
Environment Variable Overrides
eneros-core supports overriding configuration file fields through environment variables, with the unified prefix ENEROS_:
| Environment Variable | Override Field | Type |
|---|
ENEROS_POWERFLOW_TOLERANCE | powerflow.tolerance | f64 |
ENEROS_GATEWAY_RATE_LIMIT | gateway.rate_limit_per_second | u32 |
ENEROS_TS_RETENTION_DAYS | timeseries.retention_days | u32 |
ENEROS_AGENT_MAX_CONCURRENT | agent.max_concurrent | usize |
| Operation | Latency |
|---|
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 Version | EnerOS Version | Compatibility Notes |
|---|
| 0.47.x | 0.47.x | Current stable version, stable API |
| 0.46.x | 0.46.x | Compatible with 0.45.x configuration files |
| 0.45.x (LTS) | 0.45.x | LTS version, security updates until 2028 |