Updated the parameter names contained within the matlab controller david1606

TBD: parameters outside the controller, parameters contained on the old parameter doc
This commit is contained in:
robert 2026-09-11 13:05:38 +00:00
parent 72dd781dbc
commit 702647d84e
15 changed files with 2485 additions and 5 deletions

11
.gitignore vendored
View file

@ -23,3 +23,14 @@ slprj/
# Personal data — recruitment notes on named candidates. Kept locally, never committed.
# Root-anchored so it cannot catch the generic question template in docs/.
/Interview Questions-*.md
# Python — the shared environment is rebuilt from uv.lock with `uv sync`; never commit it
.venv/
__pycache__/
*.pyc
.pytest_cache/
*.egg-info/
# Experiment logs and local data downloads
wandb/
**/data/raw/

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12

66
AICONTROL/README.md Normal file
View file

@ -0,0 +1,66 @@
# AICONTROL — the AI & Control cluster's folder
Everything our cluster writes lives here: the placeholder environment, the forecasting
harness, the training rig, the evaluation pipeline, the safety layer and the dashboard. The
plan we work from is [`docs/01-project/ai-control-cluster-plan-2026-2027.md`](../docs/01-project/ai-control-cluster-plan-2026-2027.md).
## Clone and run
The whole team shares one Python environment, pinned at the repository root. You need Python
3.12 and [uv](https://docs.astral.sh/uv/) (one-off: `pip install uv`).
```bash
git clone https://git.teamshiftenergy.com/pepe/ALLSHIFT.git
cd ALLSHIFT
uv sync # first time: creates .venv/ with everyone's tools, a few minutes
uv run pytest # runs our tests; all green means your setup works
```
`uv sync` reads `uv.lock`, so everyone gets exactly the same versions. Never commit `.venv/`.
If you add a package, add it to the root `pyproject.toml`, run `uv lock`, and commit the
updated `uv.lock` with your change.
## What is where
```
AICONTROL/
├── aicontrol/ ← Python package (import aicontrol)
│ └── env/
│ └── spaces.py ← what the agent sees and controls, built from configs/env.yaml
├── configs/
│ └── env.yaml ← plant sizes, observation ranges, forecast layout, PLACEHOLDER reward weights
├── docs/
│ └── interface-draft.md← the Simulations → AI handover, drafted for the November session
├── tests/ ← pytest; run from the repository root with `uv run pytest`
└── README.md
```
Folders that will appear as the work does: `env/placeholder.py` (the placeholder
environment), `forecast/`, `train/`, `evaluate/`, `safety/`, `dashboard/`.
## Who does what
| Person | Seats |
|---|---|
| Lead | RL Environment + Safety |
| Person 2 | RL Training |
| Person 3 | RL Evaluation + Explainability & Dashboard |
| Person 4 | Forecasting |
## Q1 goal — what runs on Friday 16 October
1. The placeholder environment, with a determinism test and an energy test.
2. A training script (PPO or SAC on the placeholder, three seeds, logged to Weights & Biases).
3. The results pipeline: any controller × any scenario → one row; the comparison table; the
column list for Business. Controller names reserved: `rule_based`, `mpc`,
`perfect_knowledge`, `agent`.
4. The forecasting harness with "same as yesterday" baselines and a skill score.
5. The interface document, ready for the joint session with Simulations.
## Rules we keep
- Never commit `.venv/`, Weights & Biases run folders, or raw data downloads (the `.gitignore`
covers them).
- Machine-made tables are Parquet; hand-written settings are YAML; documents are Markdown.
- The placeholder environment never gets better physics. The day the twin runs, we point at it
and delete the placeholder.

View file

@ -0,0 +1,3 @@
"""AI & Control cluster package for Team SHIFT."""
__version__ = "0.1.0"

1
AICONTROL/aicontrol/env/__init__.py vendored Normal file
View file

@ -0,0 +1 @@
"""The environment side: spaces now, the placeholder environment next week."""

147
AICONTROL/aicontrol/env/spaces.py vendored Normal file
View file

@ -0,0 +1,147 @@
"""Observation and action spaces for the SHIFT hospital microgrid: the sketch.
Week-1 work for the RL Environment seat. This fixes the *shape* of what the agent sees and
what it controls, built from ``configs/env.yaml``, so that the placeholder environment (next
week) and later the real twin from Simulations both expose exactly this interface.
Conventions
-----------
* One observation is one flat float32 vector. Every slot has a name, a unit and a range.
* One action is four floats: electrolyser, fuel cell, battery, shedding level.
* Names follow the Simulator I/O sheet where it has one (``P_PV``, ``SoC``, ``H2_level``,
``p_tank``, ``price``, ``CO2_int``, ``grid_on``); the Project Manual's interface list
decides what is in and what is out.
* Ranges are bounds for scaling and sanity checks, not physical guarantees.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
import yaml
from gymnasium import spaces
DEFAULT_CONFIG = Path(__file__).resolve().parents[2] / "configs" / "env.yaml"
# Shedding levels, as the manual lists them.
SHEDDING_NONE = 0 # nothing shed
SHEDDING_TIER3 = 1 # Tier 3 shed
SHEDDING_TIERS_2_3 = 2 # Tiers 2 and 3 shed; Tier 1 is never shed
ACTION_NAMES = ("u_ele", "u_fc", "u_batt", "shed")
@dataclass(frozen=True)
class Slot:
"""One named block of the observation vector."""
name: str
unit: str
low: float
high: float
size: int = 1
source: str = "" # where the value comes from: twin state, data, calendar, forecaster
def load_config(path: str | Path | None = None) -> dict[str, Any]:
"""Read the environment settings (defaults to ``AICONTROL/configs/env.yaml``)."""
with open(path or DEFAULT_CONFIG, encoding="utf-8") as f:
return yaml.safe_load(f)
def observation_slots(cfg: dict[str, Any]) -> list[Slot]:
"""The observation layout, in order. Change the config, not this list, to resize things."""
plant = cfg["plant"]
obs = cfg["observation"]
fc = cfg["forecast"]
price_low, price_high = obs["price_range"]
co2_low, co2_high = obs["co2_range"]
slots = [
Slot("time_of_day", "sin, cos", -1.0, 1.0, 2, "calendar"),
Slot("time_of_year", "sin, cos", -1.0, 1.0, 2, "calendar"),
Slot("P_PV", "kW", 0.0, plant["pv_kw"], 1, "twin: solar output now"),
Slot("L_tier1", "kW", 0.0, plant["load_max_kw"], 1, "data: critical load now"),
Slot("L_tier2", "kW", 0.0, plant["load_max_kw"], 1, "data: essential load now"),
Slot("L_tier3", "kW", 0.0, plant["load_max_kw"], 1, "data: non-critical load now"),
Slot("SoC", "fraction 0-1", 0.0, 1.0, 1, "twin: battery state of charge"),
Slot("H2_level", "kg", 0.0, plant["h2_capacity_kg"], 1, "twin: hydrogen in the tank"),
Slot("p_tank", "bar", 0.0, plant["tank_p_max_bar"], 1, "twin: tank pressure"),
Slot("price", "currency/kWh", price_low, price_high, 1, "data: electricity price now"),
Slot("CO2_int", "kg CO2/kWh", co2_low, co2_high, 1, "data: grid carbon intensity now"),
Slot("grid_on", "0/1", 0.0, 1.0, 1, "data: grid available"),
Slot("ele_on", "0/1", 0.0, 1.0, 1, "twin: electrolyser running"),
Slot("fc_on", "0/1", 0.0, 1.0, 1, "twin: fuel cell running"),
]
quantiles = ", ".join(fc["quantiles"])
for quantity in fc["quantities"]:
high = plant["pv_kw"] if quantity == "solar" else plant["load_max_kw"]
for horizon in fc["horizons_min"]:
slots.append(
Slot(
f"fcst_{quantity}_{horizon}min",
"kW",
0.0,
high,
len(fc["quantiles"]),
f"forecaster: {quantiles} at +{horizon} min",
)
)
return slots
def observation_size(cfg: dict[str, Any]) -> int:
return sum(slot.size for slot in observation_slots(cfg))
def build_observation_space(cfg: dict[str, Any]) -> spaces.Box:
slots = observation_slots(cfg)
low = np.concatenate([np.full(s.size, s.low, dtype=np.float32) for s in slots])
high = np.concatenate([np.full(s.size, s.high, dtype=np.float32) for s in slots])
return spaces.Box(low=low, high=high, dtype=np.float32)
def build_action_space(cfg: dict[str, Any]) -> spaces.Box:
"""Four floats.
``u_ele`` in [0, 1]: fraction of electrolyser rated power (0 = off)
``u_fc`` in [0, 1]: fraction of fuel cell rated power (0 = off)
``u_batt`` in [-1, 1]: fraction of battery max power; positive discharges, negative charges
``shed`` in [0, L-1]: rounded to a shedding level (0 nothing, 1 Tier 3, 2 Tiers 2 and 3)
Stable-Baselines3 algorithms take either all-continuous or all-discrete actions, so the
three-way shedding choice rides along as a continuous number and is rounded inside the
environment. To confirm at the joint session with Simulations.
"""
levels = cfg["action"]["shedding_levels"]
low = np.array([0.0, 0.0, -1.0, 0.0], dtype=np.float32)
high = np.array([1.0, 1.0, 1.0, float(levels - 1)], dtype=np.float32)
return spaces.Box(low=low, high=high, dtype=np.float32)
def shedding_level(action: np.ndarray, cfg: dict[str, Any]) -> int:
"""Turn the fourth action number into a shedding level, clipped to the allowed ones."""
levels = cfg["action"]["shedding_levels"]
return int(np.clip(np.rint(action[3]), 0, levels - 1))
def describe(cfg: dict[str, Any]) -> str:
"""Markdown table of the observation layout, for the interface document."""
lines = ["| # | name | size | unit | range | source |", "|---|---|---|---|---|---|"]
index = 0
for s in observation_slots(cfg):
lines.append(
f"| {index} | `{s.name}` | {s.size} | {s.unit} | {s.low:g}{s.high:g} | {s.source} |"
)
index += s.size
return "\n".join(lines)
if __name__ == "__main__":
config = load_config()
print(describe(config))
print(f"\nobservation size: {observation_size(config)}")
print(f"action space: {build_action_space(config)}")

View file

@ -0,0 +1,40 @@
# Environment settings — the shape of the Simulations → AI handover.
#
# This file feeds aicontrol/env/spaces.py now and the placeholder environment next week.
# Values marked PLACEHOLDER are ours until: the joint session with Simulations (November)
# for sizes, ranges and the forecast layout; the reward-weight conversation with Energy
# Management and Business (Q2) for the weights. Same YAML style as Simulations' scenario
# files, to be aligned with their core developer.
step_minutes: 5 # the manual's timestep; 105,120 steps per year
plant: # PLACEHOLDER: the Rijnstate-type pilot plant from the cost register
pv_kw: 460 # 2,300 m² of panels
electrolyser_kw: 118 # H2B2 EL20N datasheet, found in the live Simulink model
fuel_cell_kw: 100
battery_kwh: 500
battery_kw: 250
h2_capacity_kg: 200
tank_p_max_bar: 30 # 30 bar storage, from the cost register
load_max_kw: 1000 # upper bound for scaling load observations, PLACEHOLDER
observation:
price_range: [-1.0, 5.0] # currency per kWh; negative prices happen
co2_range: [0.0, 1.5] # kg CO2 per kWh
forecast: # the block of forecasts the agent sees; layout to agree with Simulations
quantities: [solar, tier1, tier2, tier3]
horizons_min: [60, 180, 360, 1440]
quantiles: [p10, p50, p90] # best / likely / worst, as the manual asks
action:
shedding_levels: 3 # 0 nothing, 1 Tier 3, 2 Tiers 2 and 3
reward: # PLACEHOLDER weights — not ours to set; a proposal for the Q2 conversation
cost_per_currency: -1.0 # energy cost this step
co2_per_kg: -0.05 # emissions this step
tier1_unserved_per_kwh: -1000.0 # must dominate everything else
tier2_shed_per_kwh: -2.0
tier3_shed_per_kwh: -0.5
limit_violation: -10.0 # battery or tank pushed past a safe limit
switching: -0.1 # equipment switched on or off this step

View file

@ -0,0 +1,103 @@
# The Simulations → AI interface — draft for the November session
| | |
|---|---|
| **What this is** | Our side of the handover the Project Manual calls "the environment interface": what the agent sees, what it controls, how it is scored. Written by the RL Environment seat; to be finished jointly with Simulations' Simulation Core Developer early in Q2 and frozen before Christmas |
| **Where it lives in code** | `aicontrol/env/spaces.py` builds the spaces from `configs/env.yaml`. Run `uv run python -m aicontrol.env.spaces` to print the current layout |
| **Grown from** | The manual's interface list (section 3.2) and our older Simulator I/O sheet, whose names we keep where they exist |
| **Status** | Draft, 11 Sep 2026. Everything marked *to agree* is open until the joint session |
## The idea in one paragraph
The agent and the simulated hospital talk through a Gymnasium environment: `reset()` starts a
run, `step(action)` advances five minutes and returns what the agent may see next, plus a
score. Simulations owns everything inside `step` (the physics, the KPIs); we own everything
outside it (the agent, the safety layer, the evaluation). The two only have to agree on three
lists, below. Until the twin exists, a placeholder environment with toy physics exposes exactly
these lists so our side can be built now.
## 1. What the agent sees
One flat vector of 64 numbers per step, laid out from `configs/env.yaml`. The first sixteen are
the present; the remaining 48 are the forecasts.
| # | name | size | unit | range | source |
|---|---|---|---|---|---|
| 0 | `time_of_day` | 2 | sin, cos | 1 … 1 | calendar |
| 2 | `time_of_year` | 2 | sin, cos | 1 … 1 | calendar |
| 4 | `P_PV` | 1 | kW | 0 … 460 | twin: solar output now |
| 5 | `L_tier1` | 1 | kW | 0 … 1000 | data: critical load now |
| 6 | `L_tier2` | 1 | kW | 0 … 1000 | data: essential load now |
| 7 | `L_tier3` | 1 | kW | 0 … 1000 | data: non-critical load now |
| 8 | `SoC` | 1 | fraction 01 | 0 … 1 | twin: battery state of charge |
| 9 | `H2_level` | 1 | kg | 0 … 200 | twin: hydrogen in the tank |
| 10 | `p_tank` | 1 | bar | 0 … 30 | twin: tank pressure |
| 11 | `price` | 1 | currency/kWh | 1 … 5 | data: electricity price now |
| 12 | `CO2_int` | 1 | kg CO₂/kWh | 0 … 1.5 | data: grid carbon intensity now |
| 13 | `grid_on` | 1 | 0/1 | 0 … 1 | data: grid available |
| 14 | `ele_on` | 1 | 0/1 | 0 … 1 | twin: electrolyser running |
| 15 | `fc_on` | 1 | 0/1 | 0 … 1 | twin: fuel cell running |
| 1663 | `fcst_<quantity>_<horizon>min` | 3 each | kW | 0 … max | forecaster: p10, p50, p90 for solar, tier1, tier2, tier3 at +60, +180, +360, +1440 min |
Why sin and cos for time: a clock hand, not a number that jumps from 23:59 to 00:00. Why
ranges: they scale the numbers for the network and let a test catch nonsense; they are not
physical guarantees.
*To agree with Simulations:* which of these the twin reports directly (tank pressure or only
kilograms; equipment state as on/off or as a mode); the forecast horizons and whether four are
enough; the load upper bound once the dataset scale is known; the currency.
## 2. What the agent controls
Four floats per step.
| name | range | meaning |
|---|---|---|
| `u_ele` | 0 … 1 | fraction of electrolyser rated power; 0 is off, anything below the minimum load becomes off |
| `u_fc` | 0 … 1 | fraction of fuel cell rated power |
| `u_batt` | 1 … 1 | fraction of battery max power; positive discharges, negative charges |
| `shed` | 0 … 2 | rounded to a shedding level: 0 nothing, 1 Tier 3 shed, 2 Tiers 2 and 3 shed. Tier 1 is never an option |
The grid is not an action: it absorbs whatever is left after the four above, within its
connection limit. Load shedding *is* an action here, as the manual lists it, but the safety
layer will refuse anything that would touch Tier 1.
*To agree with Simulations:* the shedding encoding (a continuous number rounded inside the
environment is the simplest thing that works with Stable-Baselines3, which cannot mix
continuous and discrete actions; the alternative is making all four discrete); whether the
twin wants setpoints in kW or in fractions; ramp limits applied inside the twin or reported
back as a clipped action.
## 3. How it is scored
The score per step is a weighted sum of the terms the manual lists. The *terms* are ours to
propose; the *weights* are agreed with Energy Management and Business, because they say how
much a euro, a kilo of CO₂ and a shed ward are worth relative to each other. The numbers in
`configs/env.yaml` are placeholders so that code can run.
| term | sign | what it measures |
|---|---|---|
| energy cost | | money paid for grid electricity this step, minus money earned exporting |
| CO₂ | | grid electricity used × carbon intensity |
| Tier 1 unserved | | any critical load not supplied; must dominate every other term |
| Tier 2 / Tier 3 shed | | non-critical load cut, priced far below Tier 1 |
| limit violation | | battery or tank pushed past a safe limit |
| switching | | equipment turned on or off this step, to stop chattering |
*To agree:* whether a clipped action (the twin refused part of it) counts as a violation or
only as a reported difference; whether the perfect-knowledge benchmark and MPC are scored on
exactly this sum, so the comparison is fair.
## 4. What the twin must return so we can compute all of the above
Per step: served load per tier, grid import and export, PV used and curtailed, battery power,
electrolyser and fuel cell power, hydrogen produced and consumed, any limit clipping, and the
KPI increments. The manual's four KPIs (Self-Sufficiency Rate, Grid Dependency Ratio, Critical
Load Uptime, SoC Violation Rate) are computed by Simulations' KPI calculator; we read them,
we do not recompute them.
## 5. What happens when the twin replaces the placeholder
Nothing on our side, if the three lists hold. The training rig, the evaluation pipeline, the
safety wrapper and the dashboard all read the lists above, never the physics. That is the whole
point of drafting this now.

View file

@ -0,0 +1,131 @@
# The AI → Business results file — draft v0.1
| | |
|---|---|
| **What this is** | The third handover in the Project Manual: "one file with a fixed set of columns: one row per controller, per scenario, per run. Business builds the euro comparison straight from it. Agree the columns before the first benchmark is run" |
| **Owner** | RL Evaluation seat (person 3). Reviewed by the lead 11 Sep 2026 from person 3's spreadsheet draft |
| **Next** | Encode as `aicontrol/evaluate/results_schema.py` (week 4), show to Business's Commercial Model Analyst, agree the open questions at the bottom, then freeze |
| **Format** | Parquet is the version of record; a CSV is written beside it so Business can double-click it. Rows are appended, never edited |
## Rules that make the file usable by a script
- Column names are lowercase `snake_case`, no spaces, no hyphens, and carry the unit where one
exists (`_kwh`, `_kg`, `_kw`). Fractions are stored as 01, never as percentages.
- One row is one run: one controller, on one scenario, with one seed. A controller is a *value*
in the `controller` column, not a row label, so the file grows to hundreds of rows.
- Controller names are fixed strings: `rule_based`, `mpc`, `perfect_knowledge`, `agent`, and
the Q1 stand-ins `random` and `do_nothing`. Business's S0 baseline (the hospital without the
hydrogen system) is not a controller but a plant configuration, so it is a *scenario* from
Simulations' library, appearing here as `scenario = no_hydrogen_<year>` with
`controller = rule_based`.
- A column may be empty for a run (for example, `wandb_run` for a rule-based run). A column
that is empty for every run at the freeze is dropped.
## Columns
### Identity — which run this row is
| column | type | meaning |
|---|---|---|
| `controller` | string | one of the fixed names above |
| `controller_version` | string | git commit for rule-based / MPC; model checkpoint tag for the agent |
| `scenario` | string | name of the scenario YAML file, e.g. `baseline_2018`, `blackout_72h`, `dark_december_week` |
| `data_year` | int | which year of data the run used |
| `held_out` | bool | true if the agent never saw this year in training; the manual's proof of generalisation |
| `run` | int | repetition index within a setup (1, 2, 3, …) |
| `seed` | int | the random seed of that repetition |
| `steps` | int | number of steps simulated |
| `step_minutes` | int | 5 |
| `twin_version` | string | version of the Simulations package that produced the physics |
| `parameter_book_version` | string | version of Energy Management's Parameter Book |
| `dataset_version` | string | version of the clean hospital dataset |
| `run_at` | timestamp | when the run finished (UTC) |
| `wandb_run` | string | Weights & Biases run id, if any |
The three `*_version` columns are the manual's rule that "every result records which versions
produced it".
### KPIs — the manual's four, plus money and carbon
Names are fixed now; the exact equations come from Energy Management's Dispatch & Grid
Engineer and are applied by Simulations' KPI calculator. We copy the numbers; we do not
recompute them.
| column | type | meaning |
|---|---|---|
| `self_sufficiency_rate` | fraction | share of load served without the grid |
| `grid_dependency_ratio` | fraction | share of load that came from the grid |
| `critical_load_uptime` | fraction | share of steps with Tier 1 fully served; must be 1.0 |
| `soc_violation_rate` | fraction | share of steps with the battery outside its window |
| `energy_cost` | float | net cost of grid electricity over the run (import cost minus export revenue) |
| `currency` | string | `USD` or `EUR`; see open questions |
| `co2_kg` | float | grid electricity used × carbon intensity, summed |
### Energy totals — the evidence behind the KPIs, and what Business's register needs
All summed over the run.
| column | type | meaning |
|---|---|---|
| `load_kwh` | float | total hospital demand |
| `load_served_kwh` | float | demand actually supplied |
| `tier1_unserved_kwh` | float | **must be 0** in every run that counts |
| `tier2_shed_kwh` | float | essential load cut |
| `tier3_shed_kwh` | float | non-critical load cut |
| `pv_kwh` | float | solar available |
| `pv_curtailed_kwh` | float | solar thrown away |
| `grid_import_kwh` | float | |
| `grid_export_kwh` | float | |
| `grid_import_peak_kwh` | float | import during tariff peak hours |
| `grid_import_offpeak_kwh` | float | import during off-peak hours |
| `peak_import_kw` | float | highest grid import in any step; drives demand charges |
| `battery_charge_kwh` | float | |
| `battery_discharge_kwh` | float | |
| `battery_full_cycles` | float | throughput ÷ capacity; the wear proxy |
| `electrolyser_kwh` | float | electricity into the electrolyser |
| `electrolyser_offpeak_kwh` | float | the part of it in off-peak hours |
| `h2_produced_kg` | float | |
| `h2_consumed_kg` | float | |
| `fuel_cell_kwh` | float | electricity out of the fuel cell |
| `fuel_cell_heat_kwh` | float | recovered heat, if the twin models it; else empty |
| `chp_kwh` | float | electricity from the CHP plant |
| `electrolyser_starts` | int | on/off switches |
| `fuel_cell_starts` | int | on/off switches |
| `limit_violations` | int | steps where the twin clipped an action against a limit |
| `notes` | string | free text |
Why the peak / off-peak and hydrogen columns: Business's cost register has seven rows marked
"ModelOutput / Missing" — off-peak share of hydrogen production, MWh shifted from peak to
off-peak, peak shaving, effective cost of electrolysis, cost per kWh from hydrogen, gas
displaced by fuel-cell heat, and the CO₂ change — and each is a ratio or difference of the
columns above between the no-hydrogen scenario, the `rule_based` rows and the `agent` rows.
The peak / off-peak definition is the tariff's, which Business and Energy Management own.
## The companion file
Business's register also asks for "simulation of net grid imports" as a series. That does not
fit one row per run. Each run therefore also writes its per-step table (the same quantities,
one row per 5-minute step) as Parquet under `results/runs/<run_id>.parquet`. The summary file
above is the one they open; the per-step file is there when they need a chart.
## Changes from person 3's spreadsheet draft
- Added the `controller` header (the draft's column A had none) and made controller a column
value rather than a row label.
- Fixed `KPI_Grid_Dependecy_Ratio``grid_dependency_ratio`; dropped the `KPI_` prefix,
hyphens and capitals from every name so the file loads without renaming.
- Replaced `cost_eur` with `energy_cost` + `currency`, because the data source is ISO New
England, which prices in dollars, while the business case is in euros.
- Added identity columns (`held_out`, `data_year`, the three versions, `run_at`), the energy
totals and the switching counts.
- Kept `scenario`, `run`, `seed`, the four KPIs and `co2_kg` as drafted.
## Open questions for Business (person 3 brings these)
1. Currency: do they want dollars as recorded, or euros converted at a fixed rate they choose?
2. Peak / off-peak: their tariff hours, so the split columns match their model.
3. Are the seven register quantities computed by us (extra columns) or by them from these
columns? Either works; theirs keeps one owner per number.
4. What exactly their S0 baseline contains (no hydrogen only, or no battery either), so we can
ask Simulations for that scenario file.
5. Anything they need per run that is not here.

17
AICONTROL/pyproject.toml Normal file
View file

@ -0,0 +1,17 @@
[project]
name = "aicontrol"
version = "0.1.0"
description = "AI & Control cluster: placeholder environment, forecasting harness, training rig, evaluation pipeline, safety layer, dashboard"
requires-python = ">=3.12,<3.13"
dependencies = [
"numpy>=2.0",
"gymnasium>=1.0",
"pyyaml>=6.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["aicontrol"]

View file

@ -0,0 +1,34 @@
"""The observation and action spaces build from the config and mean what they say."""
import numpy as np
from aicontrol.env import spaces as S
def test_observation_layout_matches_config():
cfg = S.load_config()
space = S.build_observation_space(cfg)
fixed = 4 + 12 # calendar (2 + 2) plus the twelve single-value slots
fc = cfg["forecast"]
forecast = len(fc["quantities"]) * len(fc["horizons_min"]) * len(fc["quantiles"])
assert space.shape == (fixed + forecast,)
assert S.observation_size(cfg) == space.shape[0]
assert space.contains(space.sample())
def test_action_space_and_shedding_level():
cfg = S.load_config()
space = S.build_action_space(cfg)
assert space.shape == (len(S.ACTION_NAMES),)
assert space.contains(space.sample())
assert S.shedding_level(np.array([0, 0, 0, 0.2]), cfg) == S.SHEDDING_NONE
assert S.shedding_level(np.array([0, 0, 0, 1.4]), cfg) == S.SHEDDING_TIER3
assert S.shedding_level(np.array([0, 0, 0, 2.0]), cfg) == S.SHEDDING_TIERS_2_3
assert S.shedding_level(np.array([0, 0, 0, 9.0]), cfg) == S.SHEDDING_TIERS_2_3 # clipped
def test_describe_lists_every_named_slot():
cfg = S.load_config()
table = S.describe(cfg)
for name in ("P_PV", "SoC", "H2_level", "p_tank", "grid_on", "fcst_solar_60min", "fcst_tier3_1440min"):
assert f"`{name}`" in table

112
CLAUDE.md Normal file
View file

@ -0,0 +1,112 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this repository is
ALLSHIFT is Team SHIFT's single shared repository (TU/e student team; an AI-managed hospital
hydrogen microgrid, Beth Israel Deaconess Boston as the case). It is public on Gitea
(`git.teamshiftenergy.com/pepe/ALLSHIFT`). Four kinds of content live side by side:
- `docs/` — the documentation hub. Every project document exists here as a markdown report;
`docs/README.md` is the index and ends with the project-wide open-questions list. Retired
material goes to `superseded/` with a stated reason.
- `Shift Matlab Drive/` — a mirror of the team's MATLAB Drive: Simulink models and raw data.
The Simulink work belongs to the Simulations cluster (see
`docs/04-simulations/simulink-model-inventory.md`; the live framework needs R2026a). No
Python goes here.
- `website/` — the public Astro site, with its own `website/CLAUDE.md` and `pnpm` commands.
No Python goes here either.
- Root `pyproject.toml` + `uv.lock` — the one pinned Python environment for every cluster.
Cluster code lives in top-level folders registered as uv workspace members; `AICONTROL/`
(AI & Control cluster) is the first.
## Commands (from the repository root)
```bash
uv sync # create/refresh .venv from uv.lock (Python 3.12; ~1.3 GB, torch included)
uv run pytest # all tests (pytest testpaths = AICONTROL/tests)
uv run pytest AICONTROL/tests/test_spaces.py::test_action_space_and_shedding_level # one test
uv run python -m aicontrol.env.spaces # print the current observation/action layout as a table
uv lock # after editing dependencies; commit the updated uv.lock
```
Adding a dependency: edit `dependencies` in the root `pyproject.toml`, run `uv lock`, commit
`uv.lock`. Adding a cluster folder: give it its own `pyproject.toml` (hatchling,
`packages = [...]`), add the folder to `[tool.uv.workspace] members`, and add its package name
to the root `dependencies` and to `[tool.uv.sources]` as `{ workspace = true }`.
Website: `cd website && pnpm dev | pnpm build | pnpm preview` — details in `website/CLAUDE.md`.
## Architecture
### Clusters and the three handovers
The team leader's *Project Manual* (not in the repo) fixes four clusters and exactly three
written handovers between them:
1. **Energy Management → Simulations:** the Parameter Book (a spreadsheet of every rating and
efficiency, versioned) and the clean 5-minute BIDMC dataset with Tier 1 / 2 / 3 load
assigned, as Parquet.
2. **Simulations → AI:** a Gymnasium environment. Simulations owns everything inside `step()`
— the twin's physics, the KPI calculator, and the benchmark controllers (rule-based, MPC on
forecasts, perfect knowledge; Pyomo + HiGHS). AI owns everything outside it: the agent, the
safety layer, evaluation, dashboard. Written jointly early in Q2, frozen before Christmas.
3. **AI → Business:** one results file, one row per controller × scenario × run, Parquet with a
CSV beside it.
Fixed by the manual, not up for local decision: 5-minute timestep (105,120 steps per year); a
full simulated year must run in under a minute; Boston with ISO New England prices and carbon;
Tier 1 load is never shed; reward weights are agreed across clusters and never tuned by code.
### `AICONTROL/` — the AI & Control cluster package
- Everything is built from `AICONTROL/configs/env.yaml`, so the joint session with Simulations
can change a number without touching code. Values marked `PLACEHOLDER` there (plant sizes,
load bound, reward weights) are stand-ins owned by other clusters; keep the marker until the
owning cluster confirms a value.
- `aicontrol/env/spaces.py` is the single source of the interface shape. `observation_slots(cfg)`
defines the flat 64-value observation in order (calendar 4, present-state 12, forecasts
= quantities × horizons × quantiles = 48); `build_action_space` gives a `Box(4)`: `u_ele`
[0, 1], `u_fc` [0, 1], `u_batt` [1, 1] with positive = discharge, `shed` [0, 2] rounded by
`shedding_level()` because Stable-Baselines3 cannot mix continuous and discrete actions.
Names follow the Simulator I/O sheet (`P_PV`, `SoC`, `H2_level`, `p_tank`, `price`,
`CO2_int`, `grid_on`).
- `AICONTROL/docs/interface-draft.md` is the handover document; keep its tables in step with
`spaces.py` (regenerate the observation table with `describe()`).
- The placeholder environment (next: `aicontrol/env/placeholder.py`) exposes exactly these
spaces with toy physics. It never gets better physics and is deleted the day the twin runs.
Do not build a competing simulator in this folder.
- Planned subpackages: `forecast/`, `train/`, `evaluate/`, `safety/`, `dashboard/`. Tooling is
fixed by the manual: LightGBM / scikit-learn / Optuna for forecasting, Gymnasium +
Stable-Baselines3 for RL, Weights & Biases for logging (not MLflow), SHAP + Streamlit.
### Documentation conventions
- A markdown report of a non-markdown source opens with a provenance table (Source, Format,
MD5, Status, Report generated). Reconstructed tables are labelled as such; observations that
go beyond the source are attributed to the audit, not the author.
- `docs/02-specifications/` is owned by the AI cluster. The RL state / action / reward
documents there are superseded by the manual's interface list but kept for the rationale
behind individual terms.
- `docs/01-project/ai-control-cluster-plan-2026-2027.md` is the AI cluster's plan: seats,
Q1 week by week, later blocks, open items for the leader.
## Data: what actually exists
Under `Shift Matlab Drive/Shift Matlab Drive/Energy_Managment/`: hospital load is the NREL
ComStock building `89993-0.parquet` (2018, 15-minute, simulated; `MA_hourly_load.csv` is a
mis-named numerical twin of it). Weather is NSRDB 2019, hourly, timestamps in UTC. PV is a
PVWatts 4 kW typical-year run that aligns with neither. There is no price, carbon-intensity,
outage or measured-load series. `hospital_communication_energy_system.csv` is synthetic noise;
derive nothing from it. Provenance and licences: `docs/06-data/dataset-inventory.md`.
## Things to know
- Formats: machine-made tables are Parquet, hand-written settings are YAML, documents are
Markdown.
- `.venv/`, `wandb/` run folders and `**/data/raw/` are gitignored; `uv sync` rebuilds the
environment from `uv.lock` on any machine.
- `website/CLAUDE.md` embeds live third-party keys that are due for rotation; do not copy them
anywhere.
- `Interview Questions-*.md` at the root is personal recruitment data, gitignored on purpose.

View file

@ -93,7 +93,7 @@ Set once per simulation run.
|---|---|---|---|
| `N_c_ele` | Number of cells | — | Usually difficult to find |
| `mu_F` | Faraday efficiency | — | Fall back to literature average (~0.950.99 for PEM) |
| `P_ele_max` | Rated / max power | W | |
| `P_Electro_max` | Rated / max power | W | |
| `P_ele_min` | Minimum operating power | W | Below this the electrolyser shuts off (efficiency cliff) |
| `I_ele_min` | Minimum operating current | A | Alternative to `P_ele_min` |
| `cal_H2` | H₂ production calibration factor | — | If we end up reading H₂ flow from measured data |
@ -104,13 +104,14 @@ Set once per simulation run.
|---|---|---|---|
| `N_c_fc` | Number of cells | — | |
| `utilisation_fc` | H₂ utilisation | % | |
| `P_fc_max` | Maximum output power | W | **Project spec: 100 kW** |
| `P_FuelCell_max` | Maximum output power | W | **Project spec: 100 kW** |
| `V_fc_min`, `V_fc_max` | Operating voltage range | V | |
### 1.3 Hydrogen tank
| Symbol | Name | Unit | Notes |
|---|---|---|---|
| `E_H2_max` | Maximum electrical energy capacity of the storage tank | J | |
| `V_H2_max` | Maximum stored volume | L (or kg) | **Project spec: up to 200 kg total across two tanks** |
| `V_H2_init` | Initial fill level | L (or kg) | |
| `T_tank` | Operating temperature | K | TBD — isothermal assumption likely fine |
@ -129,9 +130,11 @@ Set once per simulation run.
| Symbol | Name | Unit | Notes |
|---|---|---|---|
| `E_rated` | Rated energy capacity | Wh | |
| `E_battery_max` | Maximum energy capacity | Wh | |
| `Q_rated` | Rated charge capacity | Ah | |
| `P_batt_charge_max` | Max charge power | W | |
| `P_batt_discharge_max` | Max discharge power | W | |
| `P_battery_max` | Max (dis)charge power (assuming both are identical - this is the current assumption in the Simulink Controller) | W | |
| `P_battery_charge_max` | Max charge power | W | |
| `P_battery_discharge_max` | Max discharge power | W | |
| `SoC_init` | Initial state of charge | — (01) | |
| `SoC_min`, `SoC_max` | Operating window | — (01) | E.g. 0.10.9 |
| `eta_batt_ch`, `eta_batt_dis` | Round-trip efficiencies | — | Often split into charge & discharge |
@ -195,6 +198,8 @@ Carried forward to the next step.
| Symbol | Name | Unit |
|---|---|---|
| `SoC(t)` | Battery state of charge | — (01) |
| `E_battery_SOC(t)` | Energy currently present in battery | J |
| `E_H2_SOC(t)` | Electrical Energy that can be currently extracted from the Hydrogen Storage Tank | J |
| `H2_level(t)` | H₂ stored in tank | mol (or kg) |
| `T_tank(t)` | Tank temperature | K (only if non-isothermal model) |
| `p_tank(t)` | Tank pressure | bar (if modelled) |
@ -203,7 +208,7 @@ Carried forward to the next step.
| Symbol | Name | Unit |
|---|---|---|
| `P_PV_available(t)` | PV power available given irradiance | W |
| `P_PV(t)` | PV power available given irradiance | W |
| `P_PV_used(t)` | PV power actually consumed | W |
| `P_PV_curtailed(t)` | PV potential that was thrown away | W |
| `P_ele(t)` | Actual electrolyser consumption | W |
@ -212,6 +217,12 @@ Carried forward to the next step.
| `P_grid(t)` | Actual grid flow (signed) | W |
| `P_load_served_crit(t)` | Critical load served | W |
| `P_load_served_noncrit(t)` | Non-critical load served | W |
| `P_load_P(t)` | Power to be covered after PV (+ve = remaining power shortage, -ve = surplus to be used) | W |
| `P_load_PB(t)` | Power to be covered after PV and Battery (same convention as P_load_P) | W |
| `P_load_PBH(t)` | Power to be covered after PV, Battery and Hydrogen stroage tank (same convention as P_load_P(B)) | W |
| `P_battery_cont(t)` | Power amount the controller determines the battery should (dis)charge at (accounting for battery properties and demand) | W |
| `P_FuelCell_cont(t)` | Power amount the controller determines the fuel cell should provide (accounting for it's properties and demand) | W |
| `P_Electro_cont(t)` | Power amount the controller determines the electrolyser should extract (accounting for it's properties and demand) | W |
### 3.3 Mass flows (hydrogen)

52
pyproject.toml Normal file
View file

@ -0,0 +1,52 @@
# Team SHIFT — one pinned Python environment for every cluster.
#
# The Project Manual asks that everyone technical works in one repository from one pinned
# Python environment. This file is that environment. The exact versions live in uv.lock;
# `uv sync` at the repository root creates .venv/ with all of them, on any machine.
#
# Each cluster keeps its code in its own top-level folder (AICONTROL/, ...) as a workspace
# member: add the folder to [tool.uv.workspace] members and its package name to
# dependencies, and `uv sync` installs it in editable mode for everyone.
[project]
name = "shift"
version = "0.1.0"
description = "AI-managed hospital hydrogen microgrid — shared environment for all clusters"
requires-python = ">=3.12,<3.13"
dependencies = [
# tables, maths, plots, tests — everyone
"numpy>=2.0",
"pandas>=2.2",
"pyarrow>=17", # Parquet, the agreed format for machine-made tables
"scipy>=1.14",
"matplotlib>=3.9",
"pyyaml>=6.0", # YAML settings and scenario files
"pytest>=8.3",
# Simulations cluster
"pvlib>=0.11", # solar array model
"pyomo>=6.8", # optimisation problems for the benchmark controllers
"highspy>=1.8", # the HiGHS solver
# AI cluster
"gymnasium>=1.0", # the environment interface
"stable-baselines3>=2.4",# ready-made RL algorithms (pulls in torch)
"lightgbm>=4.5", # main forecasting model
"scikit-learn>=1.5", # forecast baselines, sensor checks
"optuna>=4.0", # settings search
"wandb>=0.18", # experiment logging (Weights & Biases)
"shap>=0.46", # explanations
"streamlit>=1.39", # dashboard
# cluster packages from this repository (editable, see [tool.uv.sources])
"aicontrol",
]
[tool.uv]
package = false # the root is an environment, not a library
[tool.uv.sources]
aicontrol = { workspace = true }
[tool.uv.workspace]
members = ["AICONTROL"]
[tool.pytest.ini_options]
testpaths = ["AICONTROL/tests"]

1751
uv.lock generated Normal file

File diff suppressed because it is too large Load diff