Dispatchables and Dispatch Policies
A Dispatchable is a controllable energy resource (a battery, diesel/gas generator, or electrolyzer) whose power setpoint is decided each step by a DispatchPolicy.
Unlike actors (whose power comes from a signal), a dispatchable's power is allocated by the policy from the microgrid's current power delta.
Each Dispatchable reports a feasible range (min_power, max_power) for the upcoming timestep. Negative values represent discharging/generation; positive values represent charging/consumption.
Battery models
SimpleBattery
SimpleBattery is an ideal, capacity-based model which is fast and easy to parameterize. It is a sensible default for simple simulations:
import vessim as vs
battery = vs.SimpleBattery(
name="battery",
capacity=5000, # Wh (total energy capacity
initial_soc=0.8, # 80 % charged at simulation start
min_soc=0.2, # never discharge below 20 % SoC
c_rate=0.5, # optional: cap charge/discharge rate at 0.5C
)
c_rate is the maximum charge/discharge rate as a fraction of capacity per hour. A 1 kWh battery with c_rate=0.5 can charge or discharge at most 500 W. If omitted, the only limit is the available energy.
ClcBattery
ClcBattery implements the C-L-C model for lithium-ion batteries (Kazhamiaka et al., 2019). It captures realistic degradation of charge/discharge limits as a function of current and is appropriate for high-fidelity studies. The default parameterization models a pack of LGM50 21700 Li-ion cells:
battery = vs.ClcBattery(
name="battery",
number_of_cells=100,
initial_soc=0.5,
min_soc=0.1,
)
Note
ClcBattery should not be used with large simulation step sizes. For coarse-grained simulations (15 min or more), prefer SimpleBattery.
Multiple dispatchables
add_microgrid accepts a list of dispatchables. The default policy serves them in order, so the first one is used until it is exhausted before the next one is touched:
environment.add_microgrid(
name="datacenter",
actors=[...],
dispatchables=[
vs.SimpleBattery(name="primary", capacity=10000, initial_soc=0.9),
vs.SimpleBattery(name="backup", capacity=5000, initial_soc=0.5),
],
)
Dispatch policies
A DispatchPolicy receives the microgrid's power delta and allocates it across dispatchables. It returns the power that needs to be exchanged with the public grid.
DefaultDispatchPolicy
DefaultDispatchPolicy is used when no policy argument is given to add_microgrid. It supports three modes:
Grid-connected (default): allocate the delta in priority order, exchange the remainder with the grid.
policy = vs.DefaultDispatchPolicy(mode="grid-connected")
Islanded: fully disconnect the microgrid from the grid. If dispatchables cannot cover a deficit, a RuntimeError is raised. Use this to verify a self-sufficient design.
policy = vs.DefaultDispatchPolicy(mode="islanded")
Fixed charge power: force storage to charge or discharge at a constant rate, balancing the difference with the grid. Useful for scheduled charging strategies; only works in grid-connected mode.
policy = vs.DefaultDispatchPolicy(charge_power=200.0) # charge at 200 W
policy = vs.DefaultDispatchPolicy(charge_power=-500.0) # discharge at 500 W
Custom dispatchables
To model hardware that does not fit a battery model, subclass Dispatchable and implement four methods:
| Method | Description |
|---|---|
feasible_range(duration) |
Return (min_power, max_power) achievable for the timestep. |
step(duration) |
Advance internal state after current_power has been set. |
config() |
Return static parameters (logged once to metadata.yaml). |
state() |
Return dynamic state (logged each step to timeseries.csv). |
A diesel generator is stateless and only generates (negative power):
class DieselGenerator(vs.Dispatchable):
def __init__(self, name: str, max_power_w: float):
super().__init__(name)
self.max_power_w = max_power_w
def feasible_range(self, duration: int) -> tuple[float, float]:
return (-self.max_power_w, 0.0) # negative = generation
def step(self, duration: int) -> None:
pass # no internal state
def config(self) -> dict:
return {"max_power_w": self.max_power_w}
def state(self) -> dict:
return {"current_power": self.current_power}
Combined with a battery, the default policy will exhaust the battery first and only call on the generator for the remaining deficit:
environment.add_microgrid(
name="datacenter",
actors=[...],
dispatchables=[
vs.SimpleBattery(name="battery", capacity=10000, initial_soc=0.9),
DieselGenerator(name="generator", max_power_w=5000),
],
)
Custom dispatch policies
For advanced energy management, subclass DispatchPolicy and implement apply. The microgrid's grid signals are passed in directly, so the policy can be carbon- or price-aware without extra wiring:
class GreenChargePolicy(vs.DispatchPolicy):
"""Only charge batteries when grid carbon intensity is low."""
def __init__(self, carbon_threshold: float):
self.carbon_threshold = carbon_threshold
def apply(self, p_delta, duration, dispatchables, grid_signals=None):
remaining = p_delta
carbon = (grid_signals or {}).get("carbon_intensity")
for d in dispatchables:
lo, hi = d.feasible_range(duration)
if remaining > 0:
# Excess: only charge if grid is clean
if carbon is not None and carbon > self.carbon_threshold:
d.set_power(0.0, duration)
continue
allocated = min(hi, remaining)
else:
# Deficit: always discharge
allocated = max(lo, remaining)
d.set_power(allocated, duration)
remaining -= allocated
return remaining # remainder goes to the grid
environment.add_microgrid(
name="datacenter",
actors=[...],
dispatchables=[vs.SimpleBattery(name="battery", capacity=5000, initial_soc=0.8)],
policy=GreenChargePolicy(carbon_threshold=200),
grid_signals={"carbon_intensity": vs.Trace.from_csv("datasets/watttime_example.csv")},
)