Commit 9ea98333 authored by Ibrahim Ahmed's avatar Ibrahim Ahmed
Browse files

controller: New option to write settings to output directory as csv

parent 3743fb3b
Loading
Loading
Loading
Loading
+1 −0
Changes for .gitignore: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ credentials.txt
questions.txt
src/Scratch.ipynb
*.xlsx
*.csv
*.pptx
*.pdf
*.txt
+2 −17
Changes for src/Baseline-Condenser.ipynb: 2 added lines, 17 removed lines.
Original line number Diff line number Diff line
%% Cell type:code id: tags:

``` python
%matplotlib inline
%reload_ext autoreload
%autoreload 2

import datetime
import sys
from os import path, environ
import pickle
import warnings

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.neural_network import MLPRegressor
from tqdm.auto import tqdm, trange

from utils import contiguous_sequences
from plotting import model_surface, plot_surface
from systems import Condenser
from baseline_control import SimpleFeedbackController, FeedbackController

chiller_file_1 = path.join(environ['DATADIR'],
                         'EngineeringScienceBuilding',
                         '2422_ESB_HVAC_1.csv')
chiller_file_2 = path.join(environ['DATADIR'],
                         'EngineeringScienceBuilding',
                         '2841_ESB_HVAC_2.csv')

plot_path = path.join('..', 'docs', 'img')
bin_path = './bin/'
```

%% Cell type:markdown id: tags:

## Controller script demo

%% Cell type:code id: tags:

``` python
%reload_ext autoreload
%autoreload 2
from datetime import datetime, timedelta
import pytz
from controller import make_arguments, get_settings, get_controller, update_controller, get_current_state, put_control_action
```

%% Cell type:code id: tags:

``` python
parser = make_arguments()
args = parser.parse_args(['-s', './local.ini'])
settings = get_settings(args)
settings['target'] = 'temperature'
settings
```

%% Cell type:code id: tags:

``` python
ctrl = get_controller(**settings)
update_controller(ctrl, **settings)
```

%% Cell type:code id: tags:

``` python
end = datetime.now(pytz.utc)
start = end - timedelta(minutes=10)
s = get_current_state(start, end, **settings)
s
```

%% Cell type:code id: tags:

``` python
ctrl = get_controller(**settings)
actions = []
feedbacks = []
temps = []
s['TempCondIn'] = 62.
s['TempWetBulb'] = 40.
for i in range(40):
    action, = ctrl.predict(s)
    feedbacks.append(ctrl.feedback(s))
    actions.append(action)
    temps.append(s['TempCondIn'])
    if i < 10:
        s['TempCondIn'] -= 1.
    elif i < 20:
        s['TempCondIn'] += 1.
    elif i < 30:
        if actions[-1] > actions[-2]:
            s['TempCondIn'] -= 1.
        else:
            s['TempCondIn'] += 1.
    elif i < 40:
        if actions[-1] > actions[-2]:
            s['TempCondIn'] += 1.
        else:
            s['TempCondIn'] -= 1.
```

%% Cell type:code id: tags:

``` python
plt.figure(figsize=(12,8))
# plt.imshow(np.zeros((1, 20)), aspect='auto', alpha=0.3)
plt.plot(temps, label='Temp /F')
plt.plot(actions, label='Setpoint /F')
plt.grid(which='both')
for line in (10,20,30):
    plt.axvline(x=line, color='black', ls=':')
plt.axhline(y=s['TempWetBulb'], label='WetBulb /F', color='red', ls='--')
plt.axhline(y=55, label='Action lower bound', color='blue', ls='--')
plt.legend(loc='upper left')

plt.text(2, 47, 'Increasing\n(Unresponsive)')
plt.text(12, 47, 'Decreasing\n(Unresponsive)')
plt.text(22, 47, 'Same direction')
plt.text(32, 47, 'Opposite direction')
plt.title('Controller response to different feedback behaviors')
plt.xlabel('Time')
plt.ylabel('Temperature /F')

plt.twinx()
plt.plot(feedbacks, 'g:', lw=3, label='feedback')
plt.legend(loc='upper right')
plt.ylabel('Feecback /F')
```

