Skip to content

Commit 042c983

Browse files
committed
test different time step sizes and fix test failing due to api consumption
1 parent a72bb3b commit 042c983

File tree

9 files changed

+307
-4
lines changed

9 files changed

+307
-4
lines changed

tests/conftest.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,64 @@
11
"""
2-
Pytest-wide configuration: make the repository importable without installation.
2+
Pytest-wide configuration: make the repository importable without installation and
3+
avoid external downloads during test runs.
34
45
By pushing the project root onto ``sys.path`` we allow ``import citylearn`` to
5-
work even when the package has not been installed into the active environment.
6-
This mirrors the behaviour of several legacy scripts and keeps local runs
7-
simple.
6+
work even when the package has not been installed. This mirrors legacy scripts
7+
and keeps local runs simple. Additionally, we make ``DataSet`` prefer the
8+
bundled datasets so tests don't hit the GitHub API.
89
"""
910

11+
import shutil
1012
import sys
1113
from pathlib import Path
1214

15+
import pytest
16+
from typing import Union
17+
1318
ROOT = Path(__file__).resolve().parents[1]
1419
if str(ROOT) not in sys.path:
1520
sys.path.insert(0, str(ROOT))
21+
22+
23+
@pytest.fixture(autouse=True)
24+
def _prefer_local_datasets(monkeypatch):
25+
"""Ensure tests use the repo's datasets without hitting the network."""
26+
27+
from citylearn.data import DataSet
28+
29+
local_root = ROOT / "data" / "datasets"
30+
original_get_dataset = DataSet.get_dataset
31+
original_get_dataset_names = DataSet.get_dataset_names
32+
33+
def _copy_dataset(src: Path, dest: Path) -> None:
34+
if dest.exists():
35+
shutil.rmtree(dest)
36+
37+
dest.parent.mkdir(parents=True, exist_ok=True)
38+
shutil.copytree(src, dest)
39+
40+
def patched_get_dataset(self, name: str, directory: Union[Path, str, None] = None):
41+
src = local_root / name
42+
43+
if src.is_dir():
44+
if directory is not None:
45+
directory = Path(directory)
46+
directory.mkdir(parents=True, exist_ok=True)
47+
dest = directory / name
48+
_copy_dataset(src, dest)
49+
return str(dest / "schema.json")
50+
51+
cache_dir = Path(self.cache_directory) / "datasets" / name
52+
_copy_dataset(src, cache_dir)
53+
return str(cache_dir / "schema.json")
54+
55+
return original_get_dataset(self, name, directory)
56+
57+
def patched_get_dataset_names(self):
58+
if local_root.is_dir():
59+
return sorted(p.name for p in local_root.iterdir() if p.is_dir())
60+
61+
return original_get_dataset_names(self)
62+
63+
monkeypatch.setattr(DataSet, "get_dataset", patched_get_dataset)
64+
monkeypatch.setattr(DataSet, "get_dataset_names", patched_get_dataset_names)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
month,hour,minutes,day_type,daylight_savings_status,indoor_dry_bulb_temperature,average_unmet_cooling_setpoint_difference,indoor_relative_humidity,non_shiftable_load,dhw_demand,cooling_demand,heating_demand,solar_generation
2+
1,0,0,1,0,21.0,0.0,45.0,1.2,0.0,0.0,0.0,0.0
3+
1,0,15,1,0,21.0,0.0,45.0,1.1,0.0,0.0,0.0,0.0
4+
1,0,30,1,0,20.9,0.0,45.0,1.0,0.0,0.0,0.0,0.0
5+
1,0,45,1,0,21.1,0.0,45.0,0.9,0.0,0.0,0.0,0.1
6+
1,1,0,1,0,21.2,0.0,45.0,0.8,0.0,0.0,0.0,0.2
7+
1,1,15,1,0,21.2,0.0,45.0,0.9,0.0,0.0,0.0,0.3
8+
1,1,30,1,0,21.3,0.0,45.0,1.0,0.0,0.0,0.0,0.4
9+
1,1,45,1,0,21.3,0.0,45.0,1.1,0.0,0.0,0.0,0.2
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
carbon_intensity
2+
0.20
3+
0.20
4+
0.18
5+
0.16
6+
0.15
7+
0.17
8+
0.19
9+
0.20
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
electric_vehicle_charger_state,electric_vehicle_id,electric_vehicle_battery_capacity_khw,current_soc,electric_vehicle_departure_time,electric_vehicle_required_soc_departure,electric_vehicle_estimated_arrival_time,electric_vehicle_estimated_soc_arrival
2+
1,Electric_Vehicle_1,20,8.0,4,80,,
3+
1,Electric_Vehicle_1,20,8.5,3,80,,
4+
1,Electric_Vehicle_1,20,9.0,2,80,,
5+
1,Electric_Vehicle_1,20,10.0,1,80,,
6+
3,Electric_Vehicle_1,20,,,,,
7+
3,Electric_Vehicle_1,20,,,,,
8+
2,Electric_Vehicle_1,20,,,,3,45
9+
2,Electric_Vehicle_1,20,,,,2,45
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
electricity_pricing,electricity_pricing_predicted_1,electricity_pricing_predicted_2,electricity_pricing_predicted_3
2+
0.10,0.11,0.12,0.13
3+
0.10,0.11,0.12,0.13
4+
0.11,0.12,0.13,0.14
5+
0.12,0.13,0.14,0.15
6+
0.13,0.14,0.15,0.16
7+
0.12,0.13,0.14,0.15
8+
0.11,0.12,0.13,0.14
9+
0.10,0.11,0.12,0.13
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
{
2+
"random_seed": 0,
3+
"root_directory": null,
4+
"central_agent": true,
5+
"simulation_start_time_step": 0,
6+
"simulation_end_time_step": 7,
7+
"episode_time_steps": 8,
8+
"rolling_episode_split": false,
9+
"random_episode_split": false,
10+
"seconds_per_time_step": 900,
11+
"start_date": "2024-01-01",
12+
"observations": {
13+
"month": { "active": true, "shared_in_central_agent": true },
14+
"day_type": { "active": true, "shared_in_central_agent": true },
15+
"hour": { "active": true, "shared_in_central_agent": true },
16+
"minutes": { "active": true, "shared_in_central_agent": true },
17+
"outdoor_dry_bulb_temperature": { "active": true, "shared_in_central_agent": true },
18+
"outdoor_dry_bulb_temperature_predicted_1": { "active": true, "shared_in_central_agent": true },
19+
"outdoor_relative_humidity": { "active": true, "shared_in_central_agent": true },
20+
"outdoor_relative_humidity_predicted_1": { "active": true, "shared_in_central_agent": true },
21+
"diffuse_solar_irradiance": { "active": true, "shared_in_central_agent": true },
22+
"diffuse_solar_irradiance_predicted_1": { "active": true, "shared_in_central_agent": true },
23+
"direct_solar_irradiance": { "active": true, "shared_in_central_agent": true },
24+
"direct_solar_irradiance_predicted_1": { "active": true, "shared_in_central_agent": true },
25+
"carbon_intensity": { "active": true, "shared_in_central_agent": true },
26+
"non_shiftable_load": { "active": true, "shared_in_central_agent": false },
27+
"solar_generation": { "active": true, "shared_in_central_agent": false },
28+
"electrical_storage_soc": { "active": true, "shared_in_central_agent": false },
29+
"net_electricity_consumption": { "active": true, "shared_in_central_agent": false },
30+
"electricity_pricing": { "active": true, "shared_in_central_agent": true },
31+
"electricity_pricing_predicted_1": { "active": true, "shared_in_central_agent": true },
32+
"electricity_pricing_predicted_2": { "active": true, "shared_in_central_agent": true },
33+
"electricity_pricing_predicted_3": { "active": true, "shared_in_central_agent": true },
34+
"electric_vehicle_charger_connected_state": { "active": true, "shared_in_central_agent": true },
35+
"connected_electric_vehicle_at_charger_battery_capacity": { "active": true, "shared_in_central_agent": true },
36+
"connected_electric_vehicle_at_charger_departure_time": { "active": true, "shared_in_central_agent": true },
37+
"connected_electric_vehicle_at_charger_required_soc_departure": { "active": true, "shared_in_central_agent": true },
38+
"connected_electric_vehicle_at_charger_soc": { "active": true, "shared_in_central_agent": true },
39+
"electric_vehicle_charger_incoming_state": { "active": true, "shared_in_central_agent": true },
40+
"incoming_electric_vehicle_at_charger_estimated_arrival_time": { "active": true, "shared_in_central_agent": true }
41+
},
42+
"actions": {
43+
"electrical_storage": { "active": true },
44+
"electric_vehicle_storage": { "active": true }
45+
},
46+
"agent": {
47+
"type": "citylearn.agents.rbc.BasicElectricVehicleRBC_ReferenceController",
48+
"attributes": {}
49+
},
50+
"reward_function": {
51+
"type": "citylearn.reward_function.RewardFunction",
52+
"attributes": {}
53+
},
54+
"electric_vehicles_def": {
55+
"Electric_Vehicle_1": {
56+
"include": true,
57+
"battery": {
58+
"type": "citylearn.energy_model.Battery",
59+
"autosize": false,
60+
"attributes": {
61+
"capacity": 20.0,
62+
"nominal_power": 7.2,
63+
"initial_soc": 0.4,
64+
"efficiency": 0.95
65+
}
66+
}
67+
}
68+
},
69+
"buildings": {
70+
"Building_1": {
71+
"include": true,
72+
"energy_simulation": "Building_1.csv",
73+
"weather": "weather.csv",
74+
"carbon_intensity": "carbon_intensity.csv",
75+
"pricing": "pricing.csv",
76+
"inactive_observations": [],
77+
"inactive_actions": [],
78+
"electrical_storage": {
79+
"type": "citylearn.energy_model.Battery",
80+
"autosize": false,
81+
"attributes": {
82+
"capacity": 5.0,
83+
"nominal_power": 5.0,
84+
"initial_soc": 0.5,
85+
"efficiency": 0.9
86+
}
87+
},
88+
"chargers": {
89+
"charger_1_1": {
90+
"type": "citylearn.electric_vehicle_charger.Charger",
91+
"charger_simulation": "charger_1_1.csv",
92+
"autosize": false,
93+
"attributes": {
94+
"nominal_power": 7.2,
95+
"efficiency": 0.95,
96+
"charger_type": 0,
97+
"max_charging_power": 7.2,
98+
"min_charging_power": 0.0,
99+
"max_discharging_power": 7.2,
100+
"min_discharging_power": 0.0
101+
}
102+
}
103+
}
104+
}
105+
}
106+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
outdoor_dry_bulb_temperature,outdoor_relative_humidity,diffuse_solar_irradiance,direct_solar_irradiance,outdoor_dry_bulb_temperature_predicted_1,outdoor_dry_bulb_temperature_predicted_2,outdoor_dry_bulb_temperature_predicted_3,outdoor_relative_humidity_predicted_1,outdoor_relative_humidity_predicted_2,outdoor_relative_humidity_predicted_3,diffuse_solar_irradiance_predicted_1,diffuse_solar_irradiance_predicted_2,diffuse_solar_irradiance_predicted_3,direct_solar_irradiance_predicted_1,direct_solar_irradiance_predicted_2,direct_solar_irradiance_predicted_3
2+
5.0,60.0,0.0,0.0,5.0,5.0,5.0,60.0,60.0,60.0,0.0,0.0,0.0,0.0,0.0,0.0
3+
5.2,59.5,0.0,0.0,5.0,5.1,5.2,60.0,59.8,59.5,0.0,0.0,0.0,0.0,0.0,0.0
4+
5.5,59.0,0.0,0.0,5.3,5.2,5.0,59.7,59.5,59.0,0.0,0.0,0.0,0.0,0.0,0.0
5+
6.0,58.5,50.0,30.0,5.8,5.5,5.2,59.3,59.0,58.5,40.0,30.0,20.0,20.0,10.0,0.0
6+
6.5,58.0,90.0,60.0,6.2,5.8,5.5,58.8,58.5,58.0,60.0,50.0,40.0,40.0,20.0,10.0
7+
6.3,57.5,120.0,80.0,6.0,5.6,5.3,58.0,57.8,57.5,70.0,60.0,50.0,50.0,40.0,20.0
8+
6.1,57.0,70.0,40.0,5.8,5.4,5.2,57.5,57.3,57.0,50.0,40.0,30.0,30.0,20.0,10.0
9+
5.8,56.5,20.0,10.0,5.5,5.2,5.0,57.0,56.8,56.5,20.0,10.0,0.0,15.0,10.0,5.0
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
"""EV reference controller run for the minute-level dataset with export enabled."""
3+
4+
from __future__ import annotations
5+
6+
import sys
7+
from pathlib import Path
8+
9+
import numpy as np
10+
11+
ROOT = Path(__file__).resolve().parents[2]
12+
if str(ROOT) not in sys.path:
13+
sys.path.insert(0, str(ROOT))
14+
15+
from citylearn.agents.rbc import BasicElectricVehicleRBC_ReferenceController as Agent # noqa: E402
16+
from citylearn.citylearn import CityLearnEnv # noqa: E402
17+
18+
SCHEMA = ROOT / "tests/data/minute_ev_demo/schema.json"
19+
20+
21+
def main() -> None:
22+
render_root = ROOT / "SimulationData"
23+
env = CityLearnEnv(
24+
str(SCHEMA),
25+
central_agent=True,
26+
render_mode="end",
27+
render_directory=render_root,
28+
random_seed=0,
29+
)
30+
31+
try:
32+
controller = Agent(env)
33+
observations, _ = env.reset()
34+
35+
for _ in range(env.episode_tracker.episode_time_steps):
36+
actions = controller.predict(observations, deterministic=True)
37+
observations, _, terminated, truncated, _ = env.step(actions)
38+
39+
if terminated or truncated:
40+
break
41+
42+
outputs_path = Path(env.new_folder_path)
43+
print(f"Exports written to: {outputs_path}")
44+
finally:
45+
env.close()
46+
47+
48+
if __name__ == "__main__":
49+
main()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
import shutil
5+
from pathlib import Path
6+
7+
import numpy as np
8+
import pytest
9+
10+
pytest.importorskip("gymnasium")
11+
12+
from citylearn.citylearn import CityLearnEnv
13+
14+
15+
DATASET = Path(__file__).resolve().parents[2] / "tests/data/minute_ev_demo/schema.json"
16+
17+
18+
def test_minute_level_export(tmp_path):
19+
env = CityLearnEnv(
20+
str(DATASET),
21+
central_agent=True,
22+
render_mode="during",
23+
render_directory=tmp_path,
24+
random_seed=0,
25+
)
26+
27+
try:
28+
env.reset()
29+
zeros = [np.zeros(env.action_space[0].shape[0], dtype="float32")]
30+
31+
while not (env.terminated or env.truncated):
32+
_, _, terminated, truncated, _ = env.step(zeros)
33+
if terminated or truncated:
34+
break
35+
36+
outputs_path = Path(env.new_folder_path)
37+
community_export = outputs_path / "exported_data_community_ep0.csv"
38+
assert community_export.exists()
39+
40+
with community_export.open() as f:
41+
header = next(f)
42+
first_row = next(f).strip()
43+
second_row = next(f).strip()
44+
45+
first_ts = first_row.split(",")[0]
46+
second_ts = second_row.split(",")[0]
47+
48+
delta = datetime.fromisoformat(second_ts) - datetime.fromisoformat(first_ts)
49+
assert delta.total_seconds() == pytest.approx(env.seconds_per_time_step)
50+
finally:
51+
outputs_root = getattr(env, "new_folder_path", None)
52+
env.close()
53+
if outputs_root is not None:
54+
shutil.rmtree(outputs_root, ignore_errors=True)

0 commit comments

Comments
 (0)