Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1108,13 +1108,103 @@ controllers on identical VPP avoidance, comfort, and task-completion metrics.
Native HEMA ReAct agent baseline. This baseline requires the original HEMA
repository as an external dependency.

```text
#### Architecture Overview

The HEMA baseline consists of three layers (all located in `experiments/benchmark/baselines/hema/`):

```
experiments/benchmark/baselines/hema/
├── hema_controller.py # HEMAControlBaseline: main controller class
├── device_bridge.py # EnergyBridgeToHEMA: state synchronization bridge
└── __init__.py # get_hema_controller() + sys.path setup
```

#### Step 1: Install HEMA

```bash
# From your project root
git clone https://github.com/humanbuildingsynergy/HEMA.git
```

Install HEMA dependencies:

```bash
cd HEMA
# Install dependencies
pip install -r requirements.txt
```

#### Step 2: How the Adapter Works

The adapter bridges EnergyBridge and HEMA through three files in `experiments/benchmark/baselines/hema/`:

##### `__init__.py`

Factory function that returns the HEMA controller class. Manages `sys.path` so HEMA imports resolve correctly without polluting the global namespace. If you move the HEMA clone to a different directory, update `_HEMA_ROOT` here.

##### `device_bridge.py` — `EnergyBridgeToHEMA`

| Function | What it does |
|----------|-------------|
| `ensure_device_config()` | Resets HEMA's global device state and rebuilds it from EnergyBridge's `appliance_config` + live `eplus_state`. Maps EnergyBridge devices (washer, dishwasher, etc.) to HEMA's schema (washing_machine, dishwasher, etc.) with °C→°F conversion. |
| `build_query()` | Constructs the natural-language prompt sent to HEMA's ReAct agent. Pulls from persona schema (tags, preferences, schedule, appliances), live state (indoor/outdoor temp, setpoint), VPP context (event window, capacity target), and price data. |
| `extract_actions()` | Parses HEMA's tool calls and converts back to EnergyBridge control JSON. Fuzzy-matches device names, maps actions (e.g., `set_temperature` → `setpoint_f`, `set_schedule` → `*_start_h`), and converts °F→°C. |

##### `hema_controller.py` — `HEMAControlBaseline`

| Function | What it does |
|----------|-------------|
| `decide()` | Entry point called by `family_runner.py` each timestep. Initializes HEMA agent on first call, then delegates to `_decide_with_retry()`. |
| `_decide_with_retry()` | Invokes HEMA agent, checks if all present appliances received commands. If any are missing, retries up to 5 times with a strengthened mandatory-action prompt. Logs raw tool calls for debugging. Tracks token usage and latency. |
| `_to_energybridge()` | Converts parsed HEMA actions to EnergyBridge format: °F→°C setpoint conversion, assembles `appliance_actions` dict, sets `next_check_hour` to wake controller after VPP ends. |

#### Step 3: Run HEMA as a Baseline

```text
# Run full Tianjin 7-day personal-user matrix
python experiments/benchmark/run_baseline_matrix.py \
--methods hema_agent \
--city Tianjin \
--days 7 \
--mpc-horizon 6 \
--date <yyyy-mm-dd> \
--workers 5 \
--resume

# Run full Germany 7-day personal-user matrix
python experiments/benchmark/run_baseline_matrix.py \
--methods hema_agent \
--city Germany \
--days 7 \
--start-date 2025-06-01 \
--mpc-horizon 6 \
--price-csv experiments/real_data/germany_2025_price.csv \
--date <yyyy-mm-dd> \
--workers 5 \
--resume

# Run all five households for Tianjin
python experiments/benchmark/run_household_matrix.py \
--methods hema_agent \
--city Tianjin \
--days 7 \
--start-date 2025-06-01 \
--date <YYYY-MM-DD> \
--workers 5 \
--resume