%% Cell type:markdown id: tags:

## Environment model

State variables (12):

`'TempCondIn', 'TempCondOut', 'TempEvapOut', 'PowChi', 'PowFanA', 'PowFanB', 'PowConP', 'TempEvapIn', 'TempAmbient', 'TempWetBulb', 'PressDiffEvap', 'PressDiffCond'`

Action variables (1):

`'TempCondInSetpoint'`

Output variables (3):

`'TempCondIn', 'TempCondOut', 'TempEvapOut', 'PowChi', 'PowFanA', 'PowFanB', 'PowConP'`

Model:

`[Action, State] --> [Output]`

%% Cell type:code id: tags:

``` python
# Choosing which chiller to use
chiller_file = chiller_file_2
# Data selection 'all' or 'chiller_on' or 'fan_on'
MODE = 'chiller_on'
# Read pre-processed data:
# Pytorch uses float32 as default type for weights etc,
# so input data points are also read in the same type.
df = pd.read_csv(chiller_file, index_col='time',
                 parse_dates=['time'], dtype=np.float32)
print('Original length: {} Records'.format(len(df)))
# # These fields were not populated until 2020-07-01, so leaving then out of analysis
# df.drop(['PowFanA', 'PowFanB', 'FlowCond', 'PowChiP', 'PerFreqConP', 'PowConP'], axis='columns', inplace=True)
df.drop(['FlowCond', 'PowChiP', 'PerFreqConP'], axis='columns', inplace=True)

df.dropna(inplace=True)
if MODE == 'chiller_on':
    df = df[df['RunChi'] != 0]
if MODE == 'fan_on':
    df = df[(df['RunFanA'] != 0.) | df['RunFanB'] != 0.]
print('Processed length: {} Records'.format(len(df)))
```

%% Cell type:code id: tags:

``` python
# load model
with open(path.join(bin_path, 'v2_condenser'), 'rb') as f:
    save = pickle.load(f, fix_imports=False)
    est_cond = save['estimator']
    std_out_cond = save['output_norm']
    statevars = save['statevars']
    actionvars = save['actionvars']
    inputs = save['inputs']
    outputs = save['outputs']
    lag = save['lag']
```

%% Cell type:markdown id: tags:

### Condenser Data

%% Cell type:code id: tags:

``` python
df_in = pd.DataFrame(columns=inputs, index=df.index)
df_in['TempCondInSetpoint'] = np.clip(df['TempWetBulb'] - 4, a_min=65, a_max=None)  # approach controller
df_in[inputs[1:]] = df[inputs[1:]]

df_out = pd.DataFrame(columns=outputs, index=df.index)
df_out[outputs] = df[outputs]

idx_list = contiguous_sequences(df.index, pd.Timedelta(5, unit='min'), filter_min=10)

# Create dataframes of contiguous sequences with a delay
# of 1 time unit to indicate causality input -> outputs
dfs_in, dfs_out = [], []
for idx in idx_list:
    dfs_in.append(df_in.loc[idx[:-max(lag) if max(lag) > 0 else None]])
    cols = []
    for l, c in zip(lag, outputs):
        window = slice(l, None if l==max(lag) else -(max(lag)-l))
        series = df_out[c].loc[idx[window]]
        cols.append(series.values)
        if l == min(lag): index = series.index
    dfs_out.append(pd.DataFrame(np.asarray(cols).T, index=index, columns=outputs))

df_in = pd.concat(dfs_in, sort=False)
df_out = pd.concat(dfs_out, sort=False)

print('{:6d} time series'.format(len(dfs_in)))
print('{:6d} total rows'.format(len(df_in)))
```

%% Cell type:markdown id: tags:

## RL Environment

%% Cell type:code id: tags:

