EMS Subsystem Simulation Model (AI / Code Generation Version)
This document specifies the EMS functional models using formal notation for automated code generation. Targets: Python, C/C++, MATLAB/Simulink.
Covers: REQ-EMS-01, 02, 03, 04, 07. Excludes: REQ-EMS-05, 06, 08.
Sign convention: P > 0 = Generation (plant exports). P < 0 = Consumption (plant imports). For BESS: P_bess > 0 = Charge, P_bess < 0 = Discharge.
Simulation Loop (Top-Level Pseudocode)
# ─── Initialization ──────────────────────────────────────────
mode = "MODE_OFF"
P_integral = 0.0
Q_integral = 0.0
P_plant_ramped = 0.0
P_bess_setpoint = 0.0
P_pv_setpoint = 0.0
P_wind_setpoint = 0.0
alarm_state = {}
# ─── Per-step loop ───────────────────────────────────────────
for k in range(N_steps):
# 1. Read all measurement inputs
P_pcc, Q_pcc, V_pcc, f = read_pcc_meter()
SoC, P_lim_chg, P_lim_dis, bms_alarms = read_bms()
P_pv_avail, pv_ok = read_pv_measurements()
P_wind_avail, wind_ok = read_wind_measurements()
breaker_closed = read_breaker_ied()
# 2. Alarm Manager: check conditions, update state
alarm_state = alarm_manager_step(
SoC, bms_alarms, f, breaker_closed,
pcc_data_age, asset_data_age
)
# 3. Mode Manager: validate and select mode
mode = mode_manager_step(mode, alarm_state, operator_cmd, enable_conditions)
P_target, Q_target = get_targets_for_mode(mode, operator_cmd)
# 4. PCC Tracking Controller
if mode in ["MODE_P", "MODE_Q", "MODE_PF", "MODE_V"]:
P_plant_cmd, P_integral = pcc_tracking_step(
P_pcc, Q_pcc, P_target, Q_target, P_integral, dt
)
else:
P_plant_cmd = 0.0
# 5. Ramp & Limit Manager
P_plant_limited, P_plant_ramped = ramp_limit_step(
P_plant_cmd, P_plant_ramped,
P_lim_chg, P_lim_dis, P_pv_avail, P_wind_avail, dt
)
# 6. Coordinator: split to assets
P_bess_setpoint, P_pv_setpoint, P_wind_setpoint = coordinator_step(
P_plant_limited, P_pv_avail, P_wind_avail,
SoC, P_lim_chg, P_lim_dis
)
# 7. Dispatch setpoints to asset controllers
dispatch(P_pv_setpoint, P_wind_setpoint, P_bess_setpoint)
1. Mode and Compliance Manager
def mode_manager_step(current_mode, alarm_state, operator_cmd, enable_ok):
"""
Returns: new_mode (string)
"""
# Critical fault → force off
if alarm_state.get("critical"):
return "MODE_OFF"
# Comms loss → hold
if alarm_state.get("comms_loss"):
return "MODE_HOLD"
# Normal operator transitions
if enable_ok and operator_cmd.mode_request in VALID_MODES:
return operator_cmd.mode_request
return current_mode
VALID_MODES = ["MODE_P", "MODE_Q", "MODE_PF", "MODE_V", "MODE_OFF"]
Enable conditions (all must be TRUE before leaving MODE_OFF):
def check_enable_conditions(pcc_data_age, breaker_closed, bms_alarms, asset_available):
return (
pcc_data_age < pcc_timeout and # PCC data fresh
breaker_closed and # Grid breaker closed
not bms_alarms.critical and # No BMS critical alarm
asset_available # At least one asset OK
)
Parameters:
| Symbol | Description | Default |
|---|---|---|
pcc_timeout |
PCC data staleness limit (s) | 5.0 |
comms_loss_timeout |
Hold mode trigger (s) | 30.0 |
recovery_delay |
Re-enable delay after fault (s) | 60.0 |
2. PCC Tracking Controller
Error:
e_P[k] = P_{target} - P_{pcc}[k]
PI control with anti-windup (discrete):
I_P[k] = \mathrm{clamp}\!\left(I_P[k-1] + e_P[k] \cdot \Delta t,\ -I_{limit},\ +I_{limit}\right)
P_{plant\_cmd}[k] = K_p \cdot e_P[k] + K_i \cdot I_P[k]
Apply identically for Q when in reactive power / cosphi / voltage mode.
def pcc_tracking_step(P_pcc, Q_pcc, P_target, Q_target, P_integral, dt):
P_error = P_target - P_pcc
P_integral = clamp(P_integral + P_error * dt, -I_limit, +I_limit)
P_plant_cmd = Kp * P_error + Ki * P_integral
return P_plant_cmd, P_integral
Parameters:
| Symbol | Description | Default |
|---|---|---|
| $K_p$ | Proportional gain | 0.5 |
| $K_i$ | Integral gain | 0.1 |
| $\Delta t$ | Control step (s) | 0.5 |
| $I_{limit}$ | Anti-windup bound (MW) | P_plant_max |
3. Ramp and Limit Manager
Ramp filter (Forward-Euler discrete):
\Delta P_{max} = \dot{P}_{max} \cdot \Delta t
P_{ramped}[k] = \mathrm{clamp}\!\left(P_{cmd}[k],\ P_{ramped}[k-1] - \Delta P_{max},\ P_{ramped}[k-1] + \Delta P_{max}\right)
Hard constraint clamp:
P_{limited} = \mathrm{clamp}\!\left(P_{ramped}[k],\ P_{plant\_min},\ P_{plant\_max}\right)
Where $P_{plant_max}$ is the effective minimum of:
P_{plant\_max} = \min\!\left(P_{site\_export\_limit},\ P_{pv\_avail} + P_{wind\_avail},\ P_{lim\_dis}\right)
def ramp_limit_step(P_cmd, P_prev, P_lim_chg, P_lim_dis, P_pv_avail, P_wind_avail, dt):
delta_max = dP_max_rate * dt
P_ramped = clamp(P_cmd, P_prev - delta_max, P_prev + delta_max)
P_plant_max = min(P_site_export_limit, P_pv_avail + P_wind_avail, P_lim_dis)
P_plant_min = -P_lim_chg
P_limited = clamp(P_ramped, P_plant_min, P_plant_max)
return P_limited, P_ramped
Parameters:
| Symbol | Description | Default |
|---|---|---|
| $\dot{P}_{max}$ | Active power ramp rate (MW/s) | 0.1 |
| $\dot{Q}_{max}$ | Reactive power ramp rate (MVAr/s) | 0.1 |
4. PV-Wind-BESS Coordinator
def coordinator_step(P_plant_limited, P_pv_avail, P_wind_avail,
SoC, P_lim_chg, P_lim_dis):
"""
Returns P_bess, P_pv_sp, P_wind_sp (all in MW).
P_bess > 0 = Charge, < 0 = Discharge.
P_pv_sp, P_wind_sp >= 0 (generation setpoints).
"""
P_renewable_avail = P_pv_avail + P_wind_avail
# Compute initial BESS demand
P_bess_demand = P_plant_limited - P_renewable_avail
if P_bess_demand <= 0:
# Renewables alone are enough — possible surplus
P_pv_sp = P_pv_avail
P_wind_sp = P_wind_avail
P_surplus = -P_bess_demand
# Optionally charge BESS with surplus
if SoC < SoC_charge_trigger and P_surplus > 0:
P_bess = clamp(P_surplus, 0.0, P_lim_chg) # charge
else:
P_bess = 0.0
# Curtail renewables: actual plant output = P_plant_limited
P_curtail = P_renewable_avail - (P_plant_limited + P_bess)
P_curtail = max(0.0, P_curtail)
P_pv_sp -= P_curtail * pv_curtail_share
P_wind_sp -= P_curtail * (1.0 - pv_curtail_share)
else:
# Use all renewables + discharge BESS for deficit
P_pv_sp = P_pv_avail
P_wind_sp = P_wind_avail
P_bess = -P_bess_demand # discharge = negative
# Apply SoC safety limits
if SoC < SoC_discharge_minimum:
P_bess = max(P_bess, 0.0) # prevent discharge
if SoC > SoC_charge_disable:
P_bess = min(P_bess, 0.0) # prevent charge
# Apply BMS and PCS hardware limits
P_bess = clamp(P_bess, -P_lim_dis, +P_lim_chg)
P_bess = clamp(P_bess, -S_max_pcs, +S_max_pcs)
return P_bess, P_pv_sp, P_wind_sp
Parameters:
| Symbol | Description | Default |
|---|---|---|
SoC_charge_trigger |
BESS charges from surplus below this | 0.8 |
SoC_discharge_minimum |
Forced idle below this SoC | 0.1 |
SoC_charge_disable |
Charging blocked above this SoC | 0.95 |
pv_curtail_share |
Fraction of curtailment applied to PV | 0.5 |
S_max_pcs |
Max PCS apparent power (MW) | 1.0 |
5. Alarm and Event Manager
def alarm_manager_step(SoC, bms_alarms, f, breaker_closed,
pcc_data_age, asset_data_age):
alarms = {}
# Critical alarms → safe state
if bms_alarms.critical:
alarms["critical"] = True
alarms["source"] = "BMS"
if not breaker_closed:
alarms["critical"] = True
alarms["source"] = "Breaker_Open"
if pcc_data_age > pcc_timeout:
alarms["critical"] = True
alarms["source"] = "PCC_Comms_Loss"
if f < f_min or f > f_max:
alarms["critical"] = True
alarms["source"] = "Frequency_OOB"
# Comms loss → hold mode
if asset_data_age > comms_loss_timeout:
alarms["comms_loss"] = True
# Warnings
if SoC < SoC_discharge_minimum:
alarms["warning"] = "SoC_Low"
if SoC > SoC_charge_disable:
alarms["warning"] = "SoC_High"
return alarms
Alarm Threshold Parameters:
| Symbol | Description | Default |
|---|---|---|
pcc_timeout |
PCC data age limit (s) | 5.0 |
comms_loss_timeout |
Asset comms timeout (s) | 30.0 |
f_min |
Min grid frequency (Hz) | 49.0 |
f_max |
Max grid frequency (Hz) | 51.0 |
SoC_discharge_minimum |
SoC warning floor | 0.1 |
SoC_charge_disable |
SoC warning ceiling | 0.95 |
6. Interface Signal Definitions
| Interface | Signal Name | Type | Unit | Direction |
|---|---|---|---|---|
| IF-01 | P_pcc |
float | MW | PCC Meter → PCCLoop |
| IF-01 | Q_pcc |
float | MVAr | PCC Meter → PCCLoop |
| IF-01 | V_pcc |
float | kV | PCC Meter → PCCLoop |
| IF-01 | f |
float | Hz | PCC Meter → Alarm |
| IF-02 | SoC |
float | 0–1 | BMS → Coordinator |
| IF-02 | P_lim_chg |
float | MW | BMS → Ramp, Coordinator |
| IF-02 | P_lim_dis |
float | MW | BMS → Ramp, Coordinator |
| IF-03 | P_pv_avail |
float | MW | PV Measurements → Coordinator |
| IF-04 | P_wind_avail |
float | MW | Wind Measurements → Coordinator |
| IF-08 | P_bess_setpoint |
float | MW | Coordinator → PCS |
| IF-09 | P_pv_setpoint |
float | MW | Coordinator → PV Inverter |
| IF-10 | P_wind_setpoint |
float | MW | Coordinator → Wind Controller |