# Run all five households for Germany
python experiments/benchmark/run_household_matrix.py \
--methods hema_agent \
--city Germany \
--days 7 \
--start-date 2025-06-01 \
--price-csv experiments/real_data/germany_2025_price.csv \
--date <YYYY-MM-DD> \
--workers 5 \
--resume
```


```text
experiments/benchmark/baselines/hema
```
Expand Down
14 changes: 11 additions & 3 deletions experiments/benchmark/baselines/hema/device_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ def extract_actions(self, agent_result: Dict[str, Any]) -> Dict[str, Any]:
washer_start = _parse_hod(time_str)
elif dev in ("dishwasher", "Dishwasher"):
dishwasher_start = _parse_hod(time_str)
elif dev in ("clothes_dryer", "dryer"):
elif dev in ("clothes_dryer", "dryer", "Clothes Dryer"):
dryer_start = _parse_hod(time_str)


Expand All @@ -679,9 +679,17 @@ def extract_actions(self, agent_result: Dict[str, Any]) -> Dict[str, Any]:
wh_end = _parse_hod(time_str)
wh_preheat = True
elif action_str in ("set_temperature", "set_temp", "temperature"):
wh_temp_f = float(value)
try:
if value is None:
wh_temp_f = float(time_str)
else:
temp_str = str(value).replace("°", "").replace("F", "").replace("f","").replace("C","").replace("c", "").strip()
wh_temp_f = float(temp_str)
wh_preheat = True
except (ValueError, TypeError):
pass

elif dev in ("ev_charger", "electric_vehicle", "ev", "tesla_charger", "car_charger"):
elif dev in ("ev_charger", "electric_vehicle", "ev", "tesla_charger", "car_charger", "EV Charger"):
action_str = str(action).lower().replace(" ", "_")
if action_str in ("start", "start_charging", "begin_charging"):
ev_charge_start = _parse_hod(time_str)
Expand Down
8 changes: 5 additions & 3 deletions experiments/benchmark/baselines/hema/hema_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
# ------------------------------------------------------------------
# Read EnergyBridge .env config (fixes base_url and model)
# ------------------------------------------------------------------
_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY") or ""
_API_KEY = os.getenv("LLM_API_KEY") or ""
if not _API_KEY:
_pool = os.getenv("LLM_API_KEY_POOL", "")
if _pool:
Expand Down Expand Up @@ -165,7 +165,9 @@ def _decide_with_retry(
"- clothes_dryer: MUST schedule a start time today using schedule_device_action. "
" Choose a time outside the VPP window (18:00-19:00). Use 24-hour format like '10:00'.\n"
"- water_heater: MUST set a preheat schedule using schedule_device_action. "
" Set start time with action 'start', end time with action 'stop', and temperature with action 'set_temperature'. Must finish end time before the VPP window (18:00-19:00)\n"
" ONLY provide 'value' parameter (temperature in °F as a number, e.g., '125'), DO NOT provide 'time' parameter for set_temperature. "
" Set start time with action 'start', end time with action 'stop', and temperature with action 'set_temperature'. "
" Must finish end time before the VPP window (18:00-19:00)\n"
"- ev_charger: MUST set the charging schedule. "
" Ensure the charging window greater or equal to Minimum charging hours to reach target_soc. "
" EV CHARGING TIME CALCULATION GUIDE:\n"
Expand Down Expand Up @@ -276,7 +278,7 @@ def _to_energybridge(
if parsed.get("dryer_start_h") is not None:
appl["dryer_start_h"] = float(parsed["dryer_start_h"])
appl["dryer_skip"] = False
if parsed.get("water_heater_preheat") is not None:
if parsed.get("water_heater_preheat"):
appl["water_heater_preheat"] = True
appl["water_heater_preheat_start_h"] = float(parsed["water_heater_preheat_start_h"])
appl["water_heater_preheat_end_h"] = float(parsed["water_heater_preheat_end_h"])
Expand Down