Add shared Python environment, AICONTROL cluster folder, and CLAUDE.md
- Root pyproject.toml + uv.lock: one pinned Python 3.12 environment for every cluster (the Project Manual's rule), as a uv workspace; cluster code folders are workspace members. - AICONTROL/: the AI & Control cluster package. spaces.py builds the 64-value observation and 4-value action spaces from configs/env.yaml; interface draft for the November session with Simulations; tests; clone-and-run README. - .gitignore: Python environment, caches, W&B runs, raw data downloads. - CLAUDE.md: repository guidance for Claude Code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
72dd781dbc
commit
ef7857da31
13 changed files with 2338 additions and 0 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -23,3 +23,14 @@ slprj/
|
||||||
# Personal data — recruitment notes on named candidates. Kept locally, never committed.
|
# Personal data — recruitment notes on named candidates. Kept locally, never committed.
|
||||||
# Root-anchored so it cannot catch the generic question template in docs/.
|
# Root-anchored so it cannot catch the generic question template in docs/.
|
||||||
/Interview Questions-*.md
|
/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
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.12
|
||||||
66
AICONTROL/README.md
Normal file
66
AICONTROL/README.md
Normal 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.
|
||||||
3
AICONTROL/aicontrol/__init__.py
Normal file
3
AICONTROL/aicontrol/__init__.py
Normal 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
1
AICONTROL/aicontrol/env/__init__.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""The environment side: spaces now, the placeholder environment next week."""
|
||||||
147
AICONTROL/aicontrol/env/spaces.py
vendored
Normal file
147
AICONTROL/aicontrol/env/spaces.py
vendored
Normal 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)}")
|
||||||
40
AICONTROL/configs/env.yaml
Normal file
40
AICONTROL/configs/env.yaml
Normal 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
|
||||||
103
AICONTROL/docs/interface-draft.md
Normal file
103
AICONTROL/docs/interface-draft.md
Normal 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 0–1 | 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 |
|
||||||
|
| 16–63 | `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.
|
||||||
17
AICONTROL/pyproject.toml
Normal file
17
AICONTROL/pyproject.toml
Normal 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"]
|
||||||
34
AICONTROL/tests/test_spaces.py
Normal file
34
AICONTROL/tests/test_spaces.py
Normal 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
112
CLAUDE.md
Normal 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.
|
||||||
52
pyproject.toml
Normal file
52
pyproject.toml
Normal 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"]
|
||||||
Loading…
Add table
Reference in a new issue