Distribution¶
The distribution layer ships two environments: DistGridEnv for balanced radial feeders and DistGrid3PhaseEnv for unbalanced three-phase feeders. Both are built around backward / forward sweep (BFS) power flow, which is a specialized solver for radial topologies. See Power systems primer for BFS, voltage limits, and VUF.
In plain language, this env lets an RL agent control batteries, PV, flexible loads, or abstract DER injections on a tree-shaped distribution network, then asks a simple question each step: after those actions, what do the bus voltages, line flows, and losses become, and did the agent keep the grid safe?
DistGridEnv — balanced radial feeder¶
DistGridEnv solves a single-phase backward / forward sweep on a radial network and treats the agent action as controllable active-power injection at non-slack buses.
Here, "radial feeder" means the distribution network is tree-shaped: power flows outward from one substation / slack bus, and each downstream bus has one electrical path back to the source. "Balanced" means the three phases are assumed similar enough that the feeder can be modeled with a single-phase equivalent, instead of tracking phase A / B / C separately.
State and parameters¶
classDiagram
class DistGridState {
+time_step done
+v_mag
+p_branch q_branch
+p_loss_total
+is_safe n_violations
+resource_states
+episode_batt_soc_init
}
class DistGridParams {
+case (CaseData)
+load_profiles_p load_profiles_q
+topo (BFSTopoData)
+line_cap
+base_mva
+include_der
+der_low der_high
+v_min v_max
+cost_mode (deprecated compat field)
+resources
}
include_der=True exposes an optional per-bus aggregated DER action channel in per-unit, meaning a dimensionless value normalized by a chosen base quantity. In practice, this lets the policy specify net active-power injection or withdrawal directly at each controllable bus. Here "DER" means a bus-level aggregated control port, not a specific device model. include_der=False removes that aggregated channel and leaves only concrete resource bundles, such as battery, PV, EV, or flexible-load models with their own state and constraints.
Action layout is [aggregated DER segment | bundle segments]. In the current benchmark tasks built on DistGridEnv such as DSO and DERs, include_der=False is the common setting, so the policy usually acts through attached bundles rather than direct per-bus injections.
Because DistGridEnv exposes one shared [-1, 1] action box for all attached resources, bundle-specific physical clipping still happens inside each bundle. In particular, the DSO task's FlexLoadBundle interprets each per-device control as a non-negative fraction and clips negative values to 0.
Step flow¶
flowchart LR
LD["load_profiles[t]"]
BUN["resource bundles<br/>step + scatter MW/MVAr"]
NET["per-unit conversion<br/>p_net = p_load - p_der - p_bundle/Sbase<br/>q_net = q_load - q_bundle/Sbase"]
BFS["balanced BFS solver"]
SAFE["voltage + thermal check"]
REW["reward = -w_loss * P_loss_MW<br/>costs = (voltage, thermal, resource)"]
LD --> NET
BUN --> NET
NET --> BFS --> SAFE --> REW
Net-load construction¶
Net per-unit injection at each bus combines load, optional per-bus DER action when include_der=True, and bundle injections (after converting to per unit by dividing by base_mva). In other words, p_net and q_net are the final per-bus net injections that the BFS solver actually sees:
Bundle MW/MVAr is positive injection, so it reduces net load.
Constraints and reward¶
Voltage and thermal violations are tracked separately:
- voltage: count-based, meaning each violating bus contributes one count when
vm < v_minorvm > v_max. - thermal: count-based, meaning each overloaded line contributes one count when apparent power
sqrt(P^2 + Q^2)exceedsline_cap. info["cost_continuous"]stores distance-style severity diagnostics rather than simple violation counts.
The reward is loss-based only:
The core env always exposes the full fixed-shape CMDP vector:
with static names ("voltage_violation", "thermal_overload", "resource"). info["cost_sum"] is the sum of those reported cost components and is provided as a convenience diagnostic.
Cost channel definitions:
| Symbol | Constraint name | Info key | Meaning |
|---|---|---|---|
| \(C_{\mathrm{v}}\) | voltage_violation |
cost_voltage_violation |
Count of buses outside [v_min, v_max]. |
| \(C_{\mathrm{th}}\) | thermal_overload |
cost_thermal_overload |
Count of branches whose apparent power exceeds their rating. |
| \(C_{\mathrm{res}}\) | resource |
cost_resource |
Sum of cost_sum reported by attached resource bundles. |
The DSO benchmark does not change the env-level semantics. Instead, its task layer selects only "voltage_violation" for the CMDP constraint spec. The legacy cost_mode field is kept only so old configs can still load; readers can ignore it because it no longer changes the core env output.
That selected voltage channel is count-based: each violating bus contributes to \(C_{\mathrm{v}}\), rather than the task using a continuous distance-from-limit penalty as its benchmark cost.
Note
In the current implementation, info["soc_terminal_sq"] only tracks the first attached bundle that exposes an soc field. If that first bundle is a battery bundle, the env reports the squared deviation between episode-start SOC and terminal SOC on the terminal transition; it is zero on non-terminal steps. If a battery appears only in the second or later bundle position, this diagnostic is not triggered.
Observation Vector¶
obs = [v_mag_norm | p_branch_norm | q_branch_norm | p_load_norm | q_load_norm | sin(t) | cos(t) | <bundle_obs>].
v_mag_norm: per-bus voltage magnitude, normalized as \((v_{\text{mag}} - 1.0) / 0.1\)p_branch_norm: per-branch active-power flow, normalized as \(p_{\text{branch}} / p_{\text{load,ref}}\)q_branch_norm: per-branch reactive-power flow, normalized as \(q_{\text{branch}} / q_{\text{load,ref}}\)p_load_norm: current per-bus active load, normalized as \(p_{\text{load}} / p_{\text{load,ref}}\)q_load_norm: current per-bus reactive load, normalized as \(q_{\text{load}} / q_{\text{load,ref}}\)sin(t),cos(t): time-of-day phase features, giving the policy a periodic encoding of the current step.<bundle_obs>: appended resource summaries from attached bundles.
Here, p_load_ref and q_load_ref are just normalization reference scales for active and reactive load. They are not additional physical state variables.
For the DSO benchmark, <bundle_obs> is the FlexLoadBundle slice with 6 devices x 5 features in device-major order. "Device-major" means the five features for device 1 come first, then the five features for device 2, and so on:
curtail_norm: current-step curtailment divided by that device'scurtail_cap_mw.shift_out_norm: current-step deferred demand divided by that device'sshift_cap_mw.shift_in_norm: current-step released deferred demand divided by that device'sshift_cap_mw.buffer_fill_ratio: deferred-demand buffer occupancy relative toshift_horizon.buffer_energy_norm: total deferred energy in the buffer, normalized byshift_cap_mw * shift_horizon.
Unlike standalone FlexLoadEnv, the bundle observation does not carry time or price channels; those are already provided by the parent DistGridEnv.
DistGrid3PhaseEnv — unbalanced three-phase feeder¶
DistGrid3PhaseEnv extends the radial model to unbalanced three-phase networks. The agent controls per-phase active injections on non-reference buses, and the env solves a three-phase BFS with per-phase voltages and branch flows.
Here, "unbalanced three-phase" means the feeder still has radial topology, but phase A / B / C are no longer assumed identical. Loads, line impedances, and controllable injections may differ by phase, so the env keeps per-phase voltages, flows, and unbalance metrics such as VUF instead of collapsing the feeder into a single-phase equivalent.
State and parameters¶
classDiagram
class DistGrid3PhState {
+time_step done
+v_mag
+V_real V_imag
+P_branch Q_branch (per phase)
+p_loss_total
+resource_states
}
class DistGrid3PhParams {
+load_P_3ph load_Q_3ph
+topo (ThreePhaseTopoData)
+line_cap
+base_mva
+ref_bus v_ref_mag
+vuf_max
+include_der
+v_min v_max
+resources
}
Modeling choices¶
- Direct DER actions are per-phase and expressed in per unit.
- Resource bundles are scattered as balanced three-phase injections: each device contributes
P/3andQ/3to phases A, B, and C at its bus. - Thermal checks use per-phase apparent power, reconstructed from branch current and both endpoint voltages. The aggregate line capacity is split across the energized phases.
Action layout is [aggregated per-phase DER segment | bundle segments]. The aggregated per-phase DER segment is still a direct bus/phase-level control port, while the bundles remain concrete device models. In the current three-phase DERs eval path, include_der=False is typically used as well, so policies usually control attached bundles while the three-phase env still computes voltage, power flow, and imbalance separately for phases A / B / C.
VUF constraint¶
Voltage unbalance is tracked with the Fortescue-based voltage unbalance factor (VUF). "Fortescue-based" means the three-phase voltages are first decomposed into sequence components before measuring imbalance:
\(V_{\text{pos}}\) is the balanced positive-sequence component, while \(V_{\text{neg}}\) measures the unbalanced part that rotates in the opposite sequence. Constraint: \(\mathrm{VUF} \le vuf_{\max}\).
Cost composition¶
The static names are ("voltage_violation", "thermal_overload", "vuf_violation", "resource"). info["max_vuf_percent"] keeps the worst per-node VUF as a diagnostic. Reward stays loss-based:
Cost channel definitions:
| Symbol | Constraint name | Info key | Meaning |
|---|---|---|---|
| \(C_{\mathrm{v}}\) | voltage_violation |
cost_voltage_violation |
Count of phase voltages outside the allowed voltage band. |
| \(C_{\mathrm{th}}\) | thermal_overload |
cost_thermal_overload |
Count of phase/branch apparent-power overloads. |
| \(C_{\mathrm{vuf}}\) | vuf_violation |
cost_vuf_violation |
Count of nodes whose voltage-unbalance factor exceeds vuf_max_percent. |
| \(C_{\mathrm{res}}\) | resource |
cost_resource |
Sum of cost_sum reported by attached resource bundles. |
Observation vector¶
The observation vector is phase-separated and can be read as five groups:
- per-phase bus voltages: voltage magnitudes for phases A / B / C at each bus.
- per-phase branch active power: branch flows
P_A/B/C. - per-phase branch reactive power: branch flows
Q_A/B/C. - per-phase loads: the load level on each phase at each bus.
- auxiliary features: time features and optional bundle observations.
The point of this layout is to let the policy see how imbalance appears differently across phases, rather than collapsing everything into a single-phase equivalent summary.
Note that the stored P_A/B/C and Q_A/B/C branch channels are mainly solver diagnostics exposed through the observation vector. Thermal enforcement does not rely directly on those diagnostic channels; it uses the endpoint-voltage-and-current reconstruction described above to compute per-phase apparent power.
When to use which¶
- Use
DistGridEnvwhen the task is balanced radial control (DSO, single-phase DER coordination). - Use
DistGrid3PhaseEnvwhen the benchmark goal includes phase imbalance, VUF, or per-phase device placement.
The DSO benchmark builds on DistGridEnv. The DERs benchmark also builds on DistGridEnv (with 12 heterogeneous bundles) and provides a 3-phase OOD eval mode through DistGrid3PhaseMARLEnv. See Benchmarks → DSO and Benchmarks → DERs.
Cross references¶
- Resources — bundles attached at distribution buses.
- API → Distribution and API → Grid (shared) for the BFS solver helpers.