``` python
# Make wrapper for cooling tower such that outputs are normalized
# i.e. in physical units instead of being 0 mean and 1 variance.

externalvars = ('TempEvapIn', 'TempAmbient', 'TempWetBulb', 'PressDiffEvap', 'PressDiffCond')
externalvals = [df.loc[:, externalvars] for df in dfs_in]

class InvTransformer:

    def __init__(self, estimator, transformer):
        self.estimator = estimator
        self.transformer = transformer

    def predict(self, x):
        return self.transformer.inverse_transform(self.estimator.predict(x))


esb = Condenser(InvTransformer(est_cond, std_out_cond), externalvals)
```

%% Cell type:code id: tags:

``` python
# Visualize environment episode
done = False
states = []
power = []
esb.reset()
while not done:
    state, _, done, info = esb.step(esb.action_space.sample())
    states.append(state)
    power.append(info.get('powchi'))
esb.reset()

states = np.asarray(states)
power = np.asarray(power)
plt.subplot(2,1,1)
plt.plot(power, label='Total Power')
plt.legend()
plt.subplot(2,1,2)
plt.plot(states[:, 0], label='TempCondIn')
plt.plot(states[:, 1], label='TempCondOut')
plt.plot(states[:, 2], label='TempEvapOut')
plt.plot(states[:, 4], label='TempEvapIn')
plt.plot(states[:, 5], label='TempAmbient')
plt.plot(states[:, 6], label='TempWetBulb')
plt.legend()
plt.show()
```

%% Cell type:markdown id: tags:

## Simple Feedback Control

%% Cell type:code id: tags:

``` python
longest_seq_idx = max(range(len(dfs_in)), key= lambda i: len(dfs_in[i]))
```

%% Cell type:code id: tags:

``` python
dfs_in[longest_seq_idx]
```

%% Cell type:code id: tags:

``` python
# seqidx = np.random.randint(len(dfs_in))
seqidx = longest_seq_idx
simulate_hist = True  # Whether to use raw output data, or simulate it through historical actions

# indexing histories after 1st element because simulated trajectories
# are recorded after initial state (> 0), so lengths are equal
act_hist = dfs_in[seqidx].loc[:, ('TempCondInSetpoint')].values[1:, None]
ext = dfs_in[seqidx].loc[:, externalvars]

# Get baseline by running historic actions through environment:
if simulate_hist:
    esb.reset(external=ext, state0=dfs_in[seqidx].iloc[0, 1:].values)
    done = False
    pow_hist_chi, pow_hist_fan, temp_hist = [], [], []
    t = 0
    while not done:
        action = act_hist[t, :1]
        _, _, done, info = esb.step(action)
        # pow_hist_fan.append(info.get('powfans'))
        pow_hist_chi.append(info.get('powchi'))
        temp_hist.append(info.get('tempcondin'))
        t += 1
else:
    pow_hist_chi = dfs_out[seqidx]['PowChi'].values
    pow_hist_fans = dfs_out[seqidx]['PowFans'].values
    temp_hist = dfs_out[seqidx]['TempCondIn'].values
```

%% Cell type:code id: tags:

``` python
# define agent
class Controller1(SimpleFeedbackController):

    def feedback(self, X):
        return -sum(X[3:7])  # PowChi, PowFanA, PowFanB, PowConP
        # return -X[0]

    def starting_action(self, X):
        return np.asarray([X[9] + 4]) # TempWetBulb

    def clip_action(self, u, X):
        u = super().clip_action(u, X)
        return np.clip(u, a_min=X[9], a_max=None)

class Controller2(FeedbackController):

    def feedback(self, X):
        return -X[3]  # PowChi

    def starting_action(self, X):
        return None
        # return X[9] + 4 # TempWetBulb



agent_fn = lambda: Controller1(bounds=((60., 80.),), stepsize=1)
# agent_fn = lambda: Controller2(bounds=((55., 90.),), kp=1., ki=0.2, kd=0.)
```

%% Cell type:code id: tags:

