Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3b18e9a
Runtime analysis, Render reimagining
Pireza Dec 2, 2025
b978422
Lag endogenous agent observations without API changes
calofonseca Mar 10, 2026
91b7fa7
Guard terminal step and document RL time semantics
calofonseca Mar 10, 2026
285e575
Remove -2 loop bounds in EV SOC behavior tests
calofonseca Mar 10, 2026
49014a5
Fix subhour EV/battery scaling and mutable ratio default
calofonseca Mar 10, 2026
272365f
Add EV arrival override and subhour consistency regression tests
calofonseca Mar 10, 2026
c1dfce6
test: normalize local pytest collection and add runtime benchmark too…
calofonseca Mar 11, 2026
13caf06
perf: optimize step/render hot paths and simplify reward observation …
calofonseca Mar 11, 2026
4bc4fd6
fix: validate episode splits and guard oversized episode_time_steps
calofonseca Mar 11, 2026
a415f9e
chore: move manual scripts to scripts/manual and clean runtime artifacts
calofonseca Mar 11, 2026
c207728
refactor: extract episode export logic into internal exporter component
calofonseca Mar 11, 2026
f0ba0d8
chore: remove unused legacy renderer modules
calofonseca Mar 11, 2026
23a7e7a
ci: add lint matrix tests and performance smoke regression check
calofonseca Mar 11, 2026
61ce013
Add CI perf baseline checks and EV/battery/PV scenario smoke tests
calofonseca Mar 11, 2026
ab626fa
Add negative action-path tests and clean exporter runtime logging
calofonseca Mar 11, 2026
4264985
Introduce internal services scaffold and extract env loading pipeline
calofonseca Mar 11, 2026
834bfa9
Extract env runtime orchestration into internal runtime service
calofonseca Mar 11, 2026
3404531
Extract building observation/action orchestration into internal ops s…
calofonseca Mar 11, 2026
4e506aa
Extract KPI evaluation logic into internal KPI service
calofonseca Mar 11, 2026
d461f8a
Add refactor parity regression tests and internal architecture docs
calofonseca Mar 11, 2026
b01f8b0
test: add UI export contract regression coverage
calofonseca Mar 11, 2026
16a2780
rename charging constraints demo dataset to three-phase electrical se…
calofonseca Mar 12, 2026
f959287
add electrical service constraints, market settlement KPIs, and expor…
calofonseca Mar 12, 2026
abfd86f
align subhour KPI test normalization with epsilon-safe division
calofonseca Mar 13, 2026
c50e7c2
prepare softcpsrecsimulator 0.1.0 release
calofonseca Mar 13, 2026
c5f7308
customize PyPI metadata and long description for softcpsrecsimulator
calofonseca Mar 13, 2026
75b7d3e
Fix t0 accumulation and battery degradation scaling
calofonseca Mar 17, 2026
c1a625b
Harden action parsing for central and decentralized agents
calofonseca Mar 17, 2026
ce4070e
Handle non-finite electrical limits without NaN propagation
calofonseca Mar 17, 2026
6b8562f
Fix EV SOC history when charger action is zero
calofonseca Mar 17, 2026
9bd882e
Harden terminal transition series handling and EV drift RNG
calofonseca Mar 17, 2026
3cbd123
Harden schema boolean parsing and electrical service validation
calofonseca Mar 17, 2026
e0cf6a2
Fix single-step learn loop and add reproducibility regressions
calofonseca Mar 17, 2026
636b359
Fix baseline cost KPIs when tariff sum is zero
calofonseca Mar 17, 2026
9f71acf
Fix non-heat-pump heating input power source
calofonseca Mar 17, 2026
cefdf59
Honor runtime random_seed override during loading
calofonseca Mar 17, 2026
a8e7499
Make EV unconnected drift time-aware and add reproducibility tests
calofonseca Mar 17, 2026
0257c97
Align scenario and KPI tests with committed terminal series
calofonseca Mar 17, 2026
8032aec
Fix duplicated without-storage properties and enforce ndarray contract
calofonseca Mar 17, 2026
cea9d6c
Keep KPI export rows for undefined metrics to stabilize CSV schema
calofonseca Mar 17, 2026
eaf87d3
Bump version to 0.1.2
calofonseca Mar 17, 2026
4701bfd
Prefer local misc sizing data before GitHub API fallback
calofonseca Mar 18, 2026
5d04da5
Prepare upstream publish metadata for CityLearn 2.6.0b1
calofonseca Mar 18, 2026
e8c29e4
test signed commit
calofonseca Mar 18, 2026
a76246c
Relax OpenStudio pin to match available PyPI versions
calofonseca Mar 18, 2026
74ba2f2
Make PySAM optional to avoid install failures on unsupported environm…
calofonseca Mar 18, 2026
7fb00d6
Raise doe_xstock minimum to avoid OpenStudio 3.3 resolver path
calofonseca Mar 19, 2026
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
71 changes: 62 additions & 9 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,90 @@
name: Repository Tests
name: CI

on:
push:
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-24.04

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install lint dependencies
run: |
python -m pip install --upgrade pip
pip install ruff

- name: Lint (critical rules)
run: |
python -m ruff check citylearn tests scripts/manual scripts/ci --select E9,F821

test:
needs: lint
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10"]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: ${{ matrix.python-version }}

