Commit e71b4b6d authored by hazrmard's avatar hazrmard
Browse files

controller script tuning, dynamically configurable settings, docs for deployment

parent 05bae9e6
Loading
Loading
Loading
Loading
+84 −3
Changes for README.md: 84 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -2,12 +2,93 @@

Smart control of HVAC systems using data-driven methods.

Documentation at https://hazrmard.github.io/EngineeringScienceBuilding
[Data documentation](https://hazrmard.github.io/EngineeringScienceBuilding)

Source repository: https://git.isis.vanderbilt.edu/SmartBuildings/EngineeringScienceBuilding
[Source repository](https://git.isis.vanderbilt.edu/SmartBuildings/EngineeringScienceBuilding)

GitHub mirror: https://github.com/hazrmard/EngineeringScienceBuilding
[GitHub mirror](https://github.com/hazrmard/EngineeringScienceBuilding)

## About

This repository contains code for analysis and control of various HVAC systems at Engineering Science Building.

## Installation and Use

Environments are created using [Miniconda](https://docs.conda.io/en/latest/miniconda.html) for python 3. The following steps assume that the miniconda executable `conda` is on the system `PATH`.

### ESB condenser water control

To run the controller, review control settings, create the production environment, activate it, and call the script.

The environment needs to be created only once. Every time after, the environment is activated and the scipt is called.

**Before running the controller**

The controller settings can be specified in a settings file. The default settings file is located in `src/settings.ini`. A file can be created at another location but the file path must be specified when running the controller script. To create a custom settings file, just copy `src/settings.ini` to a location of your choice.

1. Fill in the `username` and `password` values for accessing BDX in the settings file.

2. The locations of the `output` and `logs` files can also be specified in settings. Or they can be provided as commandline arguments at runtime.

3. Choose optimization target by specifying `power` or `temperature` in `target` setting.  `power` tries to mimimize the sum of chiller, fan, and condenser pump powers. `temperature` minimizes condenser input water temperature.

4. Choose the acceptable setpoint bounds in the `bounds` setting. The bounds are additionaly clipped by the Wetbulb temperature internally which is the lower limit on what is physically possible.

**Controller interface**

```bash
conda env create -f environment.yml  # create environment
conda activate esb-prod              # activate environment
python ./src/controller.py --help    # generate script help message

usage: controller.py [-h] [-i INTERVAL] [-t {power,temperature}] [-o OUTPUT]
                     [-s SETTINGS] [-l LOGS]
                     [-v {CRITICAL,ERROR,WARNING,INFO,DEBUG}] [-d]

Condenser set-point optimization script.

optional arguments:
  -h, --help            show this help message and exit
  -i INTERVAL, --interval INTERVAL
                        Interval in seconds to apply control action.
  -t {power,temperature}, --target {power,temperature}
                        Optimization target for condenser water setpoint.
  -o OUTPUT, --output OUTPUT
                        Location of file to write output to.
  -s SETTINGS, --settings SETTINGS
                        Location of settings file.
  -l LOGS, --logs LOGS  Location of file to write logs to.
  -v {CRITICAL,ERROR,WARNING,INFO,DEBUG}, --verbosity {CRITICAL,ERROR,WARNING,INFO,DEBUG}
                        Verbosity level.
  -d, --dry-run         Exit after one action to test script.
```

Example commands:

```bash
# activate production environment
conda activate esb-prod

# Take action every 300s and output to out.txt. Logs default to ./logs.txt
python src/controller.py --interval 300 --output ./out.txt

# or, this will exit after taking one action as a test run:
python src/controller.py --interval 300 --output ./out.txt --dry-run

# or, this will read settings from a custom location
# If settings are provided at command line and in settings file, commandline
# will take precedence.
python src/controller.py --interval 300 --output ./out.txt --settings ~/ESB/mysettings.ini

```

### Development

For development, install the environment described in `dev.yml`:

```bash
conda env create -f dev.yml
conda activate esb
```

There is a minority of code that may depend on [pyTorch](https://pytorch.org/get-started/locally/). Depending on GPU availability, install the appropriate version.

dev.yml

0 → 100644
+18 −0
Changes for dev.yml: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
name: esb
channels:
  - conda-forge
dependencies:
  - python=3.7
  - numpy
  - pandas
  - matplotlib
  - xlrd
  - jupyter
  - tqdm
  - scikit-learn
  - pip
  - pip:
    - python-dateutil
    - ipyvolume
    - gym
    - git+https://git.isis.vanderbilt.edu/SmartBuildings/bdx@v0.2.5
 No newline at end of file
+3 −5
Changes for environment.yml: 3 added lines, 5 removed lines.
Original line number Diff line number Diff line
name: esb
name: esb-prod
channels:
  - conda-forge
dependencies:
@@ -7,12 +7,10 @@ dependencies:
  - pandas
  - matplotlib
  - xlrd
  - jupyter
  - tqdm
  - scikit-learn
  - pip
  - pip:
    - dateutil
    - ipyvolume
    - python-dateutil
    - gym
    - git+https://git.isis.vanderbilt.edu/SmartBuildings/bdx@v0.2.4
 No newline at end of file
    - git+https://git.isis.vanderbilt.edu/SmartBuildings/bdx@v0.2.5
 No newline at end of file
+1768 −2

File changed.

Preview size limit exceeded, changes collapsed.

+37 −13
Changes for src/controller.py: 37 added lines, 13 removed lines.
Original line number Diff line number Diff line
@@ -24,25 +24,34 @@ from sklearn.base import BaseEstimator
import bdx



def make_arguments() -> ArgumentParser:
SOURCECODE_DIR = os.path.dirname(os.path.abspath(__file__))
WORKING_DIR = os.path.abspath(os.getcwd())
    SETTINGS_FILE = os.path.join(SOURCECODE_DIR, 'settings.ini')
    LOG_FILE = os.path.join(SOURCECODE_DIR, 'log.txt')
DEFAULT_PATHS = dict(
    settings=os.path.join(SOURCECODE_DIR, 'settings.ini'),
    logs=os.path.join(WORKING_DIR, 'log.txt'),
    output=os.path.join(WORKING_DIR, 'output.txt')
)



def make_arguments() -> ArgumentParser:
    parser = ArgumentParser(description='Condenser set-point optimization script.')
    parser.add_argument('-i', '--interval', type=int, required=False, default=None,
                        help='Interval in seconds to apply control action.')
    parser.add_argument('-t', '--target', type=str, required=False, default=None,
                        help='Optimization target for condenser water setpoint.',
                        choices=('power', 'temperature'))
    parser.add_argument('-o', '--output', type=str, required=False, default=None,
                        help='Location of file to write output to.')
    parser.add_argument('-s', '--settings', type=str, required=False, default=SETTINGS_FILE,
    parser.add_argument('-s', '--settings', type=str, required=False, default=None,
                        help='Location of settings file.')
    parser.add_argument('-l', '--logfile', type=str, required=False, default=LOG_FILE,
    parser.add_argument('-l', '--logs', type=str, required=False, default=None,
                        help='Location of file to write logs to.')
    parser.add_argument('-v', '--verbosity', type=str, required=False, default=None,
                        help='Verbosity level.',
                        choices=('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'))
    parser.add_argument('-d', '--dry-run', required=False, default=False,
                        action='store_true', help='Exit after one action to test script.')
    
    return parser

@@ -55,8 +64,12 @@ def get_settings(parsed_args) -> dict:
    cfg = ConfigParser()
    cfg.read(parsed_args.settings)
    for setting, value in cfg['DEFAULT'].items():
        if (setting not in settings) or (settings[setting] is None):
        # Only update settings which were not specified in the command line
        if (setting not in settings) or (settings.get(setting) is None):
            settings[setting] = value
    for setting in ('output', 'logs', 'settings'):
        if settings.get(setting) is None:
            settings[setting] = DEFAULT_PATHS[setting]
    return settings


@@ -72,7 +85,7 @@ def make_logger(**settings) -> logging.Logger:
    formatter = logging.Formatter('%(asctime)s, %(levelname)s, %(message)s',
                                  datefmt='%Y-%m-%d %H:%M:%S')

    handler_file = logging.FileHandler(filename=settings['logfile'], mode='a')
    handler_file = logging.FileHandler(filename=settings['logs'], mode='a')
    handler_file.setFormatter(formatter)
    logger.addHandler(handler_file)

@@ -89,11 +102,15 @@ def get_controller(**settings) -> BaseEstimator:
    from baseline_control import SimpleFeedbackController
    class Controller(SimpleFeedbackController):

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.target = str(settings['target']).lower()
    
        def feedback(self, X):
            if settings['target'].lower() == 'power':
                return - X['PowChi'] - X['PowFanA'] - X['PowFanB'] - X['PowConP']
            else:
            if self.target == 'temperature':
                return -X['TempCondIn']
            else:
                return - X['PowChi'] - X['PowFanA'] - X['PowFanB'] - X['PowConP']
        
        def starting_action(self, X):
            return np.asarray([X['TempWetBulb'] + 4])
@@ -119,6 +136,7 @@ def update_controller(ctrl, **settings):
    ctrl.stepsize = stepsize
    ctrl.window = window
    ctrl.bounds = np.asarray([setpoint_bounds])
    ctrl.target = str(settings['target']).lower()
    return ctrl


@@ -177,8 +195,14 @@ if __name__ == '__main__':
                action, = ctrl.predict(state)
                logger.info('Setpoint: {}'.format(action))
                put_control_action(action, **settings)
            ev_halt.wait(float(settings['interval']) -\
                         (datetime.now(pytz.utc).timestamp() - start.timestamp()))
            if settings['dry_run']:
                logger.info('Dry run finished. Halting.')
                ev_halt.set()
            else:
                time_taken = datetime.now(pytz.utc).timestamp() - start.timestamp()
                time_left = float(settings['interval']) - time_taken
                logger.info('Waiting for {:.1f}s'.format(time_left))
                ev_halt.wait(time_left)
        except KeyboardInterrupt:
            ev_halt.set()
            logger.info('Keyboard interrupt. Halting.')
Loading