Skip to content

Commit

Permalink
Merge pull request #138 from ibpsa/issue133_parallelLearning
Browse files Browse the repository at this point in the history
Issue133 parallel learning
  • Loading branch information
javiarrobas authored Nov 16, 2023
2 parents a4f9b0e + 9b48ab8 commit f5cf4c6
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 7 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ jobs:
ls ${{ github.workspace }}
- name: Test local version
run: make test-local-in-container
test-vectorized:
runs-on: ubuntu-latest
defaults:
run:
working-directory: testing
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Pull boptestgym image from registry
run: make pull-boptestgym
- run: echo "The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: List of files in the repository
run: |
ls ${{ github.workspace }}
- name: Test vectorized environment
run: make test-vectorized-in-container
test-service:
runs-on: ubuntu-latest
defaults:
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ Running BOPTEST locally is substantially faster
2) Run a BOPTEST case with the building emulator model to be controlled (instructions [here](https://github.com/ibpsa/project1-boptest/blob/master/README.md)).
3) Check out the `master` branch of this repository and run the example above replacing the url to be `url = 'http://127.0.0.1:5000'` and avoiding the `testcase` argument to the `BoptestGymEnv` class.

## Quick-Start (running BOPTEST locally in a vectorized environment)

To facilitate the training and testing process, we provide scripts that automate the deployment of multiple BOPTEST instances using Docker Compose and then train an RL agent with a vectorized BOPTEST-gym environment. The deployment dynamically checks for available ports, generates a Docker Compose YAML file, and takes care of naming conflicts to ensure smooth deployment.
Running a vectorized environment allows you to deploy as many BoptestGymEnv instances as cores you have available for the agent to learn from all of them in parallel (see [here](https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html) for more information, we specifically use [`SubprocVecEnv`](https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html#subprocvecenv)). This substantially speeds up the training process.

### Usage

1. Specify the BOPTEST root directory either by passing it as a command-line argument or by defining the `boptest_root` variable at the beginning of the script `generateDockerComposeYml.py`. The script prioritizes the command-line argument if provided. Users are allowed to change the Start Port number and Total Services as needed.

Example using command-line argument:

```bash
python generateDockerComposeYml.py absolute_boptest_root_dir
```

2. Train an RL agent with parallel learning with the vectorized BOPTEST-gym environment. See `/examples/run_vectorized.py` for an example on how to do so.

## Versioning and main dependencies

Current BOPTEST-Gym version is `v0.5.0` which is compatible with BOPTEST `v0.5.0`
Expand Down
163 changes: 163 additions & 0 deletions examples/run_vectorized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import os
import sys
import yaml
import torch
import random

from testing import utilities
from stable_baselines3 import DQN
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.callbacks import EvalCallback
from stable_baselines3.common.vec_env.vec_monitor import VecMonitor
from stable_baselines3.common.logger import configure
from boptestGymEnv import BoptestGymEnv, NormalizedObservationWrapper, DiscretizedActionWrapper

def generate_urls_from_yml(boptest_root_dir):
'''Method that returns as many urls for BOPTEST-Gym environments
as those specified at the BOPTEST `docker-compose.yml` file.
It assumes that `generateDockerComposeYml.py` has been called first.
Parameters
----------
boptest_root_dir: str
String with directory to BOPTEST where the `docker-compose.yml`
file should be located.
Returns
-------
urls: list
List of urls where BOPTEST test cases will be allocated.
'''
docker_compose_loc = os.path.join(boptest_root_dir, "docker-compose.yml")

# Read the docker-compose.yml file
with open(docker_compose_loc, 'r') as stream:
try:
docker_compose_data = yaml.safe_load(stream)
services = docker_compose_data.get('services', {})

# Extract the port and URL of the service
urls = []
for service, config in services.items():
ports = config.get('ports', [])
for port in ports:
# Extract host port
host_port = port.split(':')[1]
urls.append(f'http://127.0.0.1:{host_port}')

print(urls) # Print URLs

except yaml.YAMLError as exc:
print(exc)

return urls

def make_env(url, seed):
''' Function that instantiates the environment.
Parameters
----------
url: string
Rest API url for communication with this environment.
seed: integer
Seed for random starting times of episodes in this environment.
'''

def _init():
random.seed(seed)
env = BoptestGymEnv(
url=url,
actions=['oveHeaPumY_u'],
observations={
'time': (0, 604800),
'reaTZon_y': (280., 310.),
'TDryBul': (265, 303),
'HDirNor': (0, 862),
'InternalGainsRad[1]': (0, 219),
'PriceElectricPowerHighlyDynamic': (-0.4, 0.4),
'LowerSetp[1]': (280., 310.),
'UpperSetp[1]': (280., 310.)
},
scenario={'electricity_price': 'dynamic'},
predictive_period=24 * 3600,
regressive_period=6 * 3600,
random_start_time=True,
excluding_periods=[(16 * 24 * 3600, 30 * 24 * 3600), (108 * 24 * 3600, 122 * 24 * 3600)],
max_episode_length=24 * 3600,
warmup_period=24 * 3600,
step_period=15 * 60
)
env = NormalizedObservationWrapper(env) # Add observation normalization if needed
env = DiscretizedActionWrapper(env, n_bins_act=10) # Add action discretization if needed

return env

return _init

def train_DQN_vectorized(venv,
log_dir=os.path.join(utilities.get_root_path(), 'examples', 'agents', 'DQN_vectorized')):
'''Method to train DQN agent using vectorized environment.
Parameters
----------
venv: stable_baselines3.common.vec_env.SubprocVecEnv
vectorized environment to be learned.
'''

# Create logging directory if not exists. Monitoring data and agent model will be stored here
os.makedirs(log_dir, exist_ok=True)

# Modify the environment to include the callback
venv = VecMonitor(venv=venv, filename=os.path.join(log_dir,'monitor.csv'))

# Create the callback: evaluate with one episode after 100 steps for training. We keep it very short for testing.
# When using multiple environments, each call to ``env.step()`` will effectively correspond to ``n_envs`` steps.
# To account for that, you can use ``eval_freq = eval_freq/venv.num_envs``
eval_freq = 100
eval_callback = EvalCallback(venv, best_model_save_path=log_dir, log_path=log_dir,
eval_freq=int(eval_freq/venv.num_envs), n_eval_episodes=1, deterministic=True)

# Try to find CUDA core since it's optimized for parallel computing tasks
device = 'cuda' if torch.cuda.is_available() else 'cpu'

# Instantiate an RL agent with DQN
model = DQN('MlpPolicy', venv, verbose=1, gamma=0.99, learning_rate=5e-4,
batch_size=24, seed=123456, buffer_size=365 * 24,
learning_starts=24, train_freq=1, exploration_initial_eps=1.0,
exploration_final_eps=0.01, exploration_fraction=0.1, device=device)

# set up logger
new_logger = configure(log_dir, ['csv'])
model.set_logger(new_logger)

# Main training loop
model.learn(total_timesteps=100, callback=eval_callback)

if __name__ == '__main__':

boptest_root = "./"

# Get the argument from command line when use Linux
if len(sys.argv) >= 2:
boptest_root_dir = sys.argv[1]
else:
boptest_root_dir = boptest_root

# Use URLs obtained from docker-compose.yml
urls = generate_urls_from_yml(boptest_root_dir=boptest_root_dir)

# Create BOPTEST-Gym environment replicas
envs = [make_env(url) for url in urls]

# Create a vectorized environment using SubprocVecEnv
venv = SubprocVecEnv(envs)

# Train vectorized environment
train_DQN_vectorized(venv)






79 changes: 79 additions & 0 deletions generateDockerComposeYml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import os
import socket
import shutil
import yaml
import sys


boptest_root = "./" # You can define boptest_root_dir here when use IDLE

# Get the argument from command line when use Linux
if len(sys.argv) >= 2:
boptest_root_dir = sys.argv[1]
else:
boptest_root_dir = boptest_root

num_services = 2 # Total Services needed
base_port = 5000 # Start Port number


# Function to check if a port is available
def is_port_available(port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
return sock.connect_ex(('localhost', port)) != 0


services = {}
last_assigned_port = base_port - 1 # Initial value set to one less than the first port to be checked
for _ in range(num_services):
port = last_assigned_port + 1 # Start checking from the next port after the last assigned one

# If the port is unavailable, continue to check the next one
while not is_port_available(port):
print(f"Port {port} is occupied.")
port += 1
if port > base_port + num_services:
raise Exception("Too many ports are occupied.")

last_assigned_port = port # Update the last assigned port

service_name = f"boptest{port}"
service_config = {
"image": "boptest_base",
"build": {"context": "."},
"volumes": [
"./testcases/${TESTCASE}/models/wrapped.fmu:${APP_PATH}/models/wrapped.fmu",
"./testcases/${TESTCASE}/doc/:${APP_PATH}/doc/",
"./restapi.py:${APP_PATH}/restapi.py",
"./testcase.py:${APP_PATH}/testcase.py",
"./version.txt:${APP_PATH}/version.txt",
"./data:${APP_PATH}/data/",
"./forecast:${APP_PATH}/forecast/",
"./kpis:${APP_PATH}/kpis/",
],
"ports": [f"127.0.0.1:{port}:5000"],
"networks": ["boptest-net"],
"restart": "on-failure" # restart on-failure
}
services[service_name] = service_config

docker_compose_content = {
"version": "3.7",
"services": services,
"networks": {
"boptest-net": {
"name": "boptest-net",
"attachable": True,
}
},
}

# Check whether the docker-compose.yml file exists in the BOPTEST root directory
docker_compose_path = os.path.join(boptest_root_dir, 'docker-compose.yml')
if os.path.exists(docker_compose_path):
# If it exists, rename to docker-compose_origin.yml
shutil.move(docker_compose_path, os.path.join(boptest_root_dir, 'docker-compose_origin.yml'))

# Create a new docker-compose.yml file in the BOPTEST root directory
with open(docker_compose_path, "w") as file:
yaml.dump(docker_compose_content, file, default_flow_style=False)
6 changes: 6 additions & 0 deletions releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

BOPTEST-Gym has two main dependencies: BOPTEST and Stable-Baselines3. For simplicity, the first two digits of the version number match the same two digits of the BOPTEST version of which BOPTEST-Gym is compatible with. For example, BOPTEST-Gym v0.3.x is compatible with BOPTEST v0.3.x. The last digit is reserved for other internal edits specific to this repository only. See [here](https://github.com/ibpsa/project1-boptest/blob/master/releasenotes.md) for BOPTEST release notes.

## BOPTEST-Gym v0.5.0-dev

Released on xx/xx/xxxx.

- Implement functionality and example with vectorized environment for parallel learning. This is for [#133](https://github.com/ibpsa/project1-boptest-gym/issues/133).

## BOPTEST-Gym v0.5.0

Released on 11/11/2023.
Expand Down
3 changes: 2 additions & 1 deletion testing/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ RUN pip install \
nbconvert==7.7.3 \
gym \
shimmy \
ipykernel==6.25.1
ipykernel==6.25.1 \
PyYAML

# Add developer user with permissions in the working directory
RUN useradd -ms /bin/bash developer
Expand Down
26 changes: 22 additions & 4 deletions testing/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ IMG_REGI=javierarroyo/boptestgym
BOPTEST_COMMIT=12ceafe42983d42e535385dee1daa1d25673e2aa

# Define current BOPTEST-Gym version (should be even with BOPTEST version defined in commit above)
VERSION = 0.5.0
VERSION = 0.5.0-dev

build-boptestgym:
docker build -f ${ROOT}/testing/Dockerfile \
Expand Down Expand Up @@ -65,20 +65,26 @@ pull-boptestgym:
docker pull ${IMG_REGI}:${VERSION}
docker tag ${IMG_REGI}:${VERSION} ${IMG_NAME}

make download-doptest:
make download-boptest:
curl -L -o boptest.zip https://github.com/ibpsa/project1-boptest/archive/${BOPTEST_COMMIT}.zip
unzip -o -q boptest.zip

run-boptest-case:
make download-doptest
make download-boptest
cd project1-boptest-${BOPTEST_COMMIT} && \
TESTCASE=bestest_hydronic_heat_pump docker-compose up -d --quiet-pull

run-boptest-case-no-cache:
make download-doptest
make download-boptest
cd project1-boptest-${BOPTEST_COMMIT} && \
TESTCASE=bestest_hydronic_heat_pump docker-compose up -d --force-recreate --build

run-boptest-vectorized:
make download-boptest && \
cd .. && python3 generateDockerComposeYml.py testing/project1-boptest-${BOPTEST_COMMIT} && \
cd testing/project1-boptest-${BOPTEST_COMMIT} && \
TESTCASE=bestest_hydronic_heat_pump docker-compose up -d --quiet-pull

stop-boptest-case:
cd project1-boptest-${BOPTEST_COMMIT} && docker-compose down

Expand All @@ -91,6 +97,10 @@ cleanup-boptest:
test-local:
python3 -m unittest test_boptestGymEnv.BoptestGymEnvTest

# Vectorized needs to run separate since modifies docker-compose.yml to have multiple boptest instances
test-vectorized:
python3 -m unittest test_boptestGymEnv.BoptestGymVecTest project1-boptest-${BOPTEST_COMMIT}

# The tutorial is using boptest-gym-service and covers most of the functionality of boptest-gym
test-service:
python3 -m unittest test_boptestGymEnv.BoptestGymServiceTest
Expand All @@ -103,6 +113,14 @@ test-local-in-container:
make stop-boptest-case
make cleanup-boptest

test-vectorized-in-container:
make run-boptest-vectorized
make run-boptestgym-detached
make exec-boptestgym ARGS="make test-vectorized"
make stop-boptestgym
make stop-boptest-case
make cleanup-boptest

test-service-in-container:
make run-boptestgym-detached
make exec-boptestgym ARGS="make test-service"
Expand Down
2 changes: 2 additions & 0 deletions testing/references/vectorized_training.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
keys,value
0,0
Loading

0 comments on commit f5cf4c6

Please sign in to comment.