- name: Cache pip dependencies
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('requirements.txt', 'test_requirements.txt', 'setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-py${{ matrix.python-version }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r test_requirements.txt
pip install gymnasium
pip install -e .


- name: Run tests
run: python -m pytest --ignore=tests/scripts
run: python -m pytest -q --ignore=scripts/manual

- name: Performance smoke check
if: matrix.python-version == '3.10'
run: |
mkdir -p ${{ runner.temp }}/perf
python scripts/ci/perf_smoke.py \
--episode-steps 600 \
--seconds 60 \
--none-max-ms 30 \
--end-max-ms 45 \
--ratio-max 2.0 \
--baseline-file scripts/ci/perf_baseline.json \
--baseline-regression-ratio 3.0 \
--baseline-slack-ms 10.0 \
--metrics-output ${{ runner.temp }}/perf/perf_smoke_report.json

- name: Upload performance smoke report
if: always() && matrix.python-version == '3.10'
uses: actions/upload-artifact@v4
with:
name: perf-smoke-report
path: ${{ runner.temp }}/perf/perf_smoke_report.json
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,5 @@ examples/*
render_logs/
SimulationData/
results/
runtime.csv
*-runtime.csv
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include citylearn/assets/*
recursive-include citylearn/misc *
recursive-include citylearn/misc *
include requirements.txt
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Install latest release in PyPi with `pip`:
pip install CityLearn
```

Optional dependency for PV autosizing (`PySAM`):
```console
pip install "CityLearn[pysam]"
```

## Documentation
Refer to the [docs](https://intelligent-environments-lab.github.io/CityLearn/).

Expand Down
2 changes: 1 addition & 1 deletion citylearn/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.5.0'
__version__ = '2.6.0b1'
31 changes: 21 additions & 10 deletions citylearn/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,12 @@ def learn(self, episodes: int = None, deterministic: bool = None, deterministic_
deterministic = deterministic or (deterministic_finish and episode >= episodes - 1)
observations, _ = self.env.reset()
self.episode_time_steps = self.episode_tracker.episode_time_steps
terminated = False
terminated = self.env.terminated
truncated = self.env.truncated
time_step = 0
rewards_list = []

while not terminated:
while not (terminated or truncated):
actions = self.predict(observations, deterministic=deterministic)

# apply actions to citylearn_env
Expand All @@ -176,13 +177,23 @@ def learn(self, episodes: int = None, deterministic: bool = None, deterministic_

time_step += 1

rewards = np.array(rewards_list, dtype='float')
rewards_summary = {
'min': rewards.min(axis=0),
'max': rewards.max(axis=0),
'sum': rewards.sum(axis=0),
'mean': rewards.mean(axis=0)
}
if len(rewards_list) > 0:
rewards = np.array(rewards_list, dtype='float')
rewards_summary = {
'min': rewards.min(axis=0),
'max': rewards.max(axis=0),
'sum': rewards.sum(axis=0),
'mean': rewards.mean(axis=0)
}
else:
reward_length = len(self.action_space)
empty = np.zeros(reward_length, dtype='float')
rewards_summary = {
'min': empty,
'max': empty,
'sum': empty,
'mean': empty,
}
logging.info(f'Completed episode: {episode + 1}/{episodes}, Reward: {rewards_summary}')

def predict(self, observations: List[List[float]], deterministic: bool = None) -> List[List[float]]:
Expand Down Expand Up @@ -282,4 +293,4 @@ def predict(self, observations: List[List[float]], deterministic: bool = None) -
self.actions = actions
self.next_time_step()

return actions
return actions
19 changes: 16 additions & 3 deletions citylearn/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,22 @@ def __next_episode_time_steps(self, episode_time_steps: Union[int, List[Tuple[in

splits = None

if isinstance(episode_time_steps, List):
if isinstance(episode_time_steps, list):
splits = episode_time_steps

else:
if episode_time_steps <= 0:
raise ValueError(f'episode_time_steps must be >= 1, got {episode_time_steps}.')

earliest_start_time_step = self.__simulation_start_time_step
latest_start_time_step = (self.__simulation_end_time_step + 1) - episode_time_steps

if latest_start_time_step < earliest_start_time_step:
raise ValueError(
f'episode_time_steps ({episode_time_steps}) exceeds available simulation window '
f'({self.simulation_time_steps}). Reduce episode_time_steps or adjust '
'simulation_start_time_step/simulation_end_time_step.'
)

if rolling_episode_split:
start_time_steps = range(earliest_start_time_step, latest_start_time_step + 1)
Expand All @@ -118,10 +128,13 @@ def __next_episode_time_steps(self, episode_time_steps: Union[int, List[Tuple[in
splits = np.array([start_time_steps, end_time_steps], dtype=int).T
splits = splits.tolist()

if len(splits) == 0:
raise ValueError('No valid episode splits could be created from the provided episode_time_steps.')

if random_episode_split:
seed = int(random_seed*(self.episode + 1))
nprs = np.random.RandomState(seed)
ix = nprs.choice(len(splits) - 1)
ix = 0 if len(splits) == 1 else int(nprs.choice(len(splits)))

else:
ix = self.episode%len(splits)
Expand Down Expand Up @@ -269,4 +282,4 @@ def reset_time_step(self):
Sets `time_step` to 0.
"""

self.__time_step = 0
self.__time_step = 0
Loading
Loading