``` python
pfan, pchi, act, rewards, temp = [], [], [], [], []

# run multiple trials over same period for stochastic policy
for trial in trange(1, leave=False):
    state = esb.reset(external=ext, state0=dfs_in[seqidx].iloc[0, 1:].values)
    agent = agent_fn()
    done = False
    pfan.append([])
    pchi.append([])
    act.append([])
    rewards.append([])
    temp.append([])
    while not done:
        action = agent.predict(state)[0]
        state, reward, done, info = esb.step(action)
        act[-1].append(action)
        # pfan[-1].append(info.get('powfans'))
        pchi[-1].append(info.get('powchi'))
        rewards[-1].append(reward)
        temp[-1].append(info.get('tempcondin'))

# get std_dev and mean of metrics
# std_pfan = np.std(pfan, axis=0, keepdims=False)
std_pchi = np.std(pchi, axis=0, keepdims=False)
std_act = np.std(act, axis=0, keepdims=False)
std_rewards = np.std(rewards, axis=0, keepdims=False)
std_temp = np.std(temp, axis=0, keepdims=False)

# pfan = np.mean(pfan, axis=0, keepdims=False)
pchi = np.mean(pchi, axis=0, keepdims=False)
act = np.mean(act, axis=0, keepdims=False)
rewards = np.mean(rewards, axis=0, keepdims=False)
temp = np.mean(temp, axis=0, keepdims=False)
```

%% Cell type:code id: tags:

``` python
plt.figure(figsize=(8,12))
# plt.subplot(4,1,1)
# plt.title('Fan Power (Average RL {:.0f}W vs Historical {:.0f}W)'\
#           .format(np.mean(pfan), np.mean(pow_hist_fan)))
# plt.plot(pfan, 'b:', label='RL.Fan')
# plt.fill_between(np.arange(len(pfan)), pfan+std_pfan, pfan-std_pfan, color='b', alpha=0.3)
# plt.plot(pow_hist_fan, 'r:', label='Historical.Fan')
# plt.ylim(bottom=0)
# plt.legend()

plt.subplot(3,1,1)
plt.title('Chiller Power (Average {:.0f}kW vs Historical {:.0f}kW)'\
          .format(np.mean(pchi), np.mean(pow_hist_chi)))
plt.plot(pchi, 'b:', label='Chiller')
plt.fill_between(np.arange(len(pchi)), pchi+std_pchi, pchi-std_pchi, color='b', alpha=0.3)
plt.plot(pow_hist_chi, 'r:', label='Historical.Chiller')
plt.ylim(bottom=0)
plt.legend()

plt.subplot(3,1,2)
plt.title('Setpoint Control (Average {:.2f} vs Historical {:.2f})'\
          .format(np.mean(act[:, 0]), np.mean(act_hist[:, 0])))
plt.plot(ext['TempAmbient'].values, 'g.', label='TempAmbient')
plt.plot(ext['TempWetBulb'].values, 'c.', label='TempWetBulb')
plt.plot(act[:, 0], 'b:', label='Setpoint')
plt.fill_between(np.arange(len(act[:, 0])), act[:, 0]+std_act[:, 0], act[:, 0]-std_act[:, 0], color='b', alpha=0.3)
plt.plot(act_hist[:, 0], 'r:', label='Historical.Setpoint')
# plt.ylim(top=1.05)
plt.legend()


plt.subplot(3,1,3)
plt.title('Output Temperature (Average {:.1f}F vs Historical {:.1f}F)'\
          .format(np.mean(temp), np.mean(temp_hist)))
plt.plot(temp, 'b:', label='Temp')
plt.fill_between(np.arange(len(temp)), temp+std_temp, temp-std_temp, color='b', alpha=0.3)
plt.plot(temp_hist, 'r:', label='Historical.Temp')
plt.legend()

plt.tight_layout()
```

%% Cell type:code id: tags:

``` python
plt.figure(figsize=(8,3))
plt.plot(dfs_in[seqidx]['TempAmbient'].values, label='Ambient Temp')
plt.plot(dfs_in[seqidx]['TempWetBulb'].values, label='WetBulb Temp')
plt.legend()
plt.ylabel('Temperature /F')
plt.title('Environmental Conditions')
plt.tight_layout()
plt.show()
```
+1 −1
Changes for src/baseline_control.py: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -179,7 +179,7 @@ class FeedbackController(BaseEstimator):
class SimpleFeedbackController(BaseEstimator):

    def __init__(self, bounds, stepsize:float=1, window: int=1, seed=None):
        self.bounds = np.asarray(bounds) # 1D array of (min, max) for setpoint
        self.bounds = np.asarray(bounds) # 2D array of [(min, max)] for setpoint
        self.stepsize = stepsize
        self.window = window
        self.seed = seed
