GenCos market MARL¶
Public API for the GenCos benchmark: a 5-agent rolling competitive market with exact PD-IPM clearing and ramp coupling. For physics, see Physics → Markets. For the benchmark task definition, see Benchmarks → GenCos.
Pure-functional core¶
from powerzoojax.envs import (
MarketMARLState,
MarketMARLParams,
make_market_marl_params,
market_marl_reset,
market_marl_step,
)
These are JIT-compatible pure functions. market_marl_reset samples a random episode start index from the load-profile pool; market_marl_step enforces ramp bounds at the LP level via offer_sced.
powerzoojax.envs.market.market_marl_core.MarketMARLState
¶
Bases: MarketState
Market MARL environment state.
Extends MarketState with per-unit dispatch profit from the last step, a fixed-length circular buffer of mean-LMP history for observations, and an episode start index for sampling diverse 48-step windows from a large profile pool.
episode_start_idx
Sampled uniformly from [0, T-1] at each reset (where T is the number of rows in load_profiles). The actual profile row used at step t is (episode_start_idx + t) % T. This gives diverse temporal coverage across training episodes when load_profiles is a large GB dataset pool.
powerzoojax.envs.market.market_marl_core.MarketMARLParams
¶
Bases: MarketParams
Market MARL environment parameters.
Extends MarketParams with GenCos-specific fields.
powerzoojax.envs.market.market_marl_core.make_market_marl_params(case, load_profiles, *, n_segments=3, max_markup=2.0, max_steps=48, delta_t_hours=0.5, steps_per_day=48, lmp_scale=100.0, cost_thermal_weight=1.0, lmp_history_len=4, ramp_up_mw_per_step=None, ramp_down_mw_per_step=None, ramp_rate_fraction=0.5)
¶
Build MarketMARLParams from a CaseData and load profile array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
case
|
CaseData
|
CaseData instance (e.g. from create_case5()). |
required |
load_profiles
|
(T, n_loads) array [MW] — absolute MW values. |
required | |
n_segments
|
int
|
Number of offer-curve segments per generator. |
3
|
max_markup
|
float
|
Maximum fractional markup (offer = base * (1 + m * max_markup)). |
2.0
|
max_steps
|
int
|
Episode length in steps. |
48
|
delta_t_hours
|
float
|
Time resolution [hours]. |
0.5
|
steps_per_day
|
int
|
Steps per day (for sin/cos obs features). |
48
|
lmp_scale
|
float
|
LMP normalisation scale [$/MWh] for observations. |
100.0
|
cost_thermal_weight
|
float
|
Weight on thermal overload cost signal. |
1.0
|
lmp_history_len
|
int
|
Number of past mean-LMP values in the obs buffer. |
4
|
ramp_up_mw_per_step
|
Optional[ndarray]
|
(n_units,) MW — per-step ramp-up limit. If None, defaults to ramp_rate_fraction × p_max. |
None
|
ramp_down_mw_per_step
|
Optional[ndarray]
|
(n_units,) MW — per-step ramp-down limit. If None, defaults to ramp_rate_fraction × p_max. |
None
|
ramp_rate_fraction
|
float
|
Fraction of p_max used as default ramp rate when ramp_up/down_mw_per_step are not provided. Default 0.5 (50% of p_max per step, i.e. 100% per hour at 30-min resolution). |
0.5
|
Returns:
| Type | Description |
|---|---|
MarketMARLParams
|
MarketMARLParams. |
powerzoojax.envs.market.market_marl_core.market_marl_reset(key, params)
¶
Reset the market MARL environment.
Samples a random episode start index from [0, T-1] where T is the number of rows in load_profiles. Each episode then accesses profile rows (episode_start_idx + t) % T for t=0..max_steps-1, giving diverse temporal windows across training episodes when load_profiles is a large pool (e.g. a full year of GB half-hourly demand data).
Runs the exact SCED with truthful prices at the sampled start row to initialise the unit dispatch and LMP history.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
PRNGKey
|
PRNG key used to sample episode_start_idx. |
required |
params
|
MarketMARLParams
|
MarketMARLParams. |
required |
Returns:
| Type | Description |
|---|---|
MarketMARLState
|
MarketMARLState at t=0 with sampled start index, truthful dispatch |
MarketMARLState
|
and initialised LMP history. |
powerzoojax.envs.market.market_marl_core.market_marl_step(key, state, flat_actions, params)
¶
Single-step market MARL transition with ramp-bounded rolling market.
Action mapping
flat_actions ∈ [-1, 1]^{n_units * n_seg} m = (a + 1) / 2 ∈ [0, 1] sorted_m = sort(m, axis=-1) (enforce monotone offers) offer_prices = base_seg_prices * (1 + sorted_m * max_markup)
Ramp coupling
p_min_rt[i] = max(p_min[i], prev_dispatch[i] - ramp_down[i]) p_max_rt[i] = min(p_max[i], prev_dispatch[i] + ramp_up[i]) offer_sced is called with p_min_rt / p_max_rt to enforce these bounds in the LP itself (solver-layer enforcement, not post-hoc clipping).
Reward: dispatch_profit_i = LMP[node_i] * P_i * dt − TC(P_i) * dt where TC(P) = (a/3) P³ + (b/2) P² + c P.
Auto-reset: when done, state is replaced with reset state (stop_gradient).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
PRNGKey
|
PRNG key (split for auto-reset). |
required |
state
|
MarketMARLState
|
Current MarketMARLState. |
required |
flat_actions
|
Array
|
(n_units * n_seg,) flattened actions in [-1, 1]. |
required |
params
|
MarketMARLParams
|
MarketMARLParams. |
required |
Returns:
| Type | Description |
|---|---|
|
(final_state, done, reward_vec, info) |
|
|
where reward_vec is (n_units,) dispatch profit per agent. |
MARL adapter¶
MarketMARLEnv wraps the pure core with the per-agent dict layout used by IPPO trainers. Agents are named genco_i, one per generator. The wrapper does not duplicate physics — it only handles action / observation splitting.
Private observation layout per agent is 8 + lmp_history_len:
- normalized first-segment base price
- normalized own
p_max - normalized own last dispatch
- normalized own last dispatch profit
- normalized own ramp-up headroom
- normalized one-step-ahead total-load forecast
sin(t),cos(t)- mean-LMP history
The wrapper returns per-agent reward and done dictionaries. The underlying pure core returns (final_state, done, reward_vec, info) and keeps market safety diagnostics such as cost_thermal_overload in info.
powerzoojax.rl.market_marl.MarketMARLEnv(params)
¶
Bases: MultiAgentEnvironment
GenCos bid-based market — multi-agent RL environment.
Agents are generator companies (GenCos). Each step: 1. Each agent submits n_seg-dimensional bid (markup levels). 2. Exact SCED clears the market with ramp-bounded dispatch bounds. 3. Each agent receives dispatch profit as reward.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
MarketMARLParams
|
MarketMARLParams (from make_market_marl_params). |
required |
Usage::
from powerzoojax.case import create_case5
from powerzoojax.envs.market.market_marl_core import make_market_marl_params
from powerzoojax.rl.market_marl import MarketMARLEnv
import jax.numpy as jnp
case = create_case5()
mid_load = (case.load_d_max + case.load_d_min) / 2.0
profiles = jnp.tile(mid_load[None, :], (48, 1))
params = make_market_marl_params(case, profiles, n_segments=3)
env = MarketMARLEnv(params)
key = jax.random.PRNGKey(0)
obs, state = env.reset(key)
obs2, state2, rew, dones, info = env.step(key, state, {n: jnp.zeros(3) for n in env.agent_names})
observation_space(agent=None)
¶
Private observation space per agent: shape (self._obs_dim,).
Dimension = 8 scalar features + params.lmp_history_len LMP history values. For the benchmark default (lmp_history_len=4) this equals OBS_DIM=12.
[own_cost_b, own_p_max, own_dispatch, own_profit,
own_ramp_headroom, demand_forecast_t+1, sin(t), cos(t), lmp_hist[0..lmp_history_len-1]]
action_space(agent=None)
¶
n_seg-dimensional action space in [-1, 1] (same for all agents).
compute_market_metrics(rollout_market_info)
¶
Compute market-specific metrics from collected rollout info.
Called by make_ippo_train after each training update when this env is detected. rollout_market_info values have shapes:
lmp : (n_steps, n_envs, n_nodes)
unit_power : (n_steps, n_envs, n_units)
ramp_binding : (n_steps, n_envs) — fraction of binding units
Implemented metrics (all keys prefixed "market/"):
price_volatility — mean CV (std/mean) of per-env LMP time series
HHI — mean Herfindahl-Hirschman Index across steps/envs
market_share_std — std of time-averaged dispatch shares across agents
(summary statistic accompanying dispatch_share_{ag})
ramp_binding_rate — mean fraction of ramp-constrained units per step
dispatch_share_{ag} — per-agent time-averaged dispatch share (values ≈ 1/n
for equal share; sum ≈ 1.0 across agents).
Together these form the market_share_dynamics
time-series signal: one value per agent per training
update, suitable for plotting dispatch evolution.
planner_cost_ratio — planner_cost / RL_gen_cost ∈ (0, 1].
Planner = ramp-free DC-OPF with true costs
(offer_sced with base_seg_prices, no ramp bounds).
Measures dispatch cost efficiency relative to the
cost-minimising planner. A value close to 1.0
means RL dispatch is nearly as cheap as the planner;
lower values indicate higher RL generation cost.
NOTE: this is a COST RATIO, not a welfare ratio.
True social_welfare_ratio = (V − RL_cost) /
(V − planner_cost) where V is consumer value
(VOLL × demand). V is not defined in this codebase,
so a proper welfare ratio cannot be computed without
an arbitrary consumer-side assumption. This metric
is therefore not a social-welfare ratio.
Requires rollout_market_info to contain load_mw
(n_steps, n_envs, n_nodes) and gen_cost
(n_steps, n_envs); omitted silently if absent
(backward-compatible with old IPPO versions).
NOT implemented (explicit future work, G5 partial — not claimed as done):
social_welfare_ratio — Requires consumer value V (VOLL ×
demand) for the ratio (V − RL_cost) / (V − planner_cost)
to be well-defined and ∈ [0, 1]. V is absent from
the current codebase; any choice would be arbitrary.
exploitability_proxy — requires a best-response oracle or self-play;
a price-taking approximation would conflate dispatch
suboptimality with strategic effect.
collusion_indicator — requires knowing the single-agent optimal markup,
which is undefined without a Nash equilibrium solver.
convergence_stability — implemented separately in the IPPO training loop
(ippo.py), NOT computable from rollout data alone;
exported as market/convergence_stability.
Usage example¶
import jax
import jax.numpy as jnp
from powerzoojax.case import load_case
from powerzoojax.envs import make_market_marl_params, market_marl_reset, market_marl_step
from powerzoojax.rl import MarketMARLEnv
case = load_case("5")
profiles = jnp.full((48, case.n_loads), 100.0, dtype=jnp.float32)
params = make_market_marl_params(case, profiles, n_segments=3, max_steps=48)
# 1. functional core (no per-agent layout)
state = market_marl_reset(jax.random.PRNGKey(0), params)
flat_actions = jnp.zeros(case.n_units * 3)
state, done, reward_vec, info = market_marl_step(
jax.random.PRNGKey(1), state, flat_actions, params
)
# 2. MARL wrapper (per-agent dict)
env = MarketMARLEnv(params)
obs_dict, state = env.reset(jax.random.PRNGKey(2))
action_dict = {f"genco_{i}": jnp.zeros(3) for i in range(case.n_units)}
obs_dict, state, reward_dict, done_dict, info = env.step(
jax.random.PRNGKey(3), state, action_dict
)