Skip to content

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