+24 −3
Changes for src/controller.py: 24 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ import logging
from logging.handlers import HTTPHandler, BufferingHandler
import smtplib
import email
import csv
from datetime import datetime, timedelta

# Issue on Windows where python does not catch keyboard interrupt b/c
@@ -39,6 +40,12 @@ DEFAULT_PATHS = dict(


def make_arguments() -> ArgumentParser:
    # If a setting can be overridden by the settings ini file, then the default
    # should be None. This is because get_settings() assumes a non-None value
    # means that the setting was explicitly provided as a flag in the command
    # line and should not be changed.
    # Actual default values should be stored as variables, or put in the settings
    # ini file.
    parser = ArgumentParser(description='Condenser set-point optimization script.',
        epilog='Additional settings can be changed from the specified settings ini file.')
    parser.add_argument('-i', '--interval', type=int, required=False, default=None,
@@ -57,6 +64,8 @@ def make_arguments() -> ArgumentParser:
    parser.add_argument('-v', '--verbosity', type=str, required=False, default=None,
                        help='Verbosity level.',
                        choices=('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'))
    parser.add_argument('--output-settings', required=False, default=None,
                        help='Path to optionally write controller settings to a csv.')
    parser.add_argument('-d', '--dry-run', required=False, default=False,
                        action='store_true', help='Exit after one action to test script.')
    parser.add_argument('-n', '--no-network', required=False, default=False,
@@ -67,6 +76,9 @@ def make_arguments() -> ArgumentParser:


def get_settings(parsed_args) -> dict:
    # Combines command line flags with settings parsed from settings ini file.
    # Command line takes precedence. Values set in command line are not over-
    # written by ini file.
    settings = {}
    settings.update(vars(parsed_args))
    # try reading them, if error, return previous settings
@@ -88,6 +100,15 @@ def get_settings(parsed_args) -> dict:
    for setting in ('output', 'logs'):
        if settings.get(setting) is None:
            settings[setting] = DEFAULT_PATHS[setting]

    if settings.get('output_settings') not in ('', None):
        with open(settings['output_settings'], 'w', newline='') as f:
            # Only these settings are written to the output settings csv
            keys = ['interval', 'stepsize', 'target', 'window', 'bounds']
            writer = csv.DictWriter(f, fieldnames=keys)
            writer.writeheader()
            writer.writerow({k: settings[k] for k in keys})

    return settings


@@ -139,9 +160,9 @@ def make_logger(**settings) -> logging.Logger:
    ctrl_name = settings.get('controller', '')
    ctrl_application = settings.get('application', '')

    if (email_verbosity is not None and \
        mailhost is not None and \
        fromaddr is not None and \
    if (email_verbosity not in ('', None) and \
        mailhost not in ('', None) and \
        fromaddr not in ('', None) and \
        len(toaddrs) > 0 and toaddrs[0]!='' and \
        username not in ('', None) and password not in ('', None)):
        
+5 −0
Changes for src/settings.ini: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -8,7 +8,10 @@ chiller_1_trend = 2422
chiller_2_trend = 2841
# Location of output file where to put control action.
output = /app001/niagara/Niagara4.2/vykon/shared/ESB_CDWT_Setpoint.csv
output_settings = /app001/niagara/Niagara4.2/vykon/shared/ESB_CDWT_PythonSettings.csv

## LOGGING
## =======
# Location of local log file where to record actions/errors
logs = ./logs.txt
# How much diagnostic output to write. One of:
@@ -33,6 +36,8 @@ logs_email_username =
logs_email_password =
logs_email_batchsize = 288

## CONTROLLER
## ==========
# Interval between querying state and putting control action.
interval = 600
# simple feedback control parameters