diff --git a/ILCAgent/ilc/criteria_handler.py b/ILCAgent/ilc/criteria_handler.py index 32fe5ae..4643f33 100644 --- a/ILCAgent/ilc/criteria_handler.py +++ b/ILCAgent/ilc/criteria_handler.py @@ -135,7 +135,7 @@ def get_score_order(self, state): if state not in cluster.criteria_labels.keys() or state not in cluster.row_average.keys(): _log.debug("Criteria - Not configured for current state: {}".format(state)) continue - + _log.debug("EVAL: {} - {}".format(evaluations.values(), cluster.criteria_labels[state])) input_arr = input_matrix(evaluations, cluster.criteria_labels[state]) scores = build_score(input_arr, cluster.row_average[state], cluster.priority) all_scored.extend(scores) @@ -154,7 +154,7 @@ def get_device(self, device_name): # this passes all data coming in to all device criteria # TODO: rethink this approach. Is there a better way to create the topic map to pass only data needed def ingest_data(self, time_stamp, data): - for device in self.devices.itervalues(): + for device in self.devices.values(): device.ingest_data(time_stamp, data) @@ -235,7 +235,7 @@ def numeric_check(self, value): :param value: :return: """ - if not isinstance(value, (int, float, long, numbers.Float, numbers.Integer)): + if not isinstance(value, (int, float, numbers.Float, numbers.Integer)): if isinstance(value, str): try: value = float(value) @@ -353,7 +353,7 @@ def fixup_dict_args(self, operation_args): result = {"always": [], "nc": []} - for key, value in operation_args.iteritems(): + for key, value in operation_args.items(): if value != "nc": result["always"].append(key) else: @@ -367,11 +367,11 @@ def build_ingest_map(self, operation_args): self.update_points = {} self.operation_arg_count = 0 - for arg_type, arg_list in operation_args.iteritems(): + for arg_type, arg_list in operation_args.items(): topic_map, topic_set = create_device_topic_map(arg_list, self.device_topic) self.device_topic_map.update(topic_map) self.device_topics |= topic_set - self.update_points[arg_type] = set(topic_map.itervalues()) + self.update_points[arg_type] = set(topic_map.values()) self.operation_arg_count += len(topic_map) def evaluate(self): @@ -383,7 +383,7 @@ def evaluate(self): return value def ingest_data(self, time_stamp, data): - for topic, point in self.device_topic_map.iteritems(): + for topic, point in self.device_topic_map.items(): if topic in data: if not self.status or point not in self.update_points.get("nc", set()): value = data[topic] diff --git a/ILCAgent/ilc/curtailment_handler.py b/ILCAgent/ilc/curtailment_handler.py index e32dbb3..1d63fda 100644 --- a/ILCAgent/ilc/curtailment_handler.py +++ b/ILCAgent/ilc/curtailment_handler.py @@ -125,7 +125,7 @@ def get_devices_status(self, state): return all_on_devices def ingest_data(self, time_stamp, data): - for device in self.devices.itervalues(): + for device in self.devices.values(): device.ingest_data(time_stamp, data) @@ -148,7 +148,7 @@ def __init__(self, logging_topic, parent, device_status_args=[], condition="", d self.logging_topic = logging_topic def ingest_data(self, time_stamp, data): - for topic, point in self.device_topic_map.iteritems(): + for topic, point in self.device_topic_map.items(): if topic in data: self.current_device_values[point] = data[topic] _log.debug("DEVICE_STATUS: {} - {} current device values: {}".format(topic, @@ -251,7 +251,7 @@ def __init__(self, device_config, logging_topic, parent, default_device=""): self.device_topics |= controls.device_topics def ingest_data(self, time_stamp, data): - for control in self.controls.itervalues(): + for control in self.controls.values(): control.ingest_data(time_stamp, data) def get_control_info(self, device_id, state): @@ -395,7 +395,7 @@ def check_condition(self): return value def ingest_data(self, time_stamp, data): - for topic, point in self.device_topic_map.iteritems(): + for topic, point in self.device_topic_map.items(): if topic in data: self.current_device_values[point] = data[topic] diff --git a/ILCAgent/ilc/ilc_agent.py b/ILCAgent/ilc/ilc_agent.py index eb9ddb3..2008a7e 100644 --- a/ILCAgent/ilc/ilc_agent.py +++ b/ILCAgent/ilc/ilc_agent.py @@ -654,7 +654,7 @@ def calculate_average_power(self, current_power, current_time): power_sort.sort(reverse=True) exp_power = 0 - for n in xrange(len(self.bldg_power)): + for n in range(len(self.bldg_power)): exp_power += power_sort[n][1] * smoothing_constant * (1.0 - smoothing_constant) ** n exp_power += power_sort[-1][1] * (1.0 - smoothing_constant) ** (len(self.bldg_power)) @@ -763,9 +763,6 @@ def load_message_handler(self, peer, sender, bus, topic, headers, message): # TODO: Refactor this code block. Disparate code paths for simulation and real devices is undesireable if self.sim_running: gevent.sleep(0.25) - while self.sim_time >= 15: - _log.debug("HOLDING {}".format(self.sim_time)) - gevent.sleep(1) self.vip.pubsub.publish("pubsub", "applications/ilc/advance", headers={}, message={}) def check_load(self): @@ -779,10 +776,10 @@ def check_load(self): if self.demand_limit is not None: if self.avg_power > self.demand_limit + self.demand_threshold: - result = "Current load of {} kW exceeds demand limit of {} kW.".format(self.avg_power, self.demand_limit) + result = "Current load of {} kW exceeds demand limit of {} kW.".format(self.avg_power, self.demand_limit+self.demand_threshold) self.curtail_load() - if self.avg_power < self.demand_limit - self.demand_threshold: - result = "Current load of {} kW is below demand limit of {} kW.".format(self.avg_power, self.demand_limit) + elif self.avg_power < self.demand_limit - self.demand_threshold: + result = "Current load of {} kW is below demand limit of {} kW.".format(self.avg_power, self.demand_limit-self.demand_threshold) self.augment_load() else: result = "Current load of {} kW meets demand goal of {} kW.".format(self.avg_power, self.demand_limit) @@ -952,10 +949,11 @@ def determine_curtail_parms(self, control, device_dict): control_load = 0.0 break load_point_values.append((load_arg[0], value)) - try: - control_load = float(load_equation.subs(load_point_values)) - except: - _log.debug("Could not convert expression for load estimation: ") + #try: + _log.debug("LOAD_EQUATION: {} - {} - {}".format(point_to_get, load_equation, load_point_values)) + control_load = float(load_equation.subs(load_point_values)) + #except: + # _log.debug("Could not convert expression for load estimation: ") try: revert_value = self.vip.rpc.call(device_actuator, "get_point", control_pt).get(timeout=30) diff --git a/ILCAgent/ilc/ilc_matrices.py b/ILCAgent/ilc/ilc_matrices.py index 3c18414..e4f1fd7 100644 --- a/ILCAgent/ilc/ilc_matrices.py +++ b/ILCAgent/ilc/ilc_matrices.py @@ -67,6 +67,7 @@ import math from volttron.platform.agent import utils from collections import defaultdict +from functools import reduce utils.setup_logging() _log = logging.getLogger(__name__) @@ -238,7 +239,7 @@ def input_matrix(builder, criteria_labels): """ sum_mat = defaultdict(float) inp_mat = {} - label_check = builder.values()[-1].keys() + label_check = list(list(builder.values())[-1].keys()) if set(label_check) != set(criteria_labels): raise Exception('Input criteria and data criteria do not match.') for device_data in builder.values(): diff --git a/ILCAgent/ilc/utils.py b/ILCAgent/ilc/utils.py index c4ecda9..86b3b27 100644 --- a/ILCAgent/ilc/utils.py +++ b/ILCAgent/ilc/utils.py @@ -67,7 +67,7 @@ def clean_text(text, rep={" ": ""}): - rep = dict((re.escape(k), v) for k, v in rep.iteritems()) + rep = dict((re.escape(k), v) for k, v in rep.items()) pattern = re.compile("|".join(rep.keys())) new_key = pattern.sub(lambda m: rep[re.escape(m.group(0))], text) return new_key diff --git a/MarketAgents/AHUAgent/ahu/agent.py b/MarketAgents/AHUAgent/ahu/agent.py index 50be168..99e78c1 100755 --- a/MarketAgents/AHUAgent/ahu/agent.py +++ b/MarketAgents/AHUAgent/ahu/agent.py @@ -63,7 +63,7 @@ from volttron.platform.agent.base_market_agent.poly_line import PolyLine from volttron.platform.agent.base_market_agent.point import Point -from volttron.pnnl.models.ahuchiller import AHUChiller +from volttron.pnnl.models import Model # from pnnl.models.firstorderzone import FirstOrderZone @@ -72,31 +72,43 @@ __version__ = "0.2" -class AHUAgent(Aggregator, AHUChiller): +# https://stash.pnnl.gov/users/ngoh511/repos/transactivecontrol/raw/MarketAgents/AHUAgent/ahu/agent.py?at=refs%2Fheads%2Fahu_chiller_split + + +class AHUAgent(Aggregator, Model): """ - The SampleElectricMeterAgent serves as a sample of an electric meter that - sells electricity for a single building at a fixed price. + The AHUAgent participates in 3 markets: + 1. Electricity Market: Consumer of electricity + 2. Air Market: Supplier of air + 3. Chilled Water Market: Consumer of chilled water """ def __init__(self, config_path, **kwargs): try: config = utils.load_config(config_path) - except StandardError: + except Exception.StandardError: config = {} model_config = config.get("model_parameters") self.agent_name = config.get("agent_name", "ahu") Aggregator.__init__(self, config, **kwargs) - AHUChiller.__init__(self, model_config, **kwargs) + Model.__init__(self, model_config, **kwargs) + self.init_markets() def translate_aggregate_demand(self, air_demand, index): electric_demand_curve = PolyLine() + coil_load_demand_curve = PolyLine() oat = self.oat_predictions[index] if self.oat_predictions else None for point in air_demand.points: - electric_demand_curve.add(Point(price=point.y, quantity=self.model.calculate_load(point.x, oat))) - _log.debug("{}: electric demand : {}".format(self.agent_name, electric_demand_curve.points)) + #electric_demand_curve.add(Point(price=point.y, quantity=self.model.calculate_load(point.x, oat))) + self.model.input_zone_load(point.x) + electric_demand_curve.add(Point(price=point.y, quantity=self.model.calculate_electric_load())) + coil_load_demand_curve.add(Point(price=point.y, quantity=self.model.calculate_coil_load(oat))) + # Hard-coding the market names is not ideal. Need to come up with more robust solution self.consumer_demand_curve["electric"][index] = electric_demand_curve + self.consumer_demand_curve["chilled_water"][index] = coil_load_demand_curve + def main(): diff --git a/MarketAgents/AHUAgent/setup.py b/MarketAgents/AHUAgent/setup.py index 635162c..0836108 100644 --- a/MarketAgents/AHUAgent/setup.py +++ b/MarketAgents/AHUAgent/setup.py @@ -75,7 +75,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/MarketAgents/BESSAgent/bess/__init__.py b/MarketAgents/BESSAgent/bess/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MarketAgents/BESSAgent/bess/agent.py b/MarketAgents/BESSAgent/bess/agent.py new file mode 100644 index 0000000..6172e3b --- /dev/null +++ b/MarketAgents/BESSAgent/bess/agent.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2018, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +import sys +import logging +from volttron.platform.agent import utils +from volttron.pnnl.transactive_base.transactive.transactive import TransactiveBase +from volttron.pnnl.transactive_base.transactive.aggregator_base import Aggregator +from volttron.platform.agent.base_market_agent.poly_line import PolyLine +from volttron.platform.agent.base_market_agent.point import Point +from volttron.pnnl.models import Model +from volttron.platform.vip.agent import Agent, Core + +_log = logging.getLogger(__name__) +utils.setup_logging() +__version__ = "0.1" + + +class BESSAgent(TransactiveBase, Model): + """ + The BESS Agent participates in Electricity Market as supplier of electricity. + """ + + def __init__(self, config_path, **kwargs): + try: + config = utils.load_config(config_path) + except Exception.StandardError: + config = {} + self.agent_name = config.get("agent_name", "bess_agent") + model_config = config.get("model_parameters", {}) + TransactiveBase.__init__(self, config, **kwargs) + Model.__init__(self, model_config, **kwargs) + self.init_markets() + self.numHours = 24 + self.indices = [None]*self.numHours + + @Core.receiver('onstart') + def onstart(self, sender, **kwargs): + _log.debug("BESS onstart") + # Subscriptions + self.vip.pubsub.subscribe(peer='pubsub', + prefix='mixmarket/calculate_demand', + callback=self._calculate_demand) + + def offer_callback(self, timestamp, market_name, buyer_seller): + market_index = self.market_name.index(market_name) + + self.indices[market_index] = market_name + optimization_ready = all([False if i is None else True for i in self.indices]) + if optimization_ready: + _log.debug("BESS: market_prices = {}".format(self.market_prices)) + _log.debug("BESS: reserve_market_prices = {}".format(self.reserve_market_prices)) + bess_power_inject, bess_power_reserve, bess_soc = self.model.run_bess_optimization(self.market_prices, + self.reserve_market_prices) + bess_power_inject = [-i for i in bess_power_inject] + _log.debug("BESS: translate_aggregate_demand bess_power_inject: {}, bess_power_reserve: {}".format( + bess_power_inject, + bess_power_reserve)) + self.indices = [None] * self.numHours + price_min, price_max = self.determine_price_min_max() + _log.debug("BESS: price_min: {}, price_max: {}".format(price_min, price_max)) + for name in self.market_name: + index = self.market_name.index(name) + electric_demand_curve = PolyLine() + electric_demand_curve.add(Point(bess_power_inject[index], price_min)) + electric_demand_curve.add(Point(bess_power_inject[index], price_max)) + self.demand_curve[market_index] = electric_demand_curve + self.make_offer(name, buyer_seller, electric_demand_curve) + _log.debug("BESS: make_offer: market name: {}, electric demand : Pt: {}, index: {}".format(name, + electric_demand_curve.points, + index)) + self.update_flag[market_index] = True + + self.vip.pubsub.publish(peer='pubsub', + topic='mixmarket/reserve_demand', + message={ + "reserve_power": list(bess_power_reserve), + "sender": self.agent_name + }) + + def determine_price_min_max(self): + """ + Determine minimum and maximum price from 24-hour look ahead prices. If the TNS + market architecture is not utilized, this function must be overwritten in the child class. + :return: + """ + prices = self.determine_prices() + price_min = prices[0] + price_max = prices[len(prices)-1] + return price_min, price_max + + def determine_control(self, sets, prices, price): + return self.model.calculate_control(self.current_datetime) + + def _calculate_demand(self, peer, sender, bus, topic, headers, message): + prices = message['prices'] + reserve_prices = message['reserve_prices'] + bess_power_inject, bess_power_reserve, bess_soc = self.model.run_bess_optimization(prices, + reserve_prices) + bess_power_inject = [-i for i in bess_power_inject] + self.vip.pubsub.publish(peer='pubsub', + topic='mixmarket/tess_bess_demand', + message={ + "power": bess_power_inject, + "reserve_power": bess_power_reserve, + "sender": self.agent_name + }) + +def main(): + """Main method called to start the agent.""" + utils.vip_main(BESSAgent, version=__version__) + + +if __name__ == '__main__': + # Entry point for script + try: + sys.exit(main()) + except KeyboardInterrupt: + pass \ No newline at end of file diff --git a/MarketAgents/BESSAgent/setup.py b/MarketAgents/BESSAgent/setup.py new file mode 100644 index 0000000..635162c --- /dev/null +++ b/MarketAgents/BESSAgent/setup.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2016, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +from os import path +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = '' +for package in find_packages(): + # Because there could be other packages such as tests + if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: + agent_package = package +if not agent_package: + raise RuntimeError('None of the packages under {dir} contain the file ' + '{main_module}'.format(main_module=MAIN_MODULE + '.py', + dir=path.abspath('.'))) + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) diff --git a/MarketAgents/CampusAgent/campus/__init__.py b/MarketAgents/CampusAgent/campus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MarketAgents/CampusAgent/campus/agent.py b/MarketAgents/CampusAgent/campus/agent.py new file mode 100755 index 0000000..4e4164f --- /dev/null +++ b/MarketAgents/CampusAgent/campus/agent.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2015, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +#}}} + +import os +import sys +import logging +import datetime +import gevent +from datetime import datetime +from datetime import datetime +from datetime import timedelta as td +from dateutil import parser + + +from volttron.platform.vip.agent import Agent, Core, PubSub, RPC, compat +from volttron.platform.agent import utils +from volttron.platform.agent.utils import (get_aware_utc_now, format_timestamp) +from volttron.platform.messaging import topics, headers as headers_mod +from volttron.platform.agent.base_market_agent import MarketAgent +from volttron.platform.agent.base_market_agent.poly_line import PolyLine +from volttron.platform.agent.base_market_agent.point import Point +from volttron.platform.agent.base_market_agent.buy_sell import BUYER +from volttron.platform.agent.base_market_agent.buy_sell import SELLER + +from timer import Timer +from weather_service import WeatherService + +utils.setup_logging() +_log = logging.getLogger(__name__) +__version__ = '0.1' + + +class CampusAgent(MarketAgent): + def __init__(self, config_path, **kwargs): + MarketAgent.__init__(self, **kwargs) + + self.config_path = config_path + self.config = utils.load_config(config_path) + self.name = self.config.get('name') + self.agent_name = self.config.get('agent_name', 'campus_agent') + self.T = int(self.config.get('T', 24)) + + self.db_topic = self.config.get("db_topic", "tnc") + self.city_supply_topic = "{}/city/campus/supply".format(self.db_topic) + self.campus_demand_topic = "{}/campus/city/demand".format(self.db_topic) + self.campus_supply_topic = "/".join([self.db_topic, "campus/{}/supply"]) + + # Create market names to join + self.quantities = [None] * self.T + self.reserves = [None] * self.T + self.base_market_name = 'electric' # Need to agree on this with other market agents + self.market_names = [] + self.prices = [] + self.reserve_senders = self.config.get("reserve_senders", ["tess", "bess"]) + for i in range(self.T): + self.market_names.append('_'.join([self.base_market_name, str(i)])) + + + # Weather prediction + self.weather_file = self.config.get('weather_file') + self.weather_service = WeatherService(weather_file=self.weather_file) + self.senders = [] + self.mix_market_running = False + self.simulation = self.config.get('simulation', False) + try: + self.simulation_start_time = parser.parse(self.config.get('simulation_start_time')) + self.simulation_one_hour_in_seconds = int(self.config.get('simulation_one_hour_in_seconds')) + except: + self.simulation_start_time = datetime.now() + self.simulation_one_hour_in_seconds = 3600 + Timer.created_time = datetime.now() + Timer.simulation = self.simulation + Timer.sim_start_time = self.simulation_start_time + Timer.sim_one_hr_in_sec = self.simulation_one_hour_in_seconds + + @Core.receiver('onstart') + def onstart(self, sender, **kwargs): + # Subscriptions + self.vip.pubsub.subscribe(peer='pubsub', + prefix=self.city_supply_topic, + callback=self.new_supply_signal) + + # Join electric mix-markets + for market in self.market_names: + self.join_market(market, SELLER, self.reservation_callback, self.offer_callback, + None, self.price_callback, self.error_callback) + + # Join reserved market + self.vip.pubsub.subscribe(peer='pubsub', + prefix="mixmarket/reserve_demand", + callback=self.new_reserved_demands) + + def new_supply_signal(self, peer, sender, bus, topic, headers, message): + price = message['price'] + self.prices = price + price_reserved = message['price_reserved'] + converged = message['converged'] + start_of_cycle = message['start_of_cycle'] + + _log.info("Campus {} receive from city: {}, {}".format(self.name, + price, price_reserved)) + + # TODO: mock start mixmarket. + # TODO: Comment send_to_city and uncomment start_mixmarket for real testing + #self.send_to_city(map(lambda x: x*2, price), + # map(lambda x: x+5, price_reserved)) + + self.start_mixmarket(converged, price, price_reserved, start_of_cycle) + + def new_reserved_demands(self, peer, sender, bus, topic, headers, message): + #self.reserves = message['value'] + reserves = message['reserve_power'] + self.senders.append(message['sender']) + _log.debug("Received reserve prices: {} from sender: {}".format(self.reserves, + self.senders)) + for idx in range(0, self.T): + if self.reserves[idx] is None: + self.reserves[idx] = reserves[idx] + else: + self.reserves[idx] += reserves[idx] + # if len(self.senders) >= len(self.reserve_senders): + # _log.debug("Sending to city: {}, POWER".format(self.senders)) + # del self.senders[:] + # self.mix_market_running = False + # self.send_to_city(self.quantities, self.reserves) + # self.reserves = [None]*self.T + + def start_mixmarket(self, converged, price, price_reserved, start_of_cycle): + now = Timer.get_cur_time() + prediction_day = now + td(days=1) + _log.info("Getting weather prediction at {}...".format(now)) + temps = self.get_weather_next_day(now) + self.mix_market_running = True + headers = {"Date": format_timestamp(now)} + _log.info("Starting mixmarket at {}...".format(now)) + _log.debug("CAMPUS: HR: {}, sending prices : {}".format(now.hour, price)) + _log.debug("CAMPUS: HR: {}, sending reserve prices: {}".format(now.hour, price_reserved)) + self.vip.pubsub.publish(peer='pubsub', + topic='mixmarket/make_tcc_predictions', + message={ + "converged": converged, + "prices": price, + "reserved_prices": price_reserved, + "start_of_cycle": start_of_cycle, + "hour": now.hour, + "prediction_date": format_timestamp(prediction_day), + "temp": temps + }, + headers=headers) + gevent.sleep(0.125) + self.vip.pubsub.publish(peer='pubsub', + topic='mixmarket/start_new_cycle', + message={ + "converged": converged, + "prices": price, + "reserved_prices": price_reserved, + "start_of_cycle": start_of_cycle, + "hour": now.hour, + "prediction_date": format_timestamp(prediction_day), + "temp": temps + }, + headers=headers) + + def offer_callback(self, timestamp, market_name, buyer_seller): + if market_name in self.market_names: + # Get the price for the corresponding market + idx = int(market_name.split('_')[-1]) + price = self.prices[idx] + #price *= 1000. # Convert to mWh to be compatible with the mixmarket + + # Quantity + min_quantity = 0 + max_quantity = 10000 # float("inf") + + # Create supply curve + supply_curve = PolyLine() + supply_curve.add(Point(quantity=min_quantity, price=price)) + supply_curve.add(Point(quantity=max_quantity, price=price)) + + # Make offer + _log.debug("{}: offer for {} as {} at {} - Curve: {} {}".format(self.agent_name, + market_name, + SELLER, + timestamp, + supply_curve.points[0], supply_curve.points[1])) + success, message = self.make_offer(market_name, SELLER, supply_curve) + _log.debug("{}: offer has {} - Message: {}".format(self.agent_name, success, message)) + + def reservation_callback(self, timestamp, market_name, buyer_seller): + _log.debug("{}: wants reservation for {} as {} at {}".format(self.agent_name, + market_name, + buyer_seller, + timestamp)) + return True + + def aggregate_callback(self, timestamp, market_name, buyer_seller, aggregate_demand): + if buyer_seller == BUYER and market_name in self.market_names: # self.base_market_name in market_name: + _log.debug("{}: at ts {} min of aggregate curve : {}".format(self.agent_name, + timestamp, + aggregate_demand.points[0])) + _log.debug("{}: at ts {} max of aggregate curve : {}".format(self.agent_name, + timestamp, + aggregate_demand.points[- 1])) + _log.debug("At {}: Report aggregate Market: {} buyer Curve: {}".format(Timer.get_cur_time(), + market_name, + aggregate_demand)) + + def price_callback(self, timestamp, market_name, buyer_seller, price, quantity): + if buyer_seller == BUYER and market_name in self.market_names: + _log.debug("{}: cleared price ({}, {}) for {} as {} at {}".format(Timer.get_cur_time(), + price, + quantity, + market_name, + buyer_seller, + timestamp)) + idx = int(market_name.split('_')[-1]) + _log.debug("CAMPUS: price: {}, idx: {}, self.prices: {}".format(price, idx, self.prices)) + self.prices[idx] = price + if price is None: + _log.error("Market {} did not clear. Price is none.".format(market_name)) + if self.quantities[idx] is None: + self.quantities[idx] = 0. + if quantity is None: + _log.error("Quantity is None. Set it to 0. Details below.") + _log.debug("{}: ({}, {}) for {} as {} at {}".format(self.agent_name, + price, + quantity, + market_name, + buyer_seller, + timestamp)) + quantity = 0 + self.quantities[idx] += quantity + + _log.debug("At {}, q is {} and qs are: {}".format(Timer.get_cur_time(), + quantity, + self.quantities)) + if quantity is not None and quantity < 0: + _log.error("Quantity received from mixmarket is negative!!! {}".format(quantity)) + + # If all markets (ie. exclude 1st value) are done then update demands. + # Otherwise do nothing + # CHANGE: market now is done when received reserved demand + + market_done = self.check_market_done() + if market_done: + self.mix_market_running = False + + self.send_to_city(self.quantities, self.reserves) + self.quantities = [None]*self.T + self.reserves = [None]*self.T + del self.senders[:] + + def send_to_city(self, power_demand, committed_reserves): + # Note: in practice, supply should be positive and consumption should be negative + # However, because AHU, VAV, etc. agents return their consumption as negative values, + # I need to reverse the sign before sending back to city + power_demand = [-v for v in power_demand] + #committed_reserves = [-v for v in committed_reserves] + _log.debug("CAMPUS: receiving quantity : {}".format(power_demand)) + _log.debug("CAMPUS: receiving reserve quantity: {}".format(committed_reserves)) + self.vip.pubsub.publish(peer='pubsub', + topic=self.campus_demand_topic, + message={ + 'power_demand': power_demand, + 'committed_reserves': committed_reserves + }) + + def check_market_done(self): + all_q_received = all([False if q is None else True for q in self.quantities]) + all_r_received = all([False if r is None else True for r in self.reserves]) + #and \ + #len(self.senders) == len(self.reserve_senders) + _log.debug("check_market_done: {}".format(len(self.senders))) + _log.debug("check_market_done: {}, {}".format(all_q_received, all_r_received)) + return all_q_received and all_r_received + + def error_callback(self, timestamp, market_name, buyer_seller, error_code, error_message, aux): + _log.debug("{}: error for {} as {} at {} - Message: {}".format(self.agent_name, + market_name, + buyer_seller, + timestamp, + error_message)) + + def get_weather_next_day(self, now): + from datetime import timedelta + + next_day = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + times = [] + _log.debug("CAMPUS: Weather data time: {}".format(next_day)) + for i in range(0, 24): + times.append(next_day + timedelta(hours=i)) + + temps = self.weather_service.predict(times) + + return temps + + +def main(argv=sys.argv): + try: + utils.vip_main(CampusAgent) + except Exception as e: + _log.exception('unhandled exception') + + +if __name__ == '__main__': + # Entry point for script + sys.exit(main()) diff --git a/MarketAgents/CampusAgent/campus/timer.py b/MarketAgents/CampusAgent/campus/timer.py new file mode 100644 index 0000000..807ea68 --- /dev/null +++ b/MarketAgents/CampusAgent/campus/timer.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2017, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + + +from datetime import datetime + + +class Timer: + created_time = None + sim_one_hr_in_sec = 1200 + sim_start_time = None + simulation = False + + @classmethod + def get_cur_time(cls): + """ + Calculate current time based on the amount of time has passed since this object is created + :return: + """ + cur_time = datetime.now() + if cls.simulation: + ratio = 3600 / cls.sim_one_hr_in_sec + time_diff = cur_time - cls.created_time + simulation_time_diff = time_diff * ratio + cur_time = cls.sim_start_time + simulation_time_diff + + return cur_time + + +if __name__ == '__main__': + from dateutil import parser + + Timer.simulation = True + Timer.created_time = parser.parse("2017-01-01 00:00:00.000") + Timer.sim_start_time = parser.parse("2017-08-01 00:00:00.000") + print(Timer.get_cur_time()) + + Timer.sim_start_time = parser.parse("2018-07-22 00:00:00.000") + print(Timer.get_cur_time()) + + Timer.simulation = False + print(Timer.get_cur_time()) \ No newline at end of file diff --git a/MarketAgents/CampusAgent/campus/weather_service.py b/MarketAgents/CampusAgent/campus/weather_service.py new file mode 100644 index 0000000..32baa27 --- /dev/null +++ b/MarketAgents/CampusAgent/campus/weather_service.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2017, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + + +import os +import csv +import logging +from datetime import datetime, timedelta +from dateutil import parser +import dateutil.tz + + +class WeatherService(object): + """ + Predict hourly temperature (F) + Use CSV as we don't have internet access for now. Thus keep the csv file as small as possible + This can be changed to read from real-time data source such as WU + """ + + def __init__(self, weather_file=None): + self.weather_file = weather_file + + self.predicted_values = [] + self.weather_data = [] + self.last_modified = None + try: + self.localtz = dateutil.tz.tzlocal() + except: + # Problem automatically determining timezone! - Default to UTC + self.localtz = "UTC" + + # Load weather data the 1st time + self.init_weather_data() + + def init_weather_data(self): + """ + To init or re-init weather data from file. + :return: + """ + # Get latest modified time + cur_modified = os.path.getmtime(self.weather_file) + + if self.last_modified is None or cur_modified != self.last_modified: + self.last_modified = cur_modified + + # Clear weather_data for re-init + self.weather_data = [] + + with open(self.weather_file) as f: + reader = csv.DictReader(f) + self.weather_data = [r for r in reader] + for rec in self.weather_data: + rec['Timestamp'] = parser.parse(rec['Timestamp']).replace(minute=0, second=0, microsecond=0) + rec['Value'] = float(rec['Value']) + + def predict(self, times): + self.init_weather_data() + + # Copy weather data to predictedValues + self.predicted_values = [] + for start_time in times: + items = [x for x in self.weather_data if x['Timestamp'] == start_time] + if len(items) == 0: + trial_deltas = [-1, 1, -2, 2, -24, 24] + for delta in trial_deltas: + items = [x for x in self.weather_data if x['Timestamp'] == (start_time - timedelta(hours=delta))] + if len(items) > 0: + break + + # None exist, raise exception + if len(items) == 0: + raise Exception('No weather data for time: {}'.format(start_time)) + + temp = items[0]['Value'] + self.predicted_values.append(temp) + + return self.predicted_values + + +if __name__ == '__main__': + from datetime import timedelta + + now = datetime.now() + next_day = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + times = [] + for i in range(0, 24): + times.append(next_day + timedelta(hours=i)) + + ws = WeatherService( + weather_file="/Users/ngoh511/Documents/projects/volttron-GS/Market3Agent/market3/weather_data/energyplus.csv") + temps = ws.predict(times) + + print(temps) + print(len(temps)) diff --git a/MarketAgents/CampusAgent/campus_config b/MarketAgents/CampusAgent/campus_config new file mode 100755 index 0000000..a3fb887 --- /dev/null +++ b/MarketAgents/CampusAgent/campus_config @@ -0,0 +1,13 @@ +{ + "agentid": "campus_agent", + "name": "PNNL_Campus", + "agent_name": "PNNL_Campus", + "T": 24, + "weather_file": "/home/volttron/volttron-GS/MarketAgents/CampusAgent/weather_data/energyplus.csv", + "input_topic": "devices/PNNL/BUILDING1/AHU1/all", + "simulation": true, + "simulation_start_time": "2017-08-01 00:00:00", + "simulation_one_hour_in_seconds": 1200, + "senders": ["tess_agent", "bess_agent"], + "markets": ["electric", "electric_alpha"] +} diff --git a/MarketAgents/CampusAgent/setup.py b/MarketAgents/CampusAgent/setup.py new file mode 100755 index 0000000..7840a26 --- /dev/null +++ b/MarketAgents/CampusAgent/setup.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2015, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +from os import path +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = '' +for package in find_packages(): + # Because there could be other packages such as tests + if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: + agent_package = package +if not agent_package: + raise RuntimeError('None of the packages under {dir} contain the file ' + '{main_module}'.format(main_module=MAIN_MODULE + '.py', + dir=path.abspath('.'))) + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) diff --git a/MarketAgents/CampusAgent/weather_data/energyplus.csv b/MarketAgents/CampusAgent/weather_data/energyplus.csv new file mode 100644 index 0000000..f47979a --- /dev/null +++ b/MarketAgents/CampusAgent/weather_data/energyplus.csv @@ -0,0 +1,8760 @@ +Timestamp,Value +1/1/2017 1:00,-2 +1/1/2017 2:00,-3 +1/1/2017 3:00,-3 +1/1/2017 4:00,-3 +1/1/2017 5:00,-3 +1/1/2017 6:00,2 +1/1/2017 7:00,4 +1/1/2017 8:00,4 +1/1/2017 9:00,4 +1/1/2017 10:00,4 +1/1/2017 11:00,6 +1/1/2017 12:00,6 +1/1/2017 13:00,7 +1/1/2017 14:00,8 +1/1/2017 15:00,8 +1/1/2017 16:00,6 +1/1/2017 17:00,6 +1/1/2017 18:00,5 +1/1/2017 19:00,4 +1/1/2017 20:00,4 +1/1/2017 21:00,3 +1/1/2017 22:00,3 +1/1/2017 23:00,4 +1/2/2017 0:00,3 +1/2/2017 1:00,4 +1/2/2017 2:00,4 +1/2/2017 3:00,4 +1/2/2017 4:00,4 +1/2/2017 5:00,4 +1/2/2017 6:00,3.9 +1/2/2017 7:00,4 +1/2/2017 8:00,3 +1/2/2017 9:00,4 +1/2/2017 10:00,5 +1/2/2017 11:00,6 +1/2/2017 12:00,6 +1/2/2017 13:00,8 +1/2/2017 14:00,9 +1/2/2017 15:00,7 +1/2/2017 16:00,5 +1/2/2017 17:00,2 +1/2/2017 18:00,-1 +1/2/2017 19:00,-1 +1/2/2017 20:00,-2 +1/2/2017 21:00,-4 +1/2/2017 22:00,-4 +1/2/2017 23:00,-6 +1/3/2017 0:00,-6 +1/3/2017 1:00,-7 +1/3/2017 2:00,-6 +1/3/2017 3:00,-6 +1/3/2017 4:00,-4 +1/3/2017 5:00,-3 +1/3/2017 6:00,-2 +1/3/2017 7:00,-1 +1/3/2017 8:00,-1 +1/3/2017 9:00,2 +1/3/2017 10:00,2 +1/3/2017 11:00,2 +1/3/2017 12:00,3 +1/3/2017 13:00,3 +1/3/2017 14:00,4 +1/3/2017 15:00,4 +1/3/2017 16:00,4 +1/3/2017 17:00,4 +1/3/2017 18:00,4 +1/3/2017 19:00,3 +1/3/2017 20:00,2 +1/3/2017 21:00,2 +1/3/2017 22:00,2 +1/3/2017 23:00,3 +1/4/2017 0:00,2.9 +1/4/2017 1:00,3 +1/4/2017 2:00,4 +1/4/2017 3:00,7 +1/4/2017 4:00,7 +1/4/2017 5:00,7 +1/4/2017 6:00,7 +1/4/2017 7:00,7 +1/4/2017 8:00,9 +1/4/2017 9:00,9 +1/4/2017 10:00,9 +1/4/2017 11:00,11 +1/4/2017 12:00,11 +1/4/2017 13:00,11 +1/4/2017 14:00,12 +1/4/2017 15:00,11 +1/4/2017 16:00,9 +1/4/2017 17:00,7 +1/4/2017 18:00,7 +1/4/2017 19:00,6 +1/4/2017 20:00,6 +1/4/2017 21:00,6 +1/4/2017 22:00,5 +1/4/2017 23:00,5 +1/5/2017 0:00,6 +1/5/2017 1:00,5 +1/5/2017 2:00,6 +1/5/2017 3:00,4 +1/5/2017 4:00,4 +1/5/2017 5:00,3 +1/5/2017 6:00,3 +1/5/2017 7:00,3 +1/5/2017 8:00,3 +1/5/2017 9:00,3 +1/5/2017 10:00,6 +1/5/2017 11:00,8 +1/5/2017 12:00,9 +1/5/2017 13:00,9 +1/5/2017 14:00,10 +1/5/2017 15:00,9.5 +1/5/2017 16:00,8.5 +1/5/2017 17:00,7 +1/5/2017 18:00,4 +1/5/2017 19:00,4 +1/5/2017 20:00,2 +1/5/2017 21:00,3 +1/5/2017 22:00,3 +1/5/2017 23:00,2 +1/6/2017 0:00,2 +1/6/2017 1:00,2 +1/6/2017 2:00,-1 +1/6/2017 3:00,-4 +1/6/2017 4:00,-4 +1/6/2017 5:00,-4 +1/6/2017 6:00,-4 +1/6/2017 7:00,-4 +1/6/2017 8:00,-5 +1/6/2017 9:00,-2 +1/6/2017 10:00,1 +1/6/2017 11:00,1.8 +1/6/2017 12:00,2 +1/6/2017 13:00,3 +1/6/2017 14:00,1 +1/6/2017 15:00,1 +1/6/2017 16:00,1 +1/6/2017 17:00,1 +1/6/2017 18:00,1 +1/6/2017 19:00,1 +1/6/2017 20:00,1 +1/6/2017 21:00,1 +1/6/2017 22:00,1 +1/6/2017 23:00,1 +1/7/2017 0:00,-1 +1/7/2017 1:00,-1 +1/7/2017 2:00,-3 +1/7/2017 3:00,-3 +1/7/2017 4:00,-3 +1/7/2017 5:00,-4 +1/7/2017 6:00,-3 +1/7/2017 7:00,-4 +1/7/2017 8:00,-3 +1/7/2017 9:00,-1 +1/7/2017 10:00,3 +1/7/2017 11:00,5 +1/7/2017 12:00,6 +1/7/2017 13:00,8 +1/7/2017 14:00,9 +1/7/2017 15:00,10 +1/7/2017 16:00,9 +1/7/2017 17:00,8 +1/7/2017 18:00,7 +1/7/2017 19:00,6 +1/7/2017 20:00,7 +1/7/2017 21:00,7 +1/7/2017 22:00,8 +1/7/2017 23:00,8 +1/8/2017 0:00,7 +1/8/2017 1:00,7 +1/8/2017 2:00,8 +1/8/2017 3:00,7 +1/8/2017 4:00,7 +1/8/2017 5:00,8 +1/8/2017 6:00,8 +1/8/2017 7:00,9 +1/8/2017 8:00,9 +1/8/2017 9:00,8 +1/8/2017 10:00,8 +1/8/2017 11:00,9 +1/8/2017 12:00,9 +1/8/2017 13:00,10 +1/8/2017 14:00,12 +1/8/2017 15:00,10 +1/8/2017 16:00,9 +1/8/2017 17:00,7 +1/8/2017 18:00,7 +1/8/2017 19:00,7 +1/8/2017 20:00,6 +1/8/2017 21:00,6 +1/8/2017 22:00,6.2 +1/8/2017 23:00,6.5 +1/9/2017 0:00,7 +1/9/2017 1:00,8 +1/9/2017 2:00,6 +1/9/2017 3:00,7 +1/9/2017 4:00,6 +1/9/2017 5:00,6 +1/9/2017 6:00,6 +1/9/2017 7:00,6 +1/9/2017 8:00,6 +1/9/2017 9:00,7 +1/9/2017 10:00,10 +1/9/2017 11:00,11 +1/9/2017 12:00,12 +1/9/2017 13:00,11 +1/9/2017 14:00,10 +1/9/2017 15:00,9 +1/9/2017 16:00,8 +1/9/2017 17:00,6 +1/9/2017 18:00,4 +1/9/2017 19:00,3 +1/9/2017 20:00,2 +1/9/2017 21:00,2 +1/9/2017 22:00,3 +1/9/2017 23:00,2 +1/10/2017 0:00,3 +1/10/2017 1:00,2 +1/10/2017 2:00,2 +1/10/2017 3:00,2 +1/10/2017 4:00,2 +1/10/2017 5:00,2.4 +1/10/2017 6:00,3 +1/10/2017 7:00,3 +1/10/2017 8:00,3 +1/10/2017 9:00,4 +1/10/2017 10:00,4 +1/10/2017 11:00,3.8 +1/10/2017 12:00,3 +1/10/2017 13:00,3 +1/10/2017 14:00,4 +1/10/2017 15:00,4 +1/10/2017 16:00,3 +1/10/2017 17:00,3 +1/10/2017 18:00,2 +1/10/2017 19:00,2 +1/10/2017 20:00,1 +1/10/2017 21:00,1 +1/10/2017 22:00,1 +1/10/2017 23:00,1 +1/11/2017 0:00,1 +1/11/2017 1:00,1 +1/11/2017 2:00,1 +1/11/2017 3:00,0 +1/11/2017 4:00,1 +1/11/2017 5:00,-1 +1/11/2017 6:00,-1 +1/11/2017 7:00,-3 +1/11/2017 8:00,-2 +1/11/2017 9:00,-1 +1/11/2017 10:00,2 +1/11/2017 11:00,3 +1/11/2017 12:00,3 +1/11/2017 13:00,4 +1/11/2017 14:00,4 +1/11/2017 15:00,4 +1/11/2017 16:00,1 +1/11/2017 17:00,1 +1/11/2017 18:00,1 +1/11/2017 19:00,-1 +1/11/2017 20:00,0 +1/11/2017 21:00,3 +1/11/2017 22:00,3 +1/11/2017 23:00,3 +1/12/2017 0:00,3 +1/12/2017 1:00,3 +1/12/2017 2:00,3 +1/12/2017 3:00,3 +1/12/2017 4:00,2 +1/12/2017 5:00,3 +1/12/2017 6:00,2 +1/12/2017 7:00,2 +1/12/2017 8:00,3 +1/12/2017 9:00,4 +1/12/2017 10:00,4 +1/12/2017 11:00,5.4 +1/12/2017 12:00,6.2 +1/12/2017 13:00,7 +1/12/2017 14:00,7 +1/12/2017 15:00,6 +1/12/2017 16:00,6 +1/12/2017 17:00,4 +1/12/2017 18:00,4 +1/12/2017 19:00,4 +1/12/2017 20:00,4 +1/12/2017 21:00,4 +1/12/2017 22:00,6 +1/12/2017 23:00,6 +1/13/2017 0:00,7 +1/13/2017 1:00,7 +1/13/2017 2:00,5 +1/13/2017 3:00,5 +1/13/2017 4:00,6 +1/13/2017 5:00,6 +1/13/2017 6:00,6 +1/13/2017 7:00,7 +1/13/2017 8:00,6 +1/13/2017 9:00,5 +1/13/2017 10:00,6 +1/13/2017 11:00,7 +1/13/2017 12:00,7 +1/13/2017 13:00,8 +1/13/2017 14:00,7 +1/13/2017 15:00,7 +1/13/2017 16:00,6 +1/13/2017 17:00,6 +1/13/2017 18:00,4 +1/13/2017 19:00,3 +1/13/2017 20:00,3 +1/13/2017 21:00,3 +1/13/2017 22:00,3 +1/13/2017 23:00,3 +1/14/2017 0:00,3 +1/14/2017 1:00,3 +1/14/2017 2:00,2 +1/14/2017 3:00,2 +1/14/2017 4:00,2 +1/14/2017 5:00,2 +1/14/2017 6:00,2 +1/14/2017 7:00,2 +1/14/2017 8:00,2 +1/14/2017 9:00,3 +1/14/2017 10:00,4 +1/14/2017 11:00,5 +1/14/2017 12:00,10 +1/14/2017 13:00,10 +1/14/2017 14:00,11 +1/14/2017 15:00,11 +1/14/2017 16:00,10 +1/14/2017 17:00,9 +1/14/2017 18:00,8 +1/14/2017 19:00,7 +1/14/2017 20:00,7 +1/14/2017 21:00,7 +1/14/2017 22:00,6 +1/14/2017 23:00,6 +1/15/2017 0:00,4 +1/15/2017 1:00,3 +1/15/2017 2:00,3 +1/15/2017 3:00,2 +1/15/2017 4:00,3 +1/15/2017 5:00,2 +1/15/2017 6:00,2 +1/15/2017 7:00,-1 +1/15/2017 8:00,-1 +1/15/2017 9:00,-2 +1/15/2017 10:00,1 +1/15/2017 11:00,2 +1/15/2017 12:00,3.4 +1/15/2017 13:00,4.8 +1/15/2017 14:00,6 +1/15/2017 15:00,4 +1/15/2017 16:00,4 +1/15/2017 17:00,3 +1/15/2017 18:00,3 +1/15/2017 19:00,3 +1/15/2017 20:00,2 +1/15/2017 21:00,2 +1/15/2017 22:00,2 +1/15/2017 23:00,1 +1/16/2017 0:00,1 +1/16/2017 1:00,1 +1/16/2017 2:00,1 +1/16/2017 3:00,1 +1/16/2017 4:00,1 +1/16/2017 5:00,1 +1/16/2017 6:00,1 +1/16/2017 7:00,1 +1/16/2017 8:00,1 +1/16/2017 9:00,2 +1/16/2017 10:00,2 +1/16/2017 11:00,3 +1/16/2017 12:00,4 +1/16/2017 13:00,6 +1/16/2017 14:00,8 +1/16/2017 15:00,9 +1/16/2017 16:00,9 +1/16/2017 17:00,7 +1/16/2017 18:00,6 +1/16/2017 19:00,5 +1/16/2017 20:00,4 +1/16/2017 21:00,4 +1/16/2017 22:00,2 +1/16/2017 23:00,3 +1/17/2017 0:00,3 +1/17/2017 1:00,7 +1/17/2017 2:00,4 +1/17/2017 3:00,4 +1/17/2017 4:00,2 +1/17/2017 5:00,2 +1/17/2017 6:00,2 +1/17/2017 7:00,2 +1/17/2017 8:00,0 +1/17/2017 9:00,2 +1/17/2017 10:00,6 +1/17/2017 11:00,7 +1/17/2017 12:00,7 +1/17/2017 13:00,8 +1/17/2017 14:00,8 +1/17/2017 15:00,8 +1/17/2017 16:00,7 +1/17/2017 17:00,4 +1/17/2017 18:00,1 +1/17/2017 19:00,0 +1/17/2017 20:00,0 +1/17/2017 21:00,0 +1/17/2017 22:00,1 +1/17/2017 23:00,-1 +1/18/2017 0:00,-1 +1/18/2017 1:00,-2 +1/18/2017 2:00,-2 +1/18/2017 3:00,-4 +1/18/2017 4:00,-3 +1/18/2017 5:00,-4 +1/18/2017 6:00,-4 +1/18/2017 7:00,-4 +1/18/2017 8:00,-4 +1/18/2017 9:00,-4 +1/18/2017 10:00,-2 +1/18/2017 11:00,-1 +1/18/2017 12:00,0 +1/18/2017 13:00,1 +1/18/2017 14:00,1 +1/18/2017 15:00,2 +1/18/2017 16:00,1 +1/18/2017 17:00,0 +1/18/2017 18:00,-2 +1/18/2017 19:00,-3 +1/18/2017 20:00,-3 +1/18/2017 21:00,-4 +1/18/2017 22:00,-3 +1/18/2017 23:00,-4 +1/19/2017 0:00,-5 +1/19/2017 1:00,-6 +1/19/2017 2:00,-6 +1/19/2017 3:00,-7 +1/19/2017 4:00,-7 +1/19/2017 5:00,-7 +1/19/2017 6:00,-7 +1/19/2017 7:00,-7 +1/19/2017 8:00,-7 +1/19/2017 9:00,-4 +1/19/2017 10:00,-3 +1/19/2017 11:00,-2 +1/19/2017 12:00,-1 +1/19/2017 13:00,0 +1/19/2017 14:00,0 +1/19/2017 15:00,0 +1/19/2017 16:00,0 +1/19/2017 17:00,0 +1/19/2017 18:00,-1 +1/19/2017 19:00,-1 +1/19/2017 20:00,-1 +1/19/2017 21:00,-1 +1/19/2017 22:00,-1 +1/19/2017 23:00,-1 +1/20/2017 0:00,-1 +1/20/2017 1:00,-2 +1/20/2017 2:00,-1 +1/20/2017 3:00,-1 +1/20/2017 4:00,-2 +1/20/2017 5:00,-2 +1/20/2017 6:00,-3 +1/20/2017 7:00,-2 +1/20/2017 8:00,-2 +1/20/2017 9:00,-2 +1/20/2017 10:00,-1 +1/20/2017 11:00,1 +1/20/2017 12:00,2 +1/20/2017 13:00,1 +1/20/2017 14:00,1 +1/20/2017 15:00,1 +1/20/2017 16:00,0 +1/20/2017 17:00,0 +1/20/2017 18:00,0 +1/20/2017 19:00,0 +1/20/2017 20:00,0 +1/20/2017 21:00,0 +1/20/2017 22:00,-1 +1/20/2017 23:00,-1 +1/21/2017 0:00,-1 +1/21/2017 1:00,-1 +1/21/2017 2:00,-1 +1/21/2017 3:00,-1 +1/21/2017 4:00,-1 +1/21/2017 5:00,-1 +1/21/2017 6:00,-1 +1/21/2017 7:00,-1 +1/21/2017 8:00,-1 +1/21/2017 9:00,0 +1/21/2017 10:00,1 +1/21/2017 11:00,1 +1/21/2017 12:00,1 +1/21/2017 13:00,2 +1/21/2017 14:00,1 +1/21/2017 15:00,1 +1/21/2017 16:00,1 +1/21/2017 17:00,1 +1/21/2017 18:00,1 +1/21/2017 19:00,1 +1/21/2017 20:00,1 +1/21/2017 21:00,1 +1/21/2017 22:00,1 +1/21/2017 23:00,1 +1/22/2017 0:00,1 +1/22/2017 1:00,1 +1/22/2017 2:00,1 +1/22/2017 3:00,0 +1/22/2017 4:00,0 +1/22/2017 5:00,0 +1/22/2017 6:00,-1 +1/22/2017 7:00,-1 +1/22/2017 8:00,-1 +1/22/2017 9:00,-0.1 +1/22/2017 10:00,1 +1/22/2017 11:00,1 +1/22/2017 12:00,2 +1/22/2017 13:00,2 +1/22/2017 14:00,2 +1/22/2017 15:00,2 +1/22/2017 16:00,2 +1/22/2017 17:00,2 +1/22/2017 18:00,1 +1/22/2017 19:00,2 +1/22/2017 20:00,1 +1/22/2017 21:00,1 +1/22/2017 22:00,1 +1/22/2017 23:00,1 +1/23/2017 0:00,1 +1/23/2017 1:00,1 +1/23/2017 2:00,1 +1/23/2017 3:00,0 +1/23/2017 4:00,0 +1/23/2017 5:00,0 +1/23/2017 6:00,0 +1/23/2017 7:00,0 +1/23/2017 8:00,-1 +1/23/2017 9:00,0 +1/23/2017 10:00,0 +1/23/2017 11:00,1 +1/23/2017 12:00,1 +1/23/2017 13:00,2 +1/23/2017 14:00,2 +1/23/2017 15:00,2 +1/23/2017 16:00,1 +1/23/2017 17:00,1 +1/23/2017 18:00,0 +1/23/2017 19:00,0 +1/23/2017 20:00,-1 +1/23/2017 21:00,-1 +1/23/2017 22:00,-1 +1/23/2017 23:00,0 +1/24/2017 0:00,0 +1/24/2017 1:00,0 +1/24/2017 2:00,0 +1/24/2017 3:00,0 +1/24/2017 4:00,0 +1/24/2017 5:00,0 +1/24/2017 6:00,0 +1/24/2017 7:00,0 +1/24/2017 8:00,0 +1/24/2017 9:00,0 +1/24/2017 10:00,1 +1/24/2017 11:00,1 +1/24/2017 12:00,1 +1/24/2017 13:00,1 +1/24/2017 14:00,1 +1/24/2017 15:00,1 +1/24/2017 16:00,1 +1/24/2017 17:00,1 +1/24/2017 18:00,1 +1/24/2017 19:00,1 +1/24/2017 20:00,1 +1/24/2017 21:00,1 +1/24/2017 22:00,1 +1/24/2017 23:00,1 +1/25/2017 0:00,1 +1/25/2017 1:00,1 +1/25/2017 2:00,1 +1/25/2017 3:00,1 +1/25/2017 4:00,1 +1/25/2017 5:00,1 +1/25/2017 6:00,1 +1/25/2017 7:00,1 +1/25/2017 8:00,1 +1/25/2017 9:00,1 +1/25/2017 10:00,2 +1/25/2017 11:00,3 +1/25/2017 12:00,3 +1/25/2017 13:00,4 +1/25/2017 14:00,5 +1/25/2017 15:00,5 +1/25/2017 16:00,4 +1/25/2017 17:00,3 +1/25/2017 18:00,3 +1/25/2017 19:00,3 +1/25/2017 20:00,2 +1/25/2017 21:00,2 +1/25/2017 22:00,2 +1/25/2017 23:00,1 +1/26/2017 0:00,1 +1/26/2017 1:00,1 +1/26/2017 2:00,-1 +1/26/2017 3:00,1 +1/26/2017 4:00,1 +1/26/2017 5:00,1 +1/26/2017 6:00,-1 +1/26/2017 7:00,1 +1/26/2017 8:00,0 +1/26/2017 9:00,2 +1/26/2017 10:00,3 +1/26/2017 11:00,6 +1/26/2017 12:00,7 +1/26/2017 13:00,8 +1/26/2017 14:00,8 +1/26/2017 15:00,8 +1/26/2017 16:00,8 +1/26/2017 17:00,5 +1/26/2017 18:00,4 +1/26/2017 19:00,-1 +1/26/2017 20:00,-1 +1/26/2017 21:00,-1.6 +1/26/2017 22:00,-2 +1/26/2017 23:00,-2 +1/27/2017 0:00,-3 +1/27/2017 1:00,-4 +1/27/2017 2:00,-5 +1/27/2017 3:00,-4 +1/27/2017 4:00,-4 +1/27/2017 5:00,-5 +1/27/2017 6:00,-5 +1/27/2017 7:00,-6 +1/27/2017 8:00,-4 +1/27/2017 9:00,-1 +1/27/2017 10:00,1 +1/27/2017 11:00,2 +1/27/2017 12:00,4 +1/27/2017 13:00,5 +1/27/2017 14:00,6 +1/27/2017 15:00,6 +1/27/2017 16:00,6 +1/27/2017 17:00,2 +1/27/2017 18:00,0 +1/27/2017 19:00,-1 +1/27/2017 20:00,-2 +1/27/2017 21:00,-3 +1/27/2017 22:00,-4 +1/27/2017 23:00,-4 +1/28/2017 0:00,-4 +1/28/2017 1:00,-4 +1/28/2017 2:00,-3 +1/28/2017 3:00,-2 +1/28/2017 4:00,-2 +1/28/2017 5:00,-2 +1/28/2017 6:00,-2 +1/28/2017 7:00,-2 +1/28/2017 8:00,-2 +1/28/2017 9:00,-2 +1/28/2017 10:00,-1 +1/28/2017 11:00,-1 +1/28/2017 12:00,-1 +1/28/2017 13:00,-1 +1/28/2017 14:00,-1 +1/28/2017 15:00,-1 +1/28/2017 16:00,-1 +1/28/2017 17:00,-1 +1/28/2017 18:00,-1 +1/28/2017 19:00,-1 +1/28/2017 20:00,0 +1/28/2017 21:00,0 +1/28/2017 22:00,-1 +1/28/2017 23:00,-1 +1/29/2017 0:00,-1 +1/29/2017 1:00,-1 +1/29/2017 2:00,-2 +1/29/2017 3:00,-2 +1/29/2017 4:00,-2 +1/29/2017 5:00,-2 +1/29/2017 6:00,-2 +1/29/2017 7:00,-1 +1/29/2017 8:00,-1 +1/29/2017 9:00,-1 +1/29/2017 10:00,-1 +1/29/2017 11:00,-1 +1/29/2017 12:00,-1 +1/29/2017 13:00,-1 +1/29/2017 14:00,-1 +1/29/2017 15:00,-1 +1/29/2017 16:00,-1 +1/29/2017 17:00,-1 +1/29/2017 18:00,-1 +1/29/2017 19:00,-1 +1/29/2017 20:00,-1 +1/29/2017 21:00,-1 +1/29/2017 22:00,-1 +1/29/2017 23:00,-1 +1/30/2017 0:00,-2 +1/30/2017 1:00,-2 +1/30/2017 2:00,-3 +1/30/2017 3:00,-5 +1/30/2017 4:00,-6 +1/30/2017 5:00,-6 +1/30/2017 6:00,-7 +1/30/2017 7:00,-7 +1/30/2017 8:00,-7 +1/30/2017 9:00,-4 +1/30/2017 10:00,-3 +1/30/2017 11:00,-2 +1/30/2017 12:00,-2 +1/30/2017 13:00,-2 +1/30/2017 14:00,-1 +1/30/2017 15:00,-1 +1/30/2017 16:00,-1 +1/30/2017 17:00,-1 +1/30/2017 18:00,-1 +1/30/2017 19:00,0 +1/30/2017 20:00,-1 +1/30/2017 21:00,0 +1/30/2017 22:00,-1 +1/30/2017 23:00,-1 +1/31/2017 0:00,-1 +1/31/2017 1:00,-1 +1/31/2017 2:00,-1 +1/31/2017 3:00,-1 +1/31/2017 4:00,-1 +1/31/2017 5:00,-1 +1/31/2017 6:00,-2 +1/31/2017 7:00,-1 +1/31/2017 8:00,-3 +1/31/2017 9:00,-2 +1/31/2017 10:00,-2 +1/31/2017 11:00,-1 +1/31/2017 12:00,0 +1/31/2017 13:00,2 +1/31/2017 14:00,2 +1/31/2017 15:00,2 +1/31/2017 16:00,1 +1/31/2017 17:00,0 +1/31/2017 18:00,1 +1/31/2017 19:00,1 +1/31/2017 20:00,1 +1/31/2017 21:00,1 +1/31/2017 22:00,1.4 +1/31/2017 23:00,1.9 +2/1/2017 0:00,2.3 +2/1/2017 1:00,2.7 +2/1/2017 2:00,3.1 +2/1/2017 3:00,3.6 +2/1/2017 4:00,4 +2/1/2017 5:00,4 +2/1/2017 6:00,4 +2/1/2017 7:00,4 +2/1/2017 8:00,4 +2/1/2017 9:00,5 +2/1/2017 10:00,6 +2/1/2017 11:00,7 +2/1/2017 12:00,8 +2/1/2017 13:00,8 +2/1/2017 14:00,10 +2/1/2017 15:00,11 +2/1/2017 16:00,10 +2/1/2017 17:00,8 +2/1/2017 18:00,6 +2/1/2017 19:00,4 +2/1/2017 20:00,4 +2/1/2017 21:00,2 +2/1/2017 22:00,2 +2/1/2017 23:00,-1 +2/2/2017 0:00,1 +2/2/2017 1:00,0 +2/2/2017 2:00,0 +2/2/2017 3:00,-1 +2/2/2017 4:00,-2 +2/2/2017 5:00,-4 +2/2/2017 6:00,-4 +2/2/2017 7:00,-5 +2/2/2017 8:00,-4 +2/2/2017 9:00,-1 +2/2/2017 10:00,1 +2/2/2017 11:00,3 +2/2/2017 12:00,5 +2/2/2017 13:00,7 +2/2/2017 14:00,8 +2/2/2017 15:00,9 +2/2/2017 16:00,9 +2/2/2017 17:00,4 +2/2/2017 18:00,1 +2/2/2017 19:00,1 +2/2/2017 20:00,0 +2/2/2017 21:00,-1 +2/2/2017 22:00,-3 +2/2/2017 23:00,-4 +2/3/2017 0:00,-5 +2/3/2017 1:00,-6 +2/3/2017 2:00,-4 +2/3/2017 3:00,-6 +2/3/2017 4:00,-6 +2/3/2017 5:00,-7 +2/3/2017 6:00,-5 +2/3/2017 7:00,-3 +2/3/2017 8:00,-1 +2/3/2017 9:00,1 +2/3/2017 10:00,2 +2/3/2017 11:00,4 +2/3/2017 12:00,8 +2/3/2017 13:00,8 +2/3/2017 14:00,9 +2/3/2017 15:00,9 +2/3/2017 16:00,10 +2/3/2017 17:00,8 +2/3/2017 18:00,6 +2/3/2017 19:00,4 +2/3/2017 20:00,3 +2/3/2017 21:00,2 +2/3/2017 22:00,-1 +2/3/2017 23:00,-4 +2/4/2017 0:00,-4 +2/4/2017 1:00,-4 +2/4/2017 2:00,-6 +2/4/2017 3:00,-6 +2/4/2017 4:00,-6 +2/4/2017 5:00,-6 +2/4/2017 6:00,-6 +2/4/2017 7:00,-7 +2/4/2017 8:00,-6 +2/4/2017 9:00,-3 +2/4/2017 10:00,-1 +2/4/2017 11:00,0 +2/4/2017 12:00,1 +2/4/2017 13:00,2 +2/4/2017 14:00,5 +2/4/2017 15:00,6 +2/4/2017 16:00,5 +2/4/2017 17:00,2 +2/4/2017 18:00,-1 +2/4/2017 19:00,-1 +2/4/2017 20:00,-2 +2/4/2017 21:00,-2 +2/4/2017 22:00,-3 +2/4/2017 23:00,-3 +2/5/2017 0:00,-4 +2/5/2017 1:00,-5 +2/5/2017 2:00,-6 +2/5/2017 3:00,-6 +2/5/2017 4:00,-5.1 +2/5/2017 5:00,-4 +2/5/2017 6:00,-3 +2/5/2017 7:00,-2 +2/5/2017 8:00,-1 +2/5/2017 9:00,-1 +2/5/2017 10:00,-1 +2/5/2017 11:00,-1 +2/5/2017 12:00,-1 +2/5/2017 13:00,0 +2/5/2017 14:00,0 +2/5/2017 15:00,0 +2/5/2017 16:00,2 +2/5/2017 17:00,2 +2/5/2017 18:00,1 +2/5/2017 19:00,-1 +2/5/2017 20:00,-2 +2/5/2017 21:00,-3 +2/5/2017 22:00,-3 +2/5/2017 23:00,-4 +2/6/2017 0:00,-5 +2/6/2017 1:00,-4 +2/6/2017 2:00,-4 +2/6/2017 3:00,-4 +2/6/2017 4:00,-3 +2/6/2017 5:00,-3 +2/6/2017 6:00,-3 +2/6/2017 7:00,-2 +2/6/2017 8:00,-2 +2/6/2017 9:00,-1 +2/6/2017 10:00,0 +2/6/2017 11:00,1 +2/6/2017 12:00,2 +2/6/2017 13:00,2 +2/6/2017 14:00,2 +2/6/2017 15:00,3 +2/6/2017 16:00,3 +2/6/2017 17:00,3 +2/6/2017 18:00,2 +2/6/2017 19:00,3 +2/6/2017 20:00,4 +2/6/2017 21:00,6 +2/6/2017 22:00,7 +2/6/2017 23:00,3 +2/7/2017 0:00,8 +2/7/2017 1:00,9 +2/7/2017 2:00,9 +2/7/2017 3:00,9 +2/7/2017 4:00,7 +2/7/2017 5:00,6 +2/7/2017 6:00,7 +2/7/2017 7:00,7 +2/7/2017 8:00,7 +2/7/2017 9:00,6 +2/7/2017 10:00,7 +2/7/2017 11:00,8 +2/7/2017 12:00,7 +2/7/2017 13:00,8 +2/7/2017 14:00,8 +2/7/2017 15:00,7 +2/7/2017 16:00,6 +2/7/2017 17:00,6 +2/7/2017 18:00,6 +2/7/2017 19:00,4 +2/7/2017 20:00,3 +2/7/2017 21:00,6 +2/7/2017 22:00,7 +2/7/2017 23:00,6 +2/8/2017 0:00,5 +2/8/2017 1:00,5 +2/8/2017 2:00,5 +2/8/2017 3:00,6 +2/8/2017 4:00,6 +2/8/2017 5:00,6 +2/8/2017 6:00,5 +2/8/2017 7:00,4 +2/8/2017 8:00,5 +2/8/2017 9:00,5.9 +2/8/2017 10:00,7 +2/8/2017 11:00,9 +2/8/2017 12:00,11 +2/8/2017 13:00,12 +2/8/2017 14:00,12 +2/8/2017 15:00,12 +2/8/2017 16:00,12 +2/8/2017 17:00,10 +2/8/2017 18:00,6 +2/8/2017 19:00,6 +2/8/2017 20:00,5 +2/8/2017 21:00,3 +2/8/2017 22:00,4 +2/8/2017 23:00,2 +2/9/2017 0:00,-1 +2/9/2017 1:00,-2 +2/9/2017 2:00,-1 +2/9/2017 3:00,2 +2/9/2017 4:00,-1 +2/9/2017 5:00,-3 +2/9/2017 6:00,-3 +2/9/2017 7:00,-3 +2/9/2017 8:00,-2 +2/9/2017 9:00,1 +2/9/2017 10:00,3 +2/9/2017 11:00,6 +2/9/2017 12:00,7 +2/9/2017 13:00,7 +2/9/2017 14:00,7 +2/9/2017 15:00,8 +2/9/2017 16:00,7 +2/9/2017 17:00,6 +2/9/2017 18:00,3 +2/9/2017 19:00,1 +2/9/2017 20:00,1 +2/9/2017 21:00,-1 +2/9/2017 22:00,-2 +2/9/2017 23:00,-2 +2/10/2017 0:00,-2 +2/10/2017 1:00,-2 +2/10/2017 2:00,-2 +2/10/2017 3:00,-3 +2/10/2017 4:00,-4 +2/10/2017 5:00,-4 +2/10/2017 6:00,-4 +2/10/2017 7:00,-5 +2/10/2017 8:00,-3 +2/10/2017 9:00,-1 +2/10/2017 10:00,2 +2/10/2017 11:00,3 +2/10/2017 12:00,4 +2/10/2017 13:00,5 +2/10/2017 14:00,6 +2/10/2017 15:00,7 +2/10/2017 16:00,7 +2/10/2017 17:00,8 +2/10/2017 18:00,7 +2/10/2017 19:00,8 +2/10/2017 20:00,7 +2/10/2017 21:00,8 +2/10/2017 22:00,7 +2/10/2017 23:00,7 +2/11/2017 0:00,6 +2/11/2017 1:00,4 +2/11/2017 2:00,3 +2/11/2017 3:00,3 +2/11/2017 4:00,1 +2/11/2017 5:00,-1 +2/11/2017 6:00,-2 +2/11/2017 7:00,-1 +2/11/2017 8:00,0 +2/11/2017 9:00,2 +2/11/2017 10:00,4 +2/11/2017 11:00,8 +2/11/2017 12:00,8 +2/11/2017 13:00,9 +2/11/2017 14:00,9 +2/11/2017 15:00,9 +2/11/2017 16:00,9 +2/11/2017 17:00,8 +2/11/2017 18:00,3 +2/11/2017 19:00,-1 +2/11/2017 20:00,-1 +2/11/2017 21:00,-1 +2/11/2017 22:00,-4 +2/11/2017 23:00,-4 +2/12/2017 0:00,-4 +2/12/2017 1:00,-6 +2/12/2017 2:00,-6 +2/12/2017 3:00,-7 +2/12/2017 4:00,-8 +2/12/2017 5:00,-7 +2/12/2017 6:00,-8 +2/12/2017 7:00,-8 +2/12/2017 8:00,-6 +2/12/2017 9:00,-3 +2/12/2017 10:00,0 +2/12/2017 11:00,2 +2/12/2017 12:00,3 +2/12/2017 13:00,5 +2/12/2017 14:00,6 +2/12/2017 15:00,8 +2/12/2017 16:00,8 +2/12/2017 17:00,6 +2/12/2017 18:00,0 +2/12/2017 19:00,0 +2/12/2017 20:00,0 +2/12/2017 21:00,0 +2/12/2017 22:00,-1 +2/12/2017 23:00,-1 +2/13/2017 0:00,-2 +2/13/2017 1:00,-3 +2/13/2017 2:00,-5 +2/13/2017 3:00,-6 +2/13/2017 4:00,-6 +2/13/2017 5:00,-7 +2/13/2017 6:00,-7 +2/13/2017 7:00,-7 +2/13/2017 8:00,-7 +2/13/2017 9:00,-2 +2/13/2017 10:00,1 +2/13/2017 11:00,4 +2/13/2017 12:00,7 +2/13/2017 13:00,8 +2/13/2017 14:00,9 +2/13/2017 15:00,9 +2/13/2017 16:00,9 +2/13/2017 17:00,7 +2/13/2017 18:00,3 +2/13/2017 19:00,0 +2/13/2017 20:00,0 +2/13/2017 21:00,3 +2/13/2017 22:00,4 +2/13/2017 23:00,1 +2/14/2017 0:00,0 +2/14/2017 1:00,-3 +2/14/2017 2:00,-1 +2/14/2017 3:00,-4 +2/14/2017 4:00,-2 +2/14/2017 5:00,-6 +2/14/2017 6:00,-5 +2/14/2017 7:00,-6 +2/14/2017 8:00,-4 +2/14/2017 9:00,0 +2/14/2017 10:00,3 +2/14/2017 11:00,4 +2/14/2017 12:00,6 +2/14/2017 13:00,8 +2/14/2017 14:00,9 +2/14/2017 15:00,10 +2/14/2017 16:00,10 +2/14/2017 17:00,9 +2/14/2017 18:00,4 +2/14/2017 19:00,3 +2/14/2017 20:00,2 +2/14/2017 21:00,0.7 +2/14/2017 22:00,-0.7 +2/14/2017 23:00,-1.8 +2/15/2017 0:00,-3 +2/15/2017 1:00,-4 +2/15/2017 2:00,-4 +2/15/2017 3:00,-4 +2/15/2017 4:00,-4 +2/15/2017 5:00,-6 +2/15/2017 6:00,-7 +2/15/2017 7:00,-6 +2/15/2017 8:00,-4 +2/15/2017 9:00,0 +2/15/2017 10:00,3 +2/15/2017 11:00,5 +2/15/2017 12:00,7 +2/15/2017 13:00,8 +2/15/2017 14:00,10 +2/15/2017 15:00,11 +2/15/2017 16:00,11 +2/15/2017 17:00,8 +2/15/2017 18:00,4 +2/15/2017 19:00,2 +2/15/2017 20:00,2 +2/15/2017 21:00,1 +2/15/2017 22:00,-1 +2/15/2017 23:00,-2 +2/16/2017 0:00,-1 +2/16/2017 1:00,-3 +2/16/2017 2:00,-3 +2/16/2017 3:00,-3 +2/16/2017 4:00,-3.6 +2/16/2017 5:00,-4 +2/16/2017 6:00,-3 +2/16/2017 7:00,-4 +2/16/2017 8:00,-3 +2/16/2017 9:00,0 +2/16/2017 10:00,2 +2/16/2017 11:00,5 +2/16/2017 12:00,7 +2/16/2017 13:00,9 +2/16/2017 14:00,10 +2/16/2017 15:00,11 +2/16/2017 16:00,11 +2/16/2017 17:00,8 +2/16/2017 18:00,4 +2/16/2017 19:00,3 +2/16/2017 20:00,3 +2/16/2017 21:00,2 +2/16/2017 22:00,-1 +2/16/2017 23:00,-1 +2/17/2017 0:00,-2 +2/17/2017 1:00,-3 +2/17/2017 2:00,-4 +2/17/2017 3:00,-4 +2/17/2017 4:00,-4 +2/17/2017 5:00,-4 +2/17/2017 6:00,-6 +2/17/2017 7:00,-5 +2/17/2017 8:00,-2 +2/17/2017 9:00,1 +2/17/2017 10:00,3 +2/17/2017 11:00,5 +2/17/2017 12:00,7 +2/17/2017 13:00,9 +2/17/2017 14:00,10 +2/17/2017 15:00,11 +2/17/2017 16:00,11 +2/17/2017 17:00,9 +2/17/2017 18:00,8 +2/17/2017 19:00,6 +2/17/2017 20:00,3 +2/17/2017 21:00,3 +2/17/2017 22:00,3 +2/17/2017 23:00,4 +2/18/2017 0:00,4 +2/18/2017 1:00,4 +2/18/2017 2:00,4 +2/18/2017 3:00,4 +2/18/2017 4:00,3 +2/18/2017 5:00,3 +2/18/2017 6:00,5 +2/18/2017 7:00,5 +2/18/2017 8:00,2 +2/18/2017 9:00,8 +2/18/2017 10:00,10 +2/18/2017 11:00,11 +2/18/2017 12:00,13 +2/18/2017 13:00,14 +2/18/2017 14:00,14 +2/18/2017 15:00,15 +2/18/2017 16:00,14 +2/18/2017 17:00,12 +2/18/2017 18:00,9 +2/18/2017 19:00,8 +2/18/2017 20:00,7 +2/18/2017 21:00,6 +2/18/2017 22:00,5 +2/18/2017 23:00,7 +2/19/2017 0:00,11 +2/19/2017 1:00,9 +2/19/2017 2:00,10 +2/19/2017 3:00,9 +2/19/2017 4:00,9 +2/19/2017 5:00,8 +2/19/2017 6:00,10 +2/19/2017 7:00,10 +2/19/2017 8:00,10 +2/19/2017 9:00,12 +2/19/2017 10:00,13 +2/19/2017 11:00,14 +2/19/2017 12:00,13 +2/19/2017 13:00,13 +2/19/2017 14:00,13 +2/19/2017 15:00,14 +2/19/2017 16:00,14 +2/19/2017 17:00,12 +2/19/2017 18:00,11 +2/19/2017 19:00,9 +2/19/2017 20:00,8 +2/19/2017 21:00,4 +2/19/2017 22:00,6 +2/19/2017 23:00,6 +2/20/2017 0:00,6 +2/20/2017 1:00,2 +2/20/2017 2:00,-2 +2/20/2017 3:00,-3 +2/20/2017 4:00,-5 +2/20/2017 5:00,-4 +2/20/2017 6:00,-3 +2/20/2017 7:00,-3 +2/20/2017 8:00,-2 +2/20/2017 9:00,3 +2/20/2017 10:00,6 +2/20/2017 11:00,9 +2/20/2017 12:00,11 +2/20/2017 13:00,12 +2/20/2017 14:00,12 +2/20/2017 15:00,12 +2/20/2017 16:00,13 +2/20/2017 17:00,11 +2/20/2017 18:00,7 +2/20/2017 19:00,6 +2/20/2017 20:00,4 +2/20/2017 21:00,2 +2/20/2017 22:00,0 +2/20/2017 23:00,0 +2/21/2017 0:00,-1 +2/21/2017 1:00,0 +2/21/2017 2:00,1 +2/21/2017 3:00,1 +2/21/2017 4:00,2 +2/21/2017 5:00,3 +2/21/2017 6:00,3 +2/21/2017 7:00,4 +2/21/2017 8:00,5 +2/21/2017 9:00,5.4 +2/21/2017 10:00,6 +2/21/2017 11:00,7 +2/21/2017 12:00,9 +2/21/2017 13:00,13 +2/21/2017 14:00,14 +2/21/2017 15:00,17 +2/21/2017 16:00,18 +2/21/2017 17:00,16 +2/21/2017 18:00,16 +2/21/2017 19:00,16 +2/21/2017 20:00,15 +2/21/2017 21:00,14 +2/21/2017 22:00,13 +2/21/2017 23:00,12 +2/22/2017 0:00,12 +2/22/2017 1:00,13 +2/22/2017 2:00,13 +2/22/2017 3:00,14 +2/22/2017 4:00,13 +2/22/2017 5:00,14 +2/22/2017 6:00,14 +2/22/2017 7:00,13 +2/22/2017 8:00,13 +2/22/2017 9:00,13 +2/22/2017 10:00,14 +2/22/2017 11:00,14 +2/22/2017 12:00,16 +2/22/2017 13:00,16 +2/22/2017 14:00,17 +2/22/2017 15:00,17 +2/22/2017 16:00,18 +2/22/2017 17:00,15 +2/22/2017 18:00,13 +2/22/2017 19:00,11 +2/22/2017 20:00,11 +2/22/2017 21:00,9 +2/22/2017 22:00,8 +2/22/2017 23:00,6 +2/23/2017 0:00,7 +2/23/2017 1:00,7 +2/23/2017 2:00,7 +2/23/2017 3:00,9 +2/23/2017 4:00,9 +2/23/2017 5:00,8 +2/23/2017 6:00,9 +2/23/2017 7:00,8 +2/23/2017 8:00,7 +2/23/2017 9:00,7 +2/23/2017 10:00,7 +2/23/2017 11:00,8 +2/23/2017 12:00,9 +2/23/2017 13:00,11 +2/23/2017 14:00,13 +2/23/2017 15:00,13 +2/23/2017 16:00,13 +2/23/2017 17:00,12 +2/23/2017 18:00,10 +2/23/2017 19:00,9 +2/23/2017 20:00,8 +2/23/2017 21:00,6 +2/23/2017 22:00,5 +2/23/2017 23:00,4 +2/24/2017 0:00,4 +2/24/2017 1:00,4 +2/24/2017 2:00,4 +2/24/2017 3:00,3 +2/24/2017 4:00,3 +2/24/2017 5:00,2 +2/24/2017 6:00,2 +2/24/2017 7:00,1 +2/24/2017 8:00,1 +2/24/2017 9:00,2 +2/24/2017 10:00,3 +2/24/2017 11:00,3 +2/24/2017 12:00,4 +2/24/2017 13:00,6 +2/24/2017 14:00,5 +2/24/2017 15:00,6 +2/24/2017 16:00,6 +2/24/2017 17:00,4 +2/24/2017 18:00,1 +2/24/2017 19:00,1 +2/24/2017 20:00,-1 +2/24/2017 21:00,-1 +2/24/2017 22:00,-3 +2/24/2017 23:00,-3 +2/25/2017 0:00,-4 +2/25/2017 1:00,-5 +2/25/2017 2:00,-6 +2/25/2017 3:00,-6 +2/25/2017 4:00,-7 +2/25/2017 5:00,-7 +2/25/2017 6:00,-8 +2/25/2017 7:00,-8 +2/25/2017 8:00,-5 +2/25/2017 9:00,-3 +2/25/2017 10:00,-1 +2/25/2017 11:00,2 +2/25/2017 12:00,3 +2/25/2017 13:00,4 +2/25/2017 14:00,6 +2/25/2017 15:00,6 +2/25/2017 16:00,6 +2/25/2017 17:00,5 +2/25/2017 18:00,2 +2/25/2017 19:00,1 +2/25/2017 20:00,-1 +2/25/2017 21:00,-1 +2/25/2017 22:00,-3 +2/25/2017 23:00,-3 +2/26/2017 0:00,-4 +2/26/2017 1:00,-6 +2/26/2017 2:00,-6 +2/26/2017 3:00,-4 +2/26/2017 4:00,-4 +2/26/2017 5:00,-6 +2/26/2017 6:00,-4 +2/26/2017 7:00,-4 +2/26/2017 8:00,-2 +2/26/2017 9:00,-1 +2/26/2017 10:00,-1 +2/26/2017 11:00,1 +2/26/2017 12:00,3 +2/26/2017 13:00,4 +2/26/2017 14:00,5 +2/26/2017 15:00,5 +2/26/2017 16:00,5 +2/26/2017 17:00,5 +2/26/2017 18:00,3 +2/26/2017 19:00,0 +2/26/2017 20:00,-2 +2/26/2017 21:00,-3 +2/26/2017 22:00,-4 +2/26/2017 23:00,-4 +2/27/2017 0:00,-5 +2/27/2017 1:00,-5 +2/27/2017 2:00,-6 +2/27/2017 3:00,-6 +2/27/2017 4:00,-6 +2/27/2017 5:00,-6 +2/27/2017 6:00,-8 +2/27/2017 7:00,-7 +2/27/2017 8:00,-4 +2/27/2017 9:00,-2 +2/27/2017 10:00,0 +2/27/2017 11:00,3 +2/27/2017 12:00,5 +2/27/2017 13:00,6 +2/27/2017 14:00,7 +2/27/2017 15:00,7 +2/27/2017 16:00,7 +2/27/2017 17:00,7 +2/27/2017 18:00,5 +2/27/2017 19:00,4 +2/27/2017 20:00,1.3 +2/27/2017 21:00,-1 +2/27/2017 22:00,6 +2/27/2017 23:00,4 +2/28/2017 0:00,2 +2/28/2017 1:00,0 +2/28/2017 2:00,1 +2/28/2017 3:00,-1 +2/28/2017 4:00,0 +2/28/2017 5:00,1 +2/28/2017 6:00,1 +2/28/2017 7:00,-2 +2/28/2017 8:00,0 +2/28/2017 9:00,3 +2/28/2017 10:00,7 +2/28/2017 11:00,8 +2/28/2017 12:00,8 +2/28/2017 13:00,9 +2/28/2017 14:00,10 +2/28/2017 15:00,10 +2/28/2017 16:00,10 +2/28/2017 17:00,9 +2/28/2017 18:00,4 +2/28/2017 19:00,3 +2/28/2017 20:00,0 +2/28/2017 21:00,-2 +2/28/2017 22:00,-1 +2/28/2017 23:00,-0.2 +3/1/2017 0:00,0.6 +3/1/2017 1:00,1.4 +3/1/2017 2:00,2.3 +3/1/2017 3:00,3.1 +3/1/2017 4:00,4 +3/1/2017 5:00,4 +3/1/2017 6:00,4 +3/1/2017 7:00,4 +3/1/2017 8:00,4 +3/1/2017 9:00,5.4 +3/1/2017 10:00,6.4 +3/1/2017 11:00,7 +3/1/2017 12:00,7 +3/1/2017 13:00,8 +3/1/2017 14:00,8 +3/1/2017 15:00,7.1 +3/1/2017 16:00,7.2 +3/1/2017 17:00,7.4 +3/1/2017 18:00,7.5 +3/1/2017 19:00,7.6 +3/1/2017 20:00,7.7 +3/1/2017 21:00,7.8 +3/1/2017 22:00,6.9 +3/1/2017 23:00,6.4 +3/2/2017 0:00,5.8 +3/2/2017 1:00,5.1 +3/2/2017 2:00,4.6 +3/2/2017 3:00,4.2 +3/2/2017 4:00,3.8 +3/2/2017 5:00,3.8 +3/2/2017 6:00,2.9 +3/2/2017 7:00,4 +3/2/2017 8:00,4.8 +3/2/2017 9:00,5.6 +3/2/2017 10:00,6 +3/2/2017 11:00,8 +3/2/2017 12:00,9.7 +3/2/2017 13:00,11 +3/2/2017 14:00,12 +3/2/2017 15:00,11.8 +3/2/2017 16:00,11.2 +3/2/2017 17:00,10 +3/2/2017 18:00,8.7 +3/2/2017 19:00,7.1 +3/2/2017 20:00,6.1 +3/2/2017 21:00,5.5 +3/2/2017 22:00,5 +3/2/2017 23:00,4.1 +3/3/2017 0:00,4.2 +3/3/2017 1:00,3.2 +3/3/2017 2:00,1.3 +3/3/2017 3:00,-1.6 +3/3/2017 4:00,-1.5 +3/3/2017 5:00,-2.5 +3/3/2017 6:00,-2.4 +3/3/2017 7:00,-3.3 +3/3/2017 8:00,1.8 +3/3/2017 9:00,5.8 +3/3/2017 10:00,7.9 +3/3/2017 11:00,9 +3/3/2017 12:00,10 +3/3/2017 13:00,9 +3/3/2017 14:00,11 +3/3/2017 15:00,12 +3/3/2017 16:00,10 +3/3/2017 17:00,10 +3/3/2017 18:00,7 +3/3/2017 19:00,6 +3/3/2017 20:00,7 +3/3/2017 21:00,5 +3/3/2017 22:00,3 +3/3/2017 23:00,3 +3/4/2017 0:00,4 +3/4/2017 1:00,3 +3/4/2017 2:00,2 +3/4/2017 3:00,2 +3/4/2017 4:00,1 +3/4/2017 5:00,-1 +3/4/2017 6:00,0 +3/4/2017 7:00,0 +3/4/2017 8:00,1 +3/4/2017 9:00,4 +3/4/2017 10:00,6 +3/4/2017 11:00,7 +3/4/2017 12:00,8 +3/4/2017 13:00,9 +3/4/2017 14:00,9 +3/4/2017 15:00,9 +3/4/2017 16:00,9 +3/4/2017 17:00,8 +3/4/2017 18:00,7 +3/4/2017 19:00,5.2 +3/4/2017 20:00,4 +3/4/2017 21:00,0 +3/4/2017 22:00,-0.5 +3/4/2017 23:00,-0.9 +3/5/2017 0:00,-0.9 +3/5/2017 1:00,-1.4 +3/5/2017 2:00,-1.7 +3/5/2017 3:00,-2 +3/5/2017 4:00,-2.1 +3/5/2017 5:00,-2.1 +3/5/2017 6:00,-2.1 +3/5/2017 7:00,-2.5 +3/5/2017 8:00,-1.3 +3/5/2017 9:00,1 +3/5/2017 10:00,4 +3/5/2017 11:00,6 +3/5/2017 12:00,8 +3/5/2017 13:00,9 +3/5/2017 14:00,9 +3/5/2017 15:00,11 +3/5/2017 16:00,10 +3/5/2017 17:00,8 +3/5/2017 18:00,5 +3/5/2017 19:00,4 +3/5/2017 20:00,3 +3/5/2017 21:00,2 +3/5/2017 22:00,1 +3/5/2017 23:00,2 +3/6/2017 0:00,0.5 +3/6/2017 1:00,0 +3/6/2017 2:00,-1.5 +3/6/2017 3:00,-2 +3/6/2017 4:00,-2.5 +3/6/2017 5:00,-1 +3/6/2017 6:00,-1.5 +3/6/2017 7:00,-1 +3/6/2017 8:00,0.5 +3/6/2017 9:00,3 +3/6/2017 10:00,5 +3/6/2017 11:00,7 +3/6/2017 12:00,8 +3/6/2017 13:00,9 +3/6/2017 14:00,9 +3/6/2017 15:00,8 +3/6/2017 16:00,9 +3/6/2017 17:00,9 +3/6/2017 18:00,6 +3/6/2017 19:00,4.2 +3/6/2017 20:00,3 +3/6/2017 21:00,3 +3/6/2017 22:00,3 +3/6/2017 23:00,3 +3/7/2017 0:00,2 +3/7/2017 1:00,4 +3/7/2017 2:00,3 +3/7/2017 3:00,2 +3/7/2017 4:00,3 +3/7/2017 5:00,3 +3/7/2017 6:00,3 +3/7/2017 7:00,2 +3/7/2017 8:00,4 +3/7/2017 9:00,7 +3/7/2017 10:00,9 +3/7/2017 11:00,11 +3/7/2017 12:00,11 +3/7/2017 13:00,12 +3/7/2017 14:00,12 +3/7/2017 15:00,12 +3/7/2017 16:00,11 +3/7/2017 17:00,10 +3/7/2017 18:00,7 +3/7/2017 19:00,3 +3/7/2017 20:00,2 +3/7/2017 21:00,2 +3/7/2017 22:00,2 +3/7/2017 23:00,2 +3/8/2017 0:00,2 +3/8/2017 1:00,2 +3/8/2017 2:00,3 +3/8/2017 3:00,3 +3/8/2017 4:00,3 +3/8/2017 5:00,2 +3/8/2017 6:00,3 +3/8/2017 7:00,4 +3/8/2017 8:00,3 +3/8/2017 9:00,4 +3/8/2017 10:00,5 +3/8/2017 11:00,6 +3/8/2017 12:00,8 +3/8/2017 13:00,10 +3/8/2017 14:00,12 +3/8/2017 15:00,11 +3/8/2017 16:00,12 +3/8/2017 17:00,12 +3/8/2017 18:00,11 +3/8/2017 19:00,9 +3/8/2017 20:00,9 +3/8/2017 21:00,8 +3/8/2017 22:00,7 +3/8/2017 23:00,6 +3/9/2017 0:00,7 +3/9/2017 1:00,7 +3/9/2017 2:00,7 +3/9/2017 3:00,7 +3/9/2017 4:00,7 +3/9/2017 5:00,6 +3/9/2017 6:00,6 +3/9/2017 7:00,7 +3/9/2017 8:00,9 +3/9/2017 9:00,9 +3/9/2017 10:00,11 +3/9/2017 11:00,12 +3/9/2017 12:00,13 +3/9/2017 13:00,13 +3/9/2017 14:00,13 +3/9/2017 15:00,13 +3/9/2017 16:00,13 +3/9/2017 17:00,12 +3/9/2017 18:00,12 +3/9/2017 19:00,12 +3/9/2017 20:00,11 +3/9/2017 21:00,11 +3/9/2017 22:00,11 +3/9/2017 23:00,10 +3/10/2017 0:00,9 +3/10/2017 1:00,8 +3/10/2017 2:00,8 +3/10/2017 3:00,6 +3/10/2017 4:00,5 +3/10/2017 5:00,4 +3/10/2017 6:00,1 +3/10/2017 7:00,2 +3/10/2017 8:00,5 +3/10/2017 9:00,8 +3/10/2017 10:00,9 +3/10/2017 11:00,10.1 +3/10/2017 12:00,11 +3/10/2017 13:00,12 +3/10/2017 14:00,14 +3/10/2017 15:00,12 +3/10/2017 16:00,11 +3/10/2017 17:00,9 +3/10/2017 18:00,9 +3/10/2017 19:00,9 +3/10/2017 20:00,9 +3/10/2017 21:00,8 +3/10/2017 22:00,8 +3/10/2017 23:00,7 +3/11/2017 0:00,7 +3/11/2017 1:00,6 +3/11/2017 2:00,4 +3/11/2017 3:00,3 +3/11/2017 4:00,2 +3/11/2017 5:00,3 +3/11/2017 6:00,2 +3/11/2017 7:00,3 +3/11/2017 8:00,4 +3/11/2017 9:00,7 +3/11/2017 10:00,10 +3/11/2017 11:00,12 +3/11/2017 12:00,13 +3/11/2017 13:00,14 +3/11/2017 14:00,16 +3/11/2017 15:00,18 +3/11/2017 16:00,18 +3/11/2017 17:00,17 +3/11/2017 18:00,12 +3/11/2017 19:00,8 +3/11/2017 20:00,7 +3/11/2017 21:00,7 +3/11/2017 22:00,7 +3/11/2017 23:00,6 +3/12/2017 0:00,6 +3/12/2017 1:00,3 +3/12/2017 2:00,3 +3/12/2017 3:00,3 +3/12/2017 4:00,3 +3/12/2017 5:00,3 +3/12/2017 6:00,2 +3/12/2017 7:00,3 +3/12/2017 8:00,6 +3/12/2017 9:00,9 +3/12/2017 10:00,12 +3/12/2017 11:00,13 +3/12/2017 12:00,14 +3/12/2017 13:00,17 +3/12/2017 14:00,18 +3/12/2017 15:00,19 +3/12/2017 16:00,18 +3/12/2017 17:00,16 +3/12/2017 18:00,14 +3/12/2017 19:00,14 +3/12/2017 20:00,13 +3/12/2017 21:00,12 +3/12/2017 22:00,9 +3/12/2017 23:00,9 +3/13/2017 0:00,9 +3/13/2017 1:00,7 +3/13/2017 2:00,6 +3/13/2017 3:00,3 +3/13/2017 4:00,3 +3/13/2017 5:00,2 +3/13/2017 6:00,2 +3/13/2017 7:00,2 +3/13/2017 8:00,6 +3/13/2017 9:00,9 +3/13/2017 10:00,12 +3/13/2017 11:00,15 +3/13/2017 12:00,17 +3/13/2017 13:00,18 +3/13/2017 14:00,19 +3/13/2017 15:00,19 +3/13/2017 16:00,20 +3/13/2017 17:00,19 +3/13/2017 18:00,17 +3/13/2017 19:00,14 +3/13/2017 20:00,11 +3/13/2017 21:00,9 +3/13/2017 22:00,7 +3/13/2017 23:00,7 +3/14/2017 0:00,6 +3/14/2017 1:00,5 +3/14/2017 2:00,4 +3/14/2017 3:00,3 +3/14/2017 4:00,2 +3/14/2017 5:00,2 +3/14/2017 6:00,3 +3/14/2017 7:00,3 +3/14/2017 8:00,10 +3/14/2017 9:00,12 +3/14/2017 10:00,14 +3/14/2017 11:00,17 +3/14/2017 12:00,18 +3/14/2017 13:00,19 +3/14/2017 14:00,20 +3/14/2017 15:00,20 +3/14/2017 16:00,20 +3/14/2017 17:00,18 +3/14/2017 18:00,17 +3/14/2017 19:00,14 +3/14/2017 20:00,12 +3/14/2017 21:00,8 +3/14/2017 22:00,8 +3/14/2017 23:00,7 +3/15/2017 0:00,7 +3/15/2017 1:00,6 +3/15/2017 2:00,8 +3/15/2017 3:00,8 +3/15/2017 4:00,8 +3/15/2017 5:00,7 +3/15/2017 6:00,7 +3/15/2017 7:00,7 +3/15/2017 8:00,10 +3/15/2017 9:00,14 +3/15/2017 10:00,16 +3/15/2017 11:00,17 +3/15/2017 12:00,18 +3/15/2017 13:00,19 +3/15/2017 14:00,19 +3/15/2017 15:00,18 +3/15/2017 16:00,18.3 +3/15/2017 17:00,18 +3/15/2017 18:00,17 +3/15/2017 19:00,14 +3/15/2017 20:00,12 +3/15/2017 21:00,12 +3/15/2017 22:00,12 +3/15/2017 23:00,11 +3/16/2017 0:00,10 +3/16/2017 1:00,7 +3/16/2017 2:00,7 +3/16/2017 3:00,6 +3/16/2017 4:00,4 +3/16/2017 5:00,8 +3/16/2017 6:00,9 +3/16/2017 7:00,9 +3/16/2017 8:00,9 +3/16/2017 9:00,12 +3/16/2017 10:00,14 +3/16/2017 11:00,14 +3/16/2017 12:00,11 +3/16/2017 13:00,14 +3/16/2017 14:00,16 +3/16/2017 15:00,13 +3/16/2017 16:00,12 +3/16/2017 17:00,12 +3/16/2017 18:00,9 +3/16/2017 19:00,7 +3/16/2017 20:00,7 +3/16/2017 21:00,7 +3/16/2017 22:00,3 +3/16/2017 23:00,4 +3/17/2017 0:00,3 +3/17/2017 1:00,5 +3/17/2017 2:00,1 +3/17/2017 3:00,-1 +3/17/2017 4:00,1 +3/17/2017 5:00,-1 +3/17/2017 6:00,1 +3/17/2017 7:00,2 +3/17/2017 8:00,7 +3/17/2017 9:00,9 +3/17/2017 10:00,11 +3/17/2017 11:00,12 +3/17/2017 12:00,12 +3/17/2017 13:00,14 +3/17/2017 14:00,14 +3/17/2017 15:00,14 +3/17/2017 16:00,14 +3/17/2017 17:00,13 +3/17/2017 18:00,12 +3/17/2017 19:00,9 +3/17/2017 20:00,5 +3/17/2017 21:00,2 +3/17/2017 22:00,1 +3/17/2017 23:00,1 +3/18/2017 0:00,2 +3/18/2017 1:00,2 +3/18/2017 2:00,1 +3/18/2017 3:00,0 +3/18/2017 4:00,-1 +3/18/2017 5:00,-2 +3/18/2017 6:00,-3 +3/18/2017 7:00,-3 +3/18/2017 8:00,1 +3/18/2017 9:00,4 +3/18/2017 10:00,7 +3/18/2017 11:00,10 +3/18/2017 12:00,13 +3/18/2017 13:00,14 +3/18/2017 14:00,16 +3/18/2017 15:00,17 +3/18/2017 16:00,16 +3/18/2017 17:00,16 +3/18/2017 18:00,13 +3/18/2017 19:00,9 +3/18/2017 20:00,7 +3/18/2017 21:00,5 +3/18/2017 22:00,4 +3/18/2017 23:00,3 +3/19/2017 0:00,1 +3/19/2017 1:00,0 +3/19/2017 2:00,-1 +3/19/2017 3:00,-1 +3/19/2017 4:00,-2 +3/19/2017 5:00,-3 +3/19/2017 6:00,-3 +3/19/2017 7:00,-2 +3/19/2017 8:00,3 +3/19/2017 9:00,7 +3/19/2017 10:00,9 +3/19/2017 11:00,11 +3/19/2017 12:00,13 +3/19/2017 13:00,15 +3/19/2017 14:00,16 +3/19/2017 15:00,17 +3/19/2017 16:00,17 +3/19/2017 17:00,14 +3/19/2017 18:00,14 +3/19/2017 19:00,11 +3/19/2017 20:00,9 +3/19/2017 21:00,9 +3/19/2017 22:00,7 +3/19/2017 23:00,4 +3/20/2017 0:00,1 +3/20/2017 1:00,2 +3/20/2017 2:00,2 +3/20/2017 3:00,0 +3/20/2017 4:00,1 +3/20/2017 5:00,0.5 +3/20/2017 6:00,0 +3/20/2017 7:00,2 +3/20/2017 8:00,4 +3/20/2017 9:00,7 +3/20/2017 10:00,10 +3/20/2017 11:00,13 +3/20/2017 12:00,14 +3/20/2017 13:00,17 +3/20/2017 14:00,19 +3/20/2017 15:00,20 +3/20/2017 16:00,20 +3/20/2017 17:00,17 +3/20/2017 18:00,13.6 +3/20/2017 19:00,10 +3/20/2017 20:00,7 +3/20/2017 21:00,8 +3/20/2017 22:00,11 +3/20/2017 23:00,9 +3/21/2017 0:00,11 +3/21/2017 1:00,9 +3/21/2017 2:00,8 +3/21/2017 3:00,7 +3/21/2017 4:00,5 +3/21/2017 5:00,5 +3/21/2017 6:00,4 +3/21/2017 7:00,5 +3/21/2017 8:00,9 +3/21/2017 9:00,12 +3/21/2017 10:00,13 +3/21/2017 11:00,14 +3/21/2017 12:00,16 +3/21/2017 13:00,17 +3/21/2017 14:00,17 +3/21/2017 15:00,16 +3/21/2017 16:00,15 +3/21/2017 17:00,14 +3/21/2017 18:00,13 +3/21/2017 19:00,12 +3/21/2017 20:00,10 +3/21/2017 21:00,10 +3/21/2017 22:00,10 +3/21/2017 23:00,10 +3/22/2017 0:00,10 +3/22/2017 1:00,9 +3/22/2017 2:00,9 +3/22/2017 3:00,9 +3/22/2017 4:00,9 +3/22/2017 5:00,9 +3/22/2017 6:00,8 +3/22/2017 7:00,8 +3/22/2017 8:00,11 +3/22/2017 9:00,12 +3/22/2017 10:00,14 +3/22/2017 11:00,16 +3/22/2017 12:00,17 +3/22/2017 13:00,18 +3/22/2017 14:00,17 +3/22/2017 15:00,17 +3/22/2017 16:00,16 +3/22/2017 17:00,16 +3/22/2017 18:00,14 +3/22/2017 19:00,14 +3/22/2017 20:00,13 +3/22/2017 21:00,13 +3/22/2017 22:00,13 +3/22/2017 23:00,13 +3/23/2017 0:00,11 +3/23/2017 1:00,9 +3/23/2017 2:00,9 +3/23/2017 3:00,9 +3/23/2017 4:00,9 +3/23/2017 5:00,9 +3/23/2017 6:00,9 +3/23/2017 7:00,9 +3/23/2017 8:00,9 +3/23/2017 9:00,9 +3/23/2017 10:00,9 +3/23/2017 11:00,9 +3/23/2017 12:00,9 +3/23/2017 13:00,10 +3/23/2017 14:00,11 +3/23/2017 15:00,11 +3/23/2017 16:00,11 +3/23/2017 17:00,11 +3/23/2017 18:00,11 +3/23/2017 19:00,11 +3/23/2017 20:00,10 +3/23/2017 21:00,9 +3/23/2017 22:00,7 +3/23/2017 23:00,7 +3/24/2017 0:00,7 +3/24/2017 1:00,4 +3/24/2017 2:00,3 +3/24/2017 3:00,4 +3/24/2017 4:00,3 +3/24/2017 5:00,4 +3/24/2017 6:00,2 +3/24/2017 7:00,4 +3/24/2017 8:00,8 +3/24/2017 9:00,10 +3/24/2017 10:00,11 +3/24/2017 11:00,14 +3/24/2017 12:00,16 +3/24/2017 13:00,17 +3/24/2017 14:00,18 +3/24/2017 15:00,19 +3/24/2017 16:00,18 +3/24/2017 17:00,18 +3/24/2017 18:00,16 +3/24/2017 19:00,14 +3/24/2017 20:00,13 +3/24/2017 21:00,13 +3/24/2017 22:00,12 +3/24/2017 23:00,8 +3/25/2017 0:00,10 +3/25/2017 1:00,6 +3/25/2017 2:00,7 +3/25/2017 3:00,6 +3/25/2017 4:00,6 +3/25/2017 5:00,5 +3/25/2017 6:00,6 +3/25/2017 7:00,8 +3/25/2017 8:00,10 +3/25/2017 9:00,11 +3/25/2017 10:00,12 +3/25/2017 11:00,14 +3/25/2017 12:00,16 +3/25/2017 13:00,18 +3/25/2017 14:00,17 +3/25/2017 15:00,18 +3/25/2017 16:00,17 +3/25/2017 17:00,17 +3/25/2017 18:00,15 +3/25/2017 19:00,12 +3/25/2017 20:00,10 +3/25/2017 21:00,10 +3/25/2017 22:00,9 +3/25/2017 23:00,9 +3/26/2017 0:00,8 +3/26/2017 1:00,8 +3/26/2017 2:00,8 +3/26/2017 3:00,8 +3/26/2017 4:00,9 +3/26/2017 5:00,8 +3/26/2017 6:00,8 +3/26/2017 7:00,8 +3/26/2017 8:00,9 +3/26/2017 9:00,10 +3/26/2017 10:00,12 +3/26/2017 11:00,13 +3/26/2017 12:00,13 +3/26/2017 13:00,13 +3/26/2017 14:00,13 +3/26/2017 15:00,14 +3/26/2017 16:00,14 +3/26/2017 17:00,13 +3/26/2017 18:00,12 +3/26/2017 19:00,9 +3/26/2017 20:00,7 +3/26/2017 21:00,6 +3/26/2017 22:00,4.9 +3/26/2017 23:00,4 +3/27/2017 0:00,3 +3/27/2017 1:00,3 +3/27/2017 2:00,3 +3/27/2017 3:00,3 +3/27/2017 4:00,3 +3/27/2017 5:00,3 +3/27/2017 6:00,2 +3/27/2017 7:00,4 +3/27/2017 8:00,7 +3/27/2017 9:00,8 +3/27/2017 10:00,9 +3/27/2017 11:00,11 +3/27/2017 12:00,12 +3/27/2017 13:00,13 +3/27/2017 14:00,9 +3/27/2017 15:00,10 +3/27/2017 16:00,12 +3/27/2017 17:00,11 +3/27/2017 18:00,8 +3/27/2017 19:00,6 +3/27/2017 20:00,2 +3/27/2017 21:00,6 +3/27/2017 22:00,2 +3/27/2017 23:00,-2 +3/28/2017 0:00,-3 +3/28/2017 1:00,-3 +3/28/2017 2:00,-4 +3/28/2017 3:00,-5 +3/28/2017 4:00,-4 +3/28/2017 5:00,-6 +3/28/2017 6:00,-7 +3/28/2017 7:00,-3 +3/28/2017 8:00,1 +3/28/2017 9:00,4 +3/28/2017 10:00,8 +3/28/2017 11:00,10 +3/28/2017 12:00,11 +3/28/2017 13:00,12 +3/28/2017 14:00,14 +3/28/2017 15:00,13 +3/28/2017 16:00,14 +3/28/2017 17:00,13 +3/28/2017 18:00,11 +3/28/2017 19:00,3 +3/28/2017 20:00,4 +3/28/2017 21:00,4 +3/28/2017 22:00,3 +3/28/2017 23:00,3 +3/29/2017 0:00,2 +3/29/2017 1:00,2 +3/29/2017 2:00,1 +3/29/2017 3:00,1 +3/29/2017 4:00,1 +3/29/2017 5:00,-1 +3/29/2017 6:00,-1 +3/29/2017 7:00,3 +3/29/2017 8:00,4 +3/29/2017 9:00,9 +3/29/2017 10:00,11 +3/29/2017 11:00,11 +3/29/2017 12:00,13 +3/29/2017 13:00,14 +3/29/2017 14:00,14 +3/29/2017 15:00,14 +3/29/2017 16:00,15 +3/29/2017 17:00,15 +3/29/2017 18:00,14 +3/29/2017 19:00,11.3 +3/29/2017 20:00,9.1 +3/29/2017 21:00,7.4 +3/29/2017 22:00,5.7 +3/29/2017 23:00,4.4 +3/30/2017 0:00,3 +3/30/2017 1:00,1.5 +3/30/2017 2:00,0.2 +3/30/2017 3:00,-1 +3/30/2017 4:00,-0.4 +3/30/2017 5:00,0.5 +3/30/2017 6:00,1.3 +3/30/2017 7:00,3 +3/30/2017 8:00,8 +3/30/2017 9:00,9 +3/30/2017 10:00,11 +3/30/2017 11:00,12 +3/30/2017 12:00,13.2 +3/30/2017 13:00,14 +3/30/2017 14:00,15 +3/30/2017 15:00,16 +3/30/2017 16:00,15 +3/30/2017 17:00,14 +3/30/2017 18:00,13 +3/30/2017 19:00,11 +3/30/2017 20:00,10 +3/30/2017 21:00,10 +3/30/2017 22:00,9 +3/30/2017 23:00,7 +3/31/2017 0:00,6 +3/31/2017 1:00,6 +3/31/2017 2:00,6 +3/31/2017 3:00,6 +3/31/2017 4:00,6 +3/31/2017 5:00,6 +3/31/2017 6:00,6 +3/31/2017 7:00,6 +3/31/2017 8:00,6 +3/31/2017 9:00,7 +3/31/2017 10:00,8 +3/31/2017 11:00,9 +3/31/2017 12:00,9 +3/31/2017 13:00,9 +3/31/2017 14:00,10 +3/31/2017 15:00,10 +3/31/2017 16:00,10 +3/31/2017 17:00,10 +3/31/2017 18:00,9 +3/31/2017 19:00,7 +3/31/2017 20:00,7 +3/31/2017 21:00,7 +3/31/2017 22:00,6.6 +3/31/2017 23:00,6.1 +4/1/2017 0:00,5.7 +4/1/2017 1:00,5.3 +4/1/2017 2:00,4.9 +4/1/2017 3:00,4.4 +4/1/2017 4:00,4 +4/1/2017 5:00,3.7 +4/1/2017 6:00,3.9 +4/1/2017 7:00,6 +4/1/2017 8:00,13 +4/1/2017 9:00,16 +4/1/2017 10:00,18 +4/1/2017 11:00,19.4 +4/1/2017 12:00,20.8 +4/1/2017 13:00,20.6 +4/1/2017 14:00,21 +4/1/2017 15:00,21.9 +4/1/2017 16:00,21.7 +4/1/2017 17:00,20.9 +4/1/2017 18:00,18.4 +4/1/2017 19:00,16.5 +4/1/2017 20:00,15.2 +4/1/2017 21:00,15 +4/1/2017 22:00,14.3 +4/1/2017 23:00,13.7 +4/2/2017 0:00,12.5 +4/2/2017 1:00,11.7 +4/2/2017 2:00,11 +4/2/2017 3:00,11.2 +4/2/2017 4:00,11.1 +4/2/2017 5:00,11.1 +4/2/2017 6:00,10.8 +4/2/2017 7:00,11.1 +4/2/2017 8:00,11.5 +4/2/2017 9:00,14.1 +4/2/2017 10:00,15 +4/2/2017 11:00,15.8 +4/2/2017 12:00,15.6 +4/2/2017 13:00,15.4 +4/2/2017 14:00,14.1 +4/2/2017 15:00,14.5 +4/2/2017 16:00,13.1 +4/2/2017 17:00,10.6 +4/2/2017 18:00,9.9 +4/2/2017 19:00,9.1 +4/2/2017 20:00,8.4 +4/2/2017 21:00,7.6 +4/2/2017 22:00,6.8 +4/2/2017 23:00,6.2 +4/3/2017 0:00,5.2 +4/3/2017 1:00,4.7 +4/3/2017 2:00,4.3 +4/3/2017 3:00,4.8 +4/3/2017 4:00,5 +4/3/2017 5:00,5.2 +4/3/2017 6:00,5.6 +4/3/2017 7:00,6 +4/3/2017 8:00,10 +4/3/2017 9:00,13.6 +4/3/2017 10:00,17 +4/3/2017 11:00,18 +4/3/2017 12:00,21 +4/3/2017 13:00,23 +4/3/2017 14:00,23 +4/3/2017 15:00,24 +4/3/2017 16:00,24 +4/3/2017 17:00,23 +4/3/2017 18:00,23 +4/3/2017 19:00,21 +4/3/2017 20:00,17 +4/3/2017 21:00,12 +4/3/2017 22:00,10.9 +4/3/2017 23:00,10.1 +4/4/2017 0:00,8.7 +4/4/2017 1:00,7.6 +4/4/2017 2:00,6.7 +4/4/2017 3:00,6.7 +4/4/2017 4:00,6.3 +4/4/2017 5:00,6 +4/4/2017 6:00,6 +4/4/2017 7:00,15 +4/4/2017 8:00,16 +4/4/2017 9:00,18 +4/4/2017 10:00,19 +4/4/2017 11:00,20 +4/4/2017 12:00,22 +4/4/2017 13:00,22 +4/4/2017 14:00,21 +4/4/2017 15:00,19 +4/4/2017 16:00,18 +4/4/2017 17:00,17 +4/4/2017 18:00,15 +4/4/2017 19:00,12 +4/4/2017 20:00,8 +4/4/2017 21:00,7 +4/4/2017 22:00,6 +4/4/2017 23:00,4 +4/5/2017 0:00,5 +4/5/2017 1:00,4 +4/5/2017 2:00,1 +4/5/2017 3:00,0 +4/5/2017 4:00,-1 +4/5/2017 5:00,-2 +4/5/2017 6:00,1 +4/5/2017 7:00,4 +4/5/2017 8:00,7 +4/5/2017 9:00,9 +4/5/2017 10:00,11 +4/5/2017 11:00,11 +4/5/2017 12:00,11 +4/5/2017 13:00,12 +4/5/2017 14:00,13 +4/5/2017 15:00,13 +4/5/2017 16:00,13 +4/5/2017 17:00,13 +4/5/2017 18:00,12 +4/5/2017 19:00,12 +4/5/2017 20:00,10 +4/5/2017 21:00,10 +4/5/2017 22:00,9 +4/5/2017 23:00,8 +4/6/2017 0:00,9 +4/6/2017 1:00,9 +4/6/2017 2:00,9 +4/6/2017 3:00,8 +4/6/2017 4:00,8 +4/6/2017 5:00,8 +4/6/2017 6:00,8 +4/6/2017 7:00,9 +4/6/2017 8:00,10 +4/6/2017 9:00,11 +4/6/2017 10:00,11 +4/6/2017 11:00,12 +4/6/2017 12:00,12 +4/6/2017 13:00,13 +4/6/2017 14:00,13 +4/6/2017 15:00,14 +4/6/2017 16:00,15 +4/6/2017 17:00,13 +4/6/2017 18:00,13 +4/6/2017 19:00,9 +4/6/2017 20:00,9 +4/6/2017 21:00,7 +4/6/2017 22:00,7 +4/6/2017 23:00,6 +4/7/2017 0:00,3 +4/7/2017 1:00,1 +4/7/2017 2:00,-2 +4/7/2017 3:00,-1 +4/7/2017 4:00,-2 +4/7/2017 5:00,-2 +4/7/2017 6:00,-2 +4/7/2017 7:00,1 +4/7/2017 8:00,5 +4/7/2017 9:00,9 +4/7/2017 10:00,11 +4/7/2017 11:00,12 +4/7/2017 12:00,14 +4/7/2017 13:00,16 +4/7/2017 14:00,17 +4/7/2017 15:00,17 +4/7/2017 16:00,18 +4/7/2017 17:00,16 +4/7/2017 18:00,13 +4/7/2017 19:00,8 +4/7/2017 20:00,5 +4/7/2017 21:00,4 +4/7/2017 22:00,5 +4/7/2017 23:00,3 +4/8/2017 0:00,3 +4/8/2017 1:00,1 +4/8/2017 2:00,-1 +4/8/2017 3:00,1 +4/8/2017 4:00,-1 +4/8/2017 5:00,-1 +4/8/2017 6:00,-1 +4/8/2017 7:00,3 +4/8/2017 8:00,6 +4/8/2017 9:00,11 +4/8/2017 10:00,13 +4/8/2017 11:00,16 +4/8/2017 12:00,17 +4/8/2017 13:00,19 +4/8/2017 14:00,21 +4/8/2017 15:00,21 +4/8/2017 16:00,21 +4/8/2017 17:00,20 +4/8/2017 18:00,19 +4/8/2017 19:00,16 +4/8/2017 20:00,13 +4/8/2017 21:00,11 +4/8/2017 22:00,10 +4/8/2017 23:00,9 +4/9/2017 0:00,6 +4/9/2017 1:00,4 +4/9/2017 2:00,3 +4/9/2017 3:00,2 +4/9/2017 4:00,1 +4/9/2017 5:00,0 +4/9/2017 6:00,1 +4/9/2017 7:00,7 +4/9/2017 8:00,12 +4/9/2017 9:00,15 +4/9/2017 10:00,17 +4/9/2017 11:00,19 +4/9/2017 12:00,20 +4/9/2017 13:00,22 +4/9/2017 14:00,22 +4/9/2017 15:00,23 +4/9/2017 16:00,23 +4/9/2017 17:00,23 +4/9/2017 18:00,21 +4/9/2017 19:00,18 +4/9/2017 20:00,16 +4/9/2017 21:00,14 +4/9/2017 22:00,13 +4/9/2017 23:00,12 +4/10/2017 0:00,11 +4/10/2017 1:00,10 +4/10/2017 2:00,8 +4/10/2017 3:00,8 +4/10/2017 4:00,8 +4/10/2017 5:00,8 +4/10/2017 6:00,8 +4/10/2017 7:00,11 +4/10/2017 8:00,13 +4/10/2017 9:00,14 +4/10/2017 10:00,17 +4/10/2017 11:00,18 +4/10/2017 12:00,19 +4/10/2017 13:00,21 +4/10/2017 14:00,23 +4/10/2017 15:00,23 +4/10/2017 16:00,24 +4/10/2017 17:00,23 +4/10/2017 18:00,21 +4/10/2017 19:00,15 +4/10/2017 20:00,14 +4/10/2017 21:00,9 +4/10/2017 22:00,6 +4/10/2017 23:00,6 +4/11/2017 0:00,4 +4/11/2017 1:00,3 +4/11/2017 2:00,3 +4/11/2017 3:00,3 +4/11/2017 4:00,2 +4/11/2017 5:00,3 +4/11/2017 6:00,2 +4/11/2017 7:00,7 +4/11/2017 8:00,10 +4/11/2017 9:00,13 +4/11/2017 10:00,16 +4/11/2017 11:00,18 +4/11/2017 12:00,21 +4/11/2017 13:00,22 +4/11/2017 14:00,24 +4/11/2017 15:00,24 +4/11/2017 16:00,24 +4/11/2017 17:00,24 +4/11/2017 18:00,23 +4/11/2017 19:00,19 +4/11/2017 20:00,16.7 +4/11/2017 21:00,15 +4/11/2017 22:00,13 +4/11/2017 23:00,12 +4/12/2017 0:00,11 +4/12/2017 1:00,12 +4/12/2017 2:00,12 +4/12/2017 3:00,10 +4/12/2017 4:00,7 +4/12/2017 5:00,8 +4/12/2017 6:00,6 +4/12/2017 7:00,11 +4/12/2017 8:00,17 +4/12/2017 9:00,21 +4/12/2017 10:00,23 +4/12/2017 11:00,24 +4/12/2017 12:00,26 +4/12/2017 13:00,27 +4/12/2017 14:00,27 +4/12/2017 15:00,26 +4/12/2017 16:00,28 +4/12/2017 17:00,24 +4/12/2017 18:00,22 +4/12/2017 19:00,21 +4/12/2017 20:00,19 +4/12/2017 21:00,19 +4/12/2017 22:00,18 +4/12/2017 23:00,17 +4/13/2017 0:00,19 +4/13/2017 1:00,16 +4/13/2017 2:00,15 +4/13/2017 3:00,14 +4/13/2017 4:00,15 +4/13/2017 5:00,16 +4/13/2017 6:00,16 +4/13/2017 7:00,17 +4/13/2017 8:00,18 +4/13/2017 9:00,19 +4/13/2017 10:00,19 +4/13/2017 11:00,19 +4/13/2017 12:00,20 +4/13/2017 13:00,19 +4/13/2017 14:00,16 +4/13/2017 15:00,13 +4/13/2017 16:00,13 +4/13/2017 17:00,13 +4/13/2017 18:00,12 +4/13/2017 19:00,12 +4/13/2017 20:00,12 +4/13/2017 21:00,11.3 +4/13/2017 22:00,11 +4/13/2017 23:00,11 +4/14/2017 0:00,11 +4/14/2017 1:00,11 +4/14/2017 2:00,11 +4/14/2017 3:00,11 +4/14/2017 4:00,11 +4/14/2017 5:00,10.7 +4/14/2017 6:00,11 +4/14/2017 7:00,11 +4/14/2017 8:00,12 +4/14/2017 9:00,12 +4/14/2017 10:00,13 +4/14/2017 11:00,15 +4/14/2017 12:00,16 +4/14/2017 13:00,17 +4/14/2017 14:00,18 +4/14/2017 15:00,18 +4/14/2017 16:00,18 +4/14/2017 17:00,18 +4/14/2017 18:00,17 +4/14/2017 19:00,16 +4/14/2017 20:00,14 +4/14/2017 21:00,13 +4/14/2017 22:00,13 +4/14/2017 23:00,12 +4/15/2017 0:00,12 +4/15/2017 1:00,12 +4/15/2017 2:00,11.6 +4/15/2017 3:00,12 +4/15/2017 4:00,12 +4/15/2017 5:00,12 +4/15/2017 6:00,12 +4/15/2017 7:00,11 +4/15/2017 8:00,12 +4/15/2017 9:00,12 +4/15/2017 10:00,13 +4/15/2017 11:00,14 +4/15/2017 12:00,14 +4/15/2017 13:00,16 +4/15/2017 14:00,17 +4/15/2017 15:00,18 +4/15/2017 16:00,18 +4/15/2017 17:00,18 +4/15/2017 18:00,17 +4/15/2017 19:00,14 +4/15/2017 20:00,13 +4/15/2017 21:00,12 +4/15/2017 22:00,10 +4/15/2017 23:00,9 +4/16/2017 0:00,9 +4/16/2017 1:00,8 +4/16/2017 2:00,8 +4/16/2017 3:00,8 +4/16/2017 4:00,9 +4/16/2017 5:00,8 +4/16/2017 6:00,9 +4/16/2017 7:00,10 +4/16/2017 8:00,12 +4/16/2017 9:00,13 +4/16/2017 10:00,14 +4/16/2017 11:00,16 +4/16/2017 12:00,17 +4/16/2017 13:00,18 +4/16/2017 14:00,18 +4/16/2017 15:00,18 +4/16/2017 16:00,18 +4/16/2017 17:00,18 +4/16/2017 18:00,18 +4/16/2017 19:00,16 +4/16/2017 20:00,12 +4/16/2017 21:00,9 +4/16/2017 22:00,10 +4/16/2017 23:00,11 +4/17/2017 0:00,8 +4/17/2017 1:00,8 +4/17/2017 2:00,6 +4/17/2017 3:00,6 +4/17/2017 4:00,4 +4/17/2017 5:00,6 +4/17/2017 6:00,5 +4/17/2017 7:00,8 +4/17/2017 8:00,9 +4/17/2017 9:00,12 +4/17/2017 10:00,14 +4/17/2017 11:00,17 +4/17/2017 12:00,19 +4/17/2017 13:00,18 +4/17/2017 14:00,19 +4/17/2017 15:00,20 +4/17/2017 16:00,21 +4/17/2017 17:00,19 +4/17/2017 18:00,18 +4/17/2017 19:00,14 +4/17/2017 20:00,14 +4/17/2017 21:00,14 +4/17/2017 22:00,13 +4/17/2017 23:00,13 +4/18/2017 0:00,13 +4/18/2017 1:00,12 +4/18/2017 2:00,12 +4/18/2017 3:00,10 +4/18/2017 4:00,11 +4/18/2017 5:00,9 +4/18/2017 6:00,8 +4/18/2017 7:00,14 +4/18/2017 8:00,14 +4/18/2017 9:00,15 +4/18/2017 10:00,17 +4/18/2017 11:00,19 +4/18/2017 12:00,22 +4/18/2017 13:00,23 +4/18/2017 14:00,23 +4/18/2017 15:00,24 +4/18/2017 16:00,25 +4/18/2017 17:00,24 +4/18/2017 18:00,23 +4/18/2017 19:00,21 +4/18/2017 20:00,19 +4/18/2017 21:00,14 +4/18/2017 22:00,14 +4/18/2017 23:00,14 +4/19/2017 0:00,12 +4/19/2017 1:00,11 +4/19/2017 2:00,11 +4/19/2017 3:00,10 +4/19/2017 4:00,9 +4/19/2017 5:00,9 +4/19/2017 6:00,9 +4/19/2017 7:00,11 +4/19/2017 8:00,12 +4/19/2017 9:00,14 +4/19/2017 10:00,16 +4/19/2017 11:00,18 +4/19/2017 12:00,19 +4/19/2017 13:00,20 +4/19/2017 14:00,22 +4/19/2017 15:00,22 +4/19/2017 16:00,22 +4/19/2017 17:00,22 +4/19/2017 18:00,21 +4/19/2017 19:00,18 +4/19/2017 20:00,16 +4/19/2017 21:00,15 +4/19/2017 22:00,14 +4/19/2017 23:00,12 +4/20/2017 0:00,12 +4/20/2017 1:00,10 +4/20/2017 2:00,11 +4/20/2017 3:00,8 +4/20/2017 4:00,4 +4/20/2017 5:00,5 +4/20/2017 6:00,7 +4/20/2017 7:00,11 +4/20/2017 8:00,13 +4/20/2017 9:00,16 +4/20/2017 10:00,17 +4/20/2017 11:00,18 +4/20/2017 12:00,19 +4/20/2017 13:00,21 +4/20/2017 14:00,22 +4/20/2017 15:00,22 +4/20/2017 16:00,22.2 +4/20/2017 17:00,22 +4/20/2017 18:00,22 +4/20/2017 19:00,20 +4/20/2017 20:00,17 +4/20/2017 21:00,16 +4/20/2017 22:00,12 +4/20/2017 23:00,8 +4/21/2017 0:00,7 +4/21/2017 1:00,6 +4/21/2017 2:00,5 +4/21/2017 3:00,4 +4/21/2017 4:00,4 +4/21/2017 5:00,3 +4/21/2017 6:00,5 +4/21/2017 7:00,9 +4/21/2017 8:00,12 +4/21/2017 9:00,16 +4/21/2017 10:00,18 +4/21/2017 11:00,20 +4/21/2017 12:00,23 +4/21/2017 13:00,23 +4/21/2017 14:00,24 +4/21/2017 15:00,26 +4/21/2017 16:00,26 +4/21/2017 17:00,26 +4/21/2017 18:00,25 +4/21/2017 19:00,23 +4/21/2017 20:00,21 +4/21/2017 21:00,21 +4/21/2017 22:00,19 +4/21/2017 23:00,18 +4/22/2017 0:00,17 +4/22/2017 1:00,16 +4/22/2017 2:00,16 +4/22/2017 3:00,14.8 +4/22/2017 4:00,13.4 +4/22/2017 5:00,12 +4/22/2017 6:00,12 +4/22/2017 7:00,13 +4/22/2017 8:00,14 +4/22/2017 9:00,16 +4/22/2017 10:00,17 +4/22/2017 11:00,18 +4/22/2017 12:00,19 +4/22/2017 13:00,19 +4/22/2017 14:00,19 +4/22/2017 15:00,19 +4/22/2017 16:00,19 +4/22/2017 17:00,18 +4/22/2017 18:00,17 +4/22/2017 19:00,11 +4/22/2017 20:00,12 +4/22/2017 21:00,11 +4/22/2017 22:00,6 +4/22/2017 23:00,8 +4/23/2017 0:00,7 +4/23/2017 1:00,7 +4/23/2017 2:00,7 +4/23/2017 3:00,7 +4/23/2017 4:00,6 +4/23/2017 5:00,6 +4/23/2017 6:00,7 +4/23/2017 7:00,9 +4/23/2017 8:00,11 +4/23/2017 9:00,13 +4/23/2017 10:00,14 +4/23/2017 11:00,16 +4/23/2017 12:00,17 +4/23/2017 13:00,17 +4/23/2017 14:00,17 +4/23/2017 15:00,17 +4/23/2017 16:00,18 +4/23/2017 17:00,16 +4/23/2017 18:00,15 +4/23/2017 19:00,12 +4/23/2017 20:00,11 +4/23/2017 21:00,10 +4/23/2017 22:00,9 +4/23/2017 23:00,7 +4/24/2017 0:00,5 +4/24/2017 1:00,6 +4/24/2017 2:00,4 +4/24/2017 3:00,3 +4/24/2017 4:00,3.1 +4/24/2017 5:00,3.3 +4/24/2017 6:00,4 +4/24/2017 7:00,7 +4/24/2017 8:00,9 +4/24/2017 9:00,10 +4/24/2017 10:00,11 +4/24/2017 11:00,12 +4/24/2017 12:00,13 +4/24/2017 13:00,14 +4/24/2017 14:00,15 +4/24/2017 15:00,15 +4/24/2017 16:00,16 +4/24/2017 17:00,15 +4/24/2017 18:00,12 +4/24/2017 19:00,12 +4/24/2017 20:00,9 +4/24/2017 21:00,8 +4/24/2017 22:00,9 +4/24/2017 23:00,10 +4/25/2017 0:00,9 +4/25/2017 1:00,9 +4/25/2017 2:00,9 +4/25/2017 3:00,10 +4/25/2017 4:00,8 +4/25/2017 5:00,10 +4/25/2017 6:00,10 +4/25/2017 7:00,9 +4/25/2017 8:00,11 +4/25/2017 9:00,12 +4/25/2017 10:00,13 +4/25/2017 11:00,14 +4/25/2017 12:00,16 +4/25/2017 13:00,17 +4/25/2017 14:00,17 +4/25/2017 15:00,18 +4/25/2017 16:00,18 +4/25/2017 17:00,17 +4/25/2017 18:00,16 +4/25/2017 19:00,14 +4/25/2017 20:00,12 +4/25/2017 21:00,10 +4/25/2017 22:00,9 +4/25/2017 23:00,7 +4/26/2017 0:00,7 +4/26/2017 1:00,6 +4/26/2017 2:00,4 +4/26/2017 3:00,3 +4/26/2017 4:00,1 +4/26/2017 5:00,1 +4/26/2017 6:00,1 +4/26/2017 7:00,7 +4/26/2017 8:00,10 +4/26/2017 9:00,12 +4/26/2017 10:00,14 +4/26/2017 11:00,17 +4/26/2017 12:00,18 +4/26/2017 13:00,19 +4/26/2017 14:00,21 +4/26/2017 15:00,21 +4/26/2017 16:00,21 +4/26/2017 17:00,21 +4/26/2017 18:00,20 +4/26/2017 19:00,13 +4/26/2017 20:00,11 +4/26/2017 21:00,9 +4/26/2017 22:00,10 +4/26/2017 23:00,9 +4/27/2017 0:00,8 +4/27/2017 1:00,9 +4/27/2017 2:00,8 +4/27/2017 3:00,7 +4/27/2017 4:00,7 +4/27/2017 5:00,7 +4/27/2017 6:00,9 +4/27/2017 7:00,12 +4/27/2017 8:00,14 +4/27/2017 9:00,16 +4/27/2017 10:00,17 +4/27/2017 11:00,18 +4/27/2017 12:00,21 +4/27/2017 13:00,22 +4/27/2017 14:00,22 +4/27/2017 15:00,23 +4/27/2017 16:00,21.7 +4/27/2017 17:00,20 +4/27/2017 18:00,21 +4/27/2017 19:00,18 +4/27/2017 20:00,16 +4/27/2017 21:00,14 +4/27/2017 22:00,13 +4/27/2017 23:00,13 +4/28/2017 0:00,11 +4/28/2017 1:00,11 +4/28/2017 2:00,10 +4/28/2017 3:00,9 +4/28/2017 4:00,9 +4/28/2017 5:00,9 +4/28/2017 6:00,8 +4/28/2017 7:00,10 +4/28/2017 8:00,12 +4/28/2017 9:00,13 +4/28/2017 10:00,14 +4/28/2017 11:00,15 +4/28/2017 12:00,16 +4/28/2017 13:00,17 +4/28/2017 14:00,17 +4/28/2017 15:00,18 +4/28/2017 16:00,18 +4/28/2017 17:00,17 +4/28/2017 18:00,16 +4/28/2017 19:00,14 +4/28/2017 20:00,12 +4/28/2017 21:00,9 +4/28/2017 22:00,8 +4/28/2017 23:00,8 +4/29/2017 0:00,7 +4/29/2017 1:00,5 +4/29/2017 2:00,4 +4/29/2017 3:00,4 +4/29/2017 4:00,4 +4/29/2017 5:00,4.2 +4/29/2017 6:00,5 +4/29/2017 7:00,8 +4/29/2017 8:00,9 +4/29/2017 9:00,12 +4/29/2017 10:00,13 +4/29/2017 11:00,14 +4/29/2017 12:00,16 +4/29/2017 13:00,17 +4/29/2017 14:00,17 +4/29/2017 15:00,17 +4/29/2017 16:00,18 +4/29/2017 17:00,17 +4/29/2017 18:00,17 +4/29/2017 19:00,15 +4/29/2017 20:00,13 +4/29/2017 21:00,8 +4/29/2017 22:00,8 +4/29/2017 23:00,9 +4/30/2017 0:00,7 +4/30/2017 1:00,8 +4/30/2017 2:00,8 +4/30/2017 3:00,7 +4/30/2017 4:00,7 +4/30/2017 5:00,4 +4/30/2017 6:00,6 +4/30/2017 7:00,10 +4/30/2017 8:00,12 +4/30/2017 9:00,14 +4/30/2017 10:00,17 +4/30/2017 11:00,19 +4/30/2017 12:00,21 +4/30/2017 13:00,22 +4/30/2017 14:00,24 +4/30/2017 15:00,25 +4/30/2017 16:00,25 +4/30/2017 17:00,23 +4/30/2017 18:00,22 +4/30/2017 19:00,18 +4/30/2017 20:00,14 +4/30/2017 21:00,14 +4/30/2017 22:00,13.2 +4/30/2017 23:00,12.4 +5/1/2017 0:00,11.6 +5/1/2017 1:00,10.7 +5/1/2017 2:00,9.9 +5/1/2017 3:00,9.1 +5/1/2017 4:00,8.3 +5/1/2017 5:00,8.3 +5/1/2017 6:00,8.3 +5/1/2017 7:00,9.4 +5/1/2017 8:00,10.9 +5/1/2017 9:00,12.2 +5/1/2017 10:00,14.4 +5/1/2017 11:00,13.8 +5/1/2017 12:00,15.3 +5/1/2017 13:00,16.6 +5/1/2017 14:00,16.1 +5/1/2017 15:00,15.5 +5/1/2017 16:00,16.1 +5/1/2017 17:00,15.5 +5/1/2017 18:00,15.5 +5/1/2017 19:00,15 +5/1/2017 20:00,14.4 +5/1/2017 21:00,14.4 +5/1/2017 22:00,11.1 +5/1/2017 23:00,11.1 +5/2/2017 0:00,10 +5/2/2017 1:00,9.8 +5/2/2017 2:00,9.5 +5/2/2017 3:00,9.4 +5/2/2017 4:00,10.5 +5/2/2017 5:00,11.1 +5/2/2017 6:00,11.6 +5/2/2017 7:00,12.2 +5/2/2017 8:00,12.7 +5/2/2017 9:00,14.4 +5/2/2017 10:00,16.6 +5/2/2017 11:00,17.2 +5/2/2017 12:00,18.8 +5/2/2017 13:00,19.4 +5/2/2017 14:00,19.4 +5/2/2017 15:00,19.4 +5/2/2017 16:00,18.8 +5/2/2017 17:00,18.3 +5/2/2017 18:00,17.2 +5/2/2017 19:00,16.1 +5/2/2017 20:00,15 +5/2/2017 21:00,12.2 +5/2/2017 22:00,12.2 +5/2/2017 23:00,10 +5/3/2017 0:00,8.5 +5/3/2017 1:00,7.8 +5/3/2017 2:00,7.2 +5/3/2017 3:00,6.6 +5/3/2017 4:00,6.6 +5/3/2017 5:00,7.7 +5/3/2017 6:00,7.2 +5/3/2017 7:00,10 +5/3/2017 8:00,12.2 +5/3/2017 9:00,13.8 +5/3/2017 10:00,15.5 +5/3/2017 11:00,16.7 +5/3/2017 12:00,17.7 +5/3/2017 13:00,18.3 +5/3/2017 14:00,18.8 +5/3/2017 15:00,18.8 +5/3/2017 16:00,19.4 +5/3/2017 17:00,19.4 +5/3/2017 18:00,18.9 +5/3/2017 19:00,17.7 +5/3/2017 20:00,15.2 +5/3/2017 21:00,13.3 +5/3/2017 22:00,11.6 +5/3/2017 23:00,9.4 +5/4/2017 0:00,7.5 +5/4/2017 1:00,6.4 +5/4/2017 2:00,5.4 +5/4/2017 3:00,4.4 +5/4/2017 4:00,4.4 +5/4/2017 5:00,5.5 +5/4/2017 6:00,6.6 +5/4/2017 7:00,8.8 +5/4/2017 8:00,11.1 +5/4/2017 9:00,12.7 +5/4/2017 10:00,14.4 +5/4/2017 11:00,16.1 +5/4/2017 12:00,16.6 +5/4/2017 13:00,17.2 +5/4/2017 14:00,17.7 +5/4/2017 15:00,16.6 +5/4/2017 16:00,16 +5/4/2017 17:00,15 +5/4/2017 18:00,15 +5/4/2017 19:00,13.8 +5/4/2017 20:00,12.7 +5/4/2017 21:00,12.7 +5/4/2017 22:00,12.2 +5/4/2017 23:00,12.2 +5/5/2017 0:00,11.1 +5/5/2017 1:00,10.9 +5/5/2017 2:00,10.6 +5/5/2017 3:00,10.5 +5/5/2017 4:00,10.5 +5/5/2017 5:00,10.5 +5/5/2017 6:00,11.1 +5/5/2017 7:00,11.8 +5/5/2017 8:00,12.3 +5/5/2017 9:00,12.7 +5/5/2017 10:00,14.2 +5/5/2017 11:00,15.5 +5/5/2017 12:00,16.1 +5/5/2017 13:00,15 +5/5/2017 14:00,15.5 +5/5/2017 15:00,13.8 +5/5/2017 16:00,13.8 +5/5/2017 17:00,14.4 +5/5/2017 18:00,13.8 +5/5/2017 19:00,13.3 +5/5/2017 20:00,13.8 +5/5/2017 21:00,12.7 +5/5/2017 22:00,12 +5/5/2017 23:00,12 +5/6/2017 0:00,11.2 +5/6/2017 1:00,11.3 +5/6/2017 2:00,11.4 +5/6/2017 3:00,11.6 +5/6/2017 4:00,11 +5/6/2017 5:00,10.3 +5/6/2017 6:00,11.1 +5/6/2017 7:00,12.2 +5/6/2017 8:00,16.1 +5/6/2017 9:00,18.3 +5/6/2017 10:00,19.4 +5/6/2017 11:00,22.2 +5/6/2017 12:00,23.4 +5/6/2017 13:00,24.4 +5/6/2017 14:00,26.1 +5/6/2017 15:00,24.4 +5/6/2017 16:00,26.1 +5/6/2017 17:00,23.8 +5/6/2017 18:00,21.1 +5/6/2017 19:00,19.4 +5/6/2017 20:00,18.8 +5/6/2017 21:00,17.7 +5/6/2017 22:00,15.5 +5/6/2017 23:00,16.1 +5/7/2017 0:00,14.3 +5/7/2017 1:00,13.4 +5/7/2017 2:00,12.4 +5/7/2017 3:00,11.6 +5/7/2017 4:00,11.4 +5/7/2017 5:00,11.1 +5/7/2017 6:00,12.2 +5/7/2017 7:00,12.7 +5/7/2017 8:00,13.8 +5/7/2017 9:00,15 +5/7/2017 10:00,15.5 +5/7/2017 11:00,17.2 +5/7/2017 12:00,17.7 +5/7/2017 13:00,19.2 +5/7/2017 14:00,20.5 +5/7/2017 15:00,16.6 +5/7/2017 16:00,17.7 +5/7/2017 17:00,17.7 +5/7/2017 18:00,16.1 +5/7/2017 19:00,15.5 +5/7/2017 20:00,15 +5/7/2017 21:00,13.3 +5/7/2017 22:00,12.4 +5/7/2017 23:00,12.1 +5/8/2017 0:00,11.2 +5/8/2017 1:00,11 +5/8/2017 2:00,10.9 +5/8/2017 3:00,10.9 +5/8/2017 4:00,11.6 +5/8/2017 5:00,11.1 +5/8/2017 6:00,12.2 +5/8/2017 7:00,13.3 +5/8/2017 8:00,14.4 +5/8/2017 9:00,16.6 +5/8/2017 10:00,16.6 +5/8/2017 11:00,17.2 +5/8/2017 12:00,17.2 +5/8/2017 13:00,20 +5/8/2017 14:00,20 +5/8/2017 15:00,19.4 +5/8/2017 16:00,20.5 +5/8/2017 17:00,21.1 +5/8/2017 18:00,21.1 +5/8/2017 19:00,18.3 +5/8/2017 20:00,17.2 +5/8/2017 21:00,15.5 +5/8/2017 22:00,14.4 +5/8/2017 23:00,12.7 +5/9/2017 0:00,10.8 +5/9/2017 1:00,9.7 +5/9/2017 2:00,8.7 +5/9/2017 3:00,7.7 +5/9/2017 4:00,7.7 +5/9/2017 5:00,7.5 +5/9/2017 6:00,8.8 +5/9/2017 7:00,12.7 +5/9/2017 8:00,15.5 +5/9/2017 9:00,17.7 +5/9/2017 10:00,19.4 +5/9/2017 11:00,20.5 +5/9/2017 12:00,22.2 +5/9/2017 13:00,23.8 +5/9/2017 14:00,25 +5/9/2017 15:00,25 +5/9/2017 16:00,21 +5/9/2017 17:00,16.6 +5/9/2017 18:00,15 +5/9/2017 19:00,15.5 +5/9/2017 20:00,15 +5/9/2017 21:00,15 +5/9/2017 22:00,14.4 +5/9/2017 23:00,14.4 +5/10/2017 0:00,13 +5/10/2017 1:00,12.3 +5/10/2017 2:00,11.8 +5/10/2017 3:00,11.3 +5/10/2017 4:00,11.6 +5/10/2017 5:00,11.5 +5/10/2017 6:00,12.2 +5/10/2017 7:00,12.7 +5/10/2017 8:00,13.3 +5/10/2017 9:00,15.4 +5/10/2017 10:00,17.2 +5/10/2017 11:00,18.8 +5/10/2017 12:00,18.3 +5/10/2017 13:00,18.8 +5/10/2017 14:00,18.3 +5/10/2017 15:00,17.7 +5/10/2017 16:00,19.4 +5/10/2017 17:00,18.8 +5/10/2017 18:00,18.3 +5/10/2017 19:00,17.6 +5/10/2017 20:00,16.1 +5/10/2017 21:00,13.3 +5/10/2017 22:00,11.9 +5/10/2017 23:00,11.1 +5/11/2017 0:00,10.1 +5/11/2017 1:00,9.9 +5/11/2017 2:00,9.8 +5/11/2017 3:00,9.8 +5/11/2017 4:00,10.5 +5/11/2017 5:00,10.5 +5/11/2017 6:00,10.5 +5/11/2017 7:00,11.1 +5/11/2017 8:00,11.6 +5/11/2017 9:00,12.2 +5/11/2017 10:00,13.3 +5/11/2017 11:00,15.1 +5/11/2017 12:00,16.6 +5/11/2017 13:00,17.3 +5/11/2017 14:00,17.7 +5/11/2017 15:00,15.8 +5/11/2017 16:00,13.7 +5/11/2017 17:00,11.1 +5/11/2017 18:00,13.3 +5/11/2017 19:00,12.7 +5/11/2017 20:00,11.6 +5/11/2017 21:00,10.5 +5/11/2017 22:00,10 +5/11/2017 23:00,9.4 +5/12/2017 0:00,7.8 +5/12/2017 1:00,7 +5/12/2017 2:00,6.2 +5/12/2017 3:00,5.5 +5/12/2017 4:00,5.6 +5/12/2017 5:00,5.6 +5/12/2017 6:00,7.2 +5/12/2017 7:00,8.8 +5/12/2017 8:00,10.5 +5/12/2017 9:00,12.2 +5/12/2017 10:00,13.6 +5/12/2017 11:00,14.6 +5/12/2017 12:00,15.5 +5/12/2017 13:00,16.2 +5/12/2017 14:00,16.6 +5/12/2017 15:00,17.5 +5/12/2017 16:00,18.3 +5/12/2017 17:00,17.2 +5/12/2017 18:00,17.2 +5/12/2017 19:00,16.6 +5/12/2017 20:00,14.4 +5/12/2017 21:00,13.8 +5/12/2017 22:00,10.5 +5/12/2017 23:00,11.6 +5/13/2017 0:00,10.7 +5/13/2017 1:00,10.5 +5/13/2017 2:00,10.4 +5/13/2017 3:00,10.4 +5/13/2017 4:00,11.1 +5/13/2017 5:00,10.3 +5/13/2017 6:00,11.1 +5/13/2017 7:00,12.7 +5/13/2017 8:00,14.4 +5/13/2017 9:00,17.2 +5/13/2017 10:00,18.8 +5/13/2017 11:00,20 +5/13/2017 12:00,21.2 +5/13/2017 13:00,22.2 +5/13/2017 14:00,22.7 +5/13/2017 15:00,23.3 +5/13/2017 16:00,23.3 +5/13/2017 17:00,23.3 +5/13/2017 18:00,22.2 +5/13/2017 19:00,20.5 +5/13/2017 20:00,15 +5/13/2017 21:00,12.2 +5/13/2017 22:00,12.2 +5/13/2017 23:00,9.4 +5/14/2017 0:00,7.5 +5/14/2017 1:00,6.4 +5/14/2017 2:00,5.4 +5/14/2017 3:00,4.4 +5/14/2017 4:00,5.1 +5/14/2017 5:00,5.6 +5/14/2017 6:00,7.7 +5/14/2017 7:00,11.1 +5/14/2017 8:00,14.4 +5/14/2017 9:00,17.2 +5/14/2017 10:00,19.4 +5/14/2017 11:00,21.1 +5/14/2017 12:00,22.2 +5/14/2017 13:00,25 +5/14/2017 14:00,26.6 +5/14/2017 15:00,26.6 +5/14/2017 16:00,26.6 +5/14/2017 17:00,26.6 +5/14/2017 18:00,25 +5/14/2017 19:00,22.7 +5/14/2017 20:00,21.1 +5/14/2017 21:00,17.7 +5/14/2017 22:00,13.8 +5/14/2017 23:00,13.8 +5/15/2017 0:00,12.6 +5/15/2017 1:00,12.2 +5/15/2017 2:00,11.9 +5/15/2017 3:00,11.6 +5/15/2017 4:00,13.1 +5/15/2017 5:00,14.4 +5/15/2017 6:00,13.8 +5/15/2017 7:00,18.3 +5/15/2017 8:00,19.4 +5/15/2017 9:00,22.2 +5/15/2017 10:00,24.4 +5/15/2017 11:00,25 +5/15/2017 12:00,26.6 +5/15/2017 13:00,26.6 +5/15/2017 14:00,28.3 +5/15/2017 15:00,28.3 +5/15/2017 16:00,28.8 +5/15/2017 17:00,28.3 +5/15/2017 18:00,26.6 +5/15/2017 19:00,25.5 +5/15/2017 20:00,23.3 +5/15/2017 21:00,22.2 +5/15/2017 22:00,18.3 +5/15/2017 23:00,15 +5/16/2017 0:00,13.9 +5/16/2017 1:00,13.7 +5/16/2017 2:00,13.4 +5/16/2017 3:00,13.3 +5/16/2017 4:00,15.5 +5/16/2017 5:00,14.4 +5/16/2017 6:00,15.5 +5/16/2017 7:00,16.6 +5/16/2017 8:00,18.3 +5/16/2017 9:00,20 +5/16/2017 10:00,21.1 +5/16/2017 11:00,22.7 +5/16/2017 12:00,23.9 +5/16/2017 13:00,25 +5/16/2017 14:00,25.5 +5/16/2017 15:00,26.1 +5/16/2017 16:00,25.5 +5/16/2017 17:00,25 +5/16/2017 18:00,22.7 +5/16/2017 19:00,20.5 +5/16/2017 20:00,18.6 +5/16/2017 21:00,17.2 +5/16/2017 22:00,16.1 +5/16/2017 23:00,15.5 +5/17/2017 0:00,14.3 +5/17/2017 1:00,13.9 +5/17/2017 2:00,13.6 +5/17/2017 3:00,13.3 +5/17/2017 4:00,12.7 +5/17/2017 5:00,12.7 +5/17/2017 6:00,13.3 +5/17/2017 7:00,14.4 +5/17/2017 8:00,16.1 +5/17/2017 9:00,17.9 +5/17/2017 10:00,19.4 +5/17/2017 11:00,20.6 +5/17/2017 12:00,21.6 +5/17/2017 13:00,22.7 +5/17/2017 14:00,22.7 +5/17/2017 15:00,23.3 +5/17/2017 16:00,22.2 +5/17/2017 17:00,21.4 +5/17/2017 18:00,20 +5/17/2017 19:00,17.7 +5/17/2017 20:00,15.5 +5/17/2017 21:00,14.4 +5/17/2017 22:00,13.8 +5/17/2017 23:00,11.1 +5/18/2017 0:00,9.6 +5/18/2017 1:00,8.8 +5/18/2017 2:00,8.2 +5/18/2017 3:00,7.5 +5/18/2017 4:00,7.7 +5/18/2017 5:00,7.7 +5/18/2017 6:00,9.4 +5/18/2017 7:00,10.5 +5/18/2017 8:00,12.7 +5/18/2017 9:00,15 +5/18/2017 10:00,15.5 +5/18/2017 11:00,17.2 +5/18/2017 12:00,18.3 +5/18/2017 13:00,20 +5/18/2017 14:00,20.8 +5/18/2017 15:00,21.1 +5/18/2017 16:00,21.6 +5/18/2017 17:00,21.6 +5/18/2017 18:00,20.8 +5/18/2017 19:00,19.4 +5/18/2017 20:00,16.6 +5/18/2017 21:00,15.5 +5/18/2017 22:00,12.7 +5/18/2017 23:00,12.7 +5/19/2017 0:00,10.3 +5/19/2017 1:00,8.6 +5/19/2017 2:00,7 +5/19/2017 3:00,5.5 +5/19/2017 4:00,5.3 +5/19/2017 5:00,5 +5/19/2017 6:00,6.6 +5/19/2017 7:00,11.1 +5/19/2017 8:00,13.8 +5/19/2017 9:00,16.1 +5/19/2017 10:00,18.8 +5/19/2017 11:00,20.1 +5/19/2017 12:00,21.1 +5/19/2017 13:00,22.7 +5/19/2017 14:00,23.3 +5/19/2017 15:00,25 +5/19/2017 16:00,25 +5/19/2017 17:00,23.8 +5/19/2017 18:00,23.3 +5/19/2017 19:00,22.2 +5/19/2017 20:00,17.2 +5/19/2017 21:00,14.2 +5/19/2017 22:00,11.6 +5/19/2017 23:00,12.2 +5/20/2017 0:00,9.9 +5/20/2017 1:00,8.4 +5/20/2017 2:00,6.9 +5/20/2017 3:00,5.5 +5/20/2017 4:00,5 +5/20/2017 5:00,7.2 +5/20/2017 6:00,9.4 +5/20/2017 7:00,12.8 +5/20/2017 8:00,16.1 +5/20/2017 9:00,18.8 +5/20/2017 10:00,21.6 +5/20/2017 11:00,23.4 +5/20/2017 12:00,25 +5/20/2017 13:00,26.1 +5/20/2017 14:00,27.2 +5/20/2017 15:00,27.7 +5/20/2017 16:00,27.9 +5/20/2017 17:00,27.7 +5/20/2017 18:00,27.2 +5/20/2017 19:00,26.6 +5/20/2017 20:00,22.7 +5/20/2017 21:00,20.5 +5/20/2017 22:00,18.8 +5/20/2017 23:00,17.2 +5/21/2017 0:00,15.1 +5/21/2017 1:00,13.8 +5/21/2017 2:00,12.6 +5/21/2017 3:00,11.5 +5/21/2017 4:00,11.1 +5/21/2017 5:00,10.5 +5/21/2017 6:00,11.6 +5/21/2017 7:00,15.5 +5/21/2017 8:00,17.7 +5/21/2017 9:00,17.7 +5/21/2017 10:00,20 +5/21/2017 11:00,21.1 +5/21/2017 12:00,22.2 +5/21/2017 13:00,23.3 +5/21/2017 14:00,23.8 +5/21/2017 15:00,24.4 +5/21/2017 16:00,23.8 +5/21/2017 17:00,23.8 +5/21/2017 18:00,23.8 +5/21/2017 19:00,22.7 +5/21/2017 20:00,18.8 +5/21/2017 21:00,14.4 +5/21/2017 22:00,13.6 +5/21/2017 23:00,13.3 +5/22/2017 0:00,11.4 +5/22/2017 1:00,10.3 +5/22/2017 2:00,9.3 +5/22/2017 3:00,8.3 +5/22/2017 4:00,8.3 +5/22/2017 5:00,8.8 +5/22/2017 6:00,12.2 +5/22/2017 7:00,14.4 +5/22/2017 8:00,16.6 +5/22/2017 9:00,19.4 +5/22/2017 10:00,22.2 +5/22/2017 11:00,24.4 +5/22/2017 12:00,26.1 +5/22/2017 13:00,26.6 +5/22/2017 14:00,26.6 +5/22/2017 15:00,26.6 +5/22/2017 16:00,27.2 +5/22/2017 17:00,26.6 +5/22/2017 18:00,26.1 +5/22/2017 19:00,23.3 +5/22/2017 20:00,18.8 +5/22/2017 21:00,15.5 +5/22/2017 22:00,12.7 +5/22/2017 23:00,11.6 +5/23/2017 0:00,9.9 +5/23/2017 1:00,8.9 +5/23/2017 2:00,8 +5/23/2017 3:00,7.2 +5/23/2017 4:00,8.1 +5/23/2017 5:00,8.8 +5/23/2017 6:00,10 +5/23/2017 7:00,12.7 +5/23/2017 8:00,15 +5/23/2017 9:00,17.7 +5/23/2017 10:00,20.5 +5/23/2017 11:00,22.7 +5/23/2017 12:00,23.8 +5/23/2017 13:00,27.2 +5/23/2017 14:00,27.7 +5/23/2017 15:00,27.7 +5/23/2017 16:00,28.3 +5/23/2017 17:00,27.7 +5/23/2017 18:00,27.2 +5/23/2017 19:00,25.5 +5/23/2017 20:00,20 +5/23/2017 21:00,16.6 +5/23/2017 22:00,16.1 +5/23/2017 23:00,15.5 +5/24/2017 0:00,13.3 +5/24/2017 1:00,11.9 +5/24/2017 2:00,10.6 +5/24/2017 3:00,9.3 +5/24/2017 4:00,8.8 +5/24/2017 5:00,10 +5/24/2017 6:00,11.6 +5/24/2017 7:00,15 +5/24/2017 8:00,18.3 +5/24/2017 9:00,20.5 +5/24/2017 10:00,22.7 +5/24/2017 11:00,25 +5/24/2017 12:00,26.7 +5/24/2017 13:00,28.3 +5/24/2017 14:00,29.4 +5/24/2017 15:00,29.4 +5/24/2017 16:00,29.4 +5/24/2017 17:00,29.4 +5/24/2017 18:00,28.3 +5/24/2017 19:00,25.1 +5/24/2017 20:00,21.1 +5/24/2017 21:00,20 +5/24/2017 22:00,13.8 +5/24/2017 23:00,16.6 +5/25/2017 0:00,13.9 +5/25/2017 1:00,12 +5/25/2017 2:00,10.1 +5/25/2017 3:00,8.3 +5/25/2017 4:00,8.8 +5/25/2017 5:00,9.2 +5/25/2017 6:00,11.1 +5/25/2017 7:00,13.8 +5/25/2017 8:00,15.5 +5/25/2017 9:00,21.6 +5/25/2017 10:00,22.2 +5/25/2017 11:00,23.8 +5/25/2017 12:00,25 +5/25/2017 13:00,26.6 +5/25/2017 14:00,25.5 +5/25/2017 15:00,27.7 +5/25/2017 16:00,21.1 +5/25/2017 17:00,22.7 +5/25/2017 18:00,21.1 +5/25/2017 19:00,20 +5/25/2017 20:00,18.8 +5/25/2017 21:00,15.5 +5/25/2017 22:00,15.5 +5/25/2017 23:00,13.3 +5/26/2017 0:00,11.1 +5/26/2017 1:00,9.8 +5/26/2017 2:00,8.4 +5/26/2017 3:00,7.2 +5/26/2017 4:00,5.5 +5/26/2017 5:00,7 +5/26/2017 6:00,10 +5/26/2017 7:00,12.7 +5/26/2017 8:00,14.4 +5/26/2017 9:00,15.5 +5/26/2017 10:00,17.7 +5/26/2017 11:00,19.4 +5/26/2017 12:00,21.1 +5/26/2017 13:00,22.2 +5/26/2017 14:00,22.7 +5/26/2017 15:00,23.8 +5/26/2017 16:00,24.4 +5/26/2017 17:00,23.8 +5/26/2017 18:00,23.3 +5/26/2017 19:00,22.2 +5/26/2017 20:00,20.5 +5/26/2017 21:00,16.6 +5/26/2017 22:00,15.5 +5/26/2017 23:00,13.3 +5/27/2017 0:00,11.4 +5/27/2017 1:00,10.3 +5/27/2017 2:00,9.3 +5/27/2017 3:00,8.3 +5/27/2017 4:00,8.3 +5/27/2017 5:00,8.6 +5/27/2017 6:00,10.5 +5/27/2017 7:00,12.7 +5/27/2017 8:00,16.1 +5/27/2017 9:00,20 +5/27/2017 10:00,22.2 +5/27/2017 11:00,25 +5/27/2017 12:00,25 +5/27/2017 13:00,25.5 +5/27/2017 14:00,26.6 +5/27/2017 15:00,27.7 +5/27/2017 16:00,27.7 +5/27/2017 17:00,27.2 +5/27/2017 18:00,27.2 +5/27/2017 19:00,25.9 +5/27/2017 20:00,23.8 +5/27/2017 21:00,21.1 +5/27/2017 22:00,17.5 +5/27/2017 23:00,14.4 +5/28/2017 0:00,12.8 +5/28/2017 1:00,12 +5/28/2017 2:00,11.3 +5/28/2017 3:00,10.5 +5/28/2017 4:00,10.7 +5/28/2017 5:00,11.1 +5/28/2017 6:00,13.3 +5/28/2017 7:00,16.1 +5/28/2017 8:00,19.4 +5/28/2017 9:00,22 +5/28/2017 10:00,24.4 +5/28/2017 11:00,26.8 +5/28/2017 12:00,29 +5/28/2017 13:00,31.1 +5/28/2017 14:00,30.5 +5/28/2017 15:00,30.6 +5/28/2017 16:00,30.5 +5/28/2017 17:00,31.1 +5/28/2017 18:00,30.5 +5/28/2017 19:00,29.4 +5/28/2017 20:00,25 +5/28/2017 21:00,21.1 +5/28/2017 22:00,18.6 +5/28/2017 23:00,16.6 +5/29/2017 0:00,14.9 +5/29/2017 1:00,13.9 +5/29/2017 2:00,13.1 +5/29/2017 3:00,12.2 +5/29/2017 4:00,12.2 +5/29/2017 5:00,12.2 +5/29/2017 6:00,14.4 +5/29/2017 7:00,17.2 +5/29/2017 8:00,20 +5/29/2017 9:00,24.4 +5/29/2017 10:00,27.2 +5/29/2017 11:00,28.8 +5/29/2017 12:00,31.1 +5/29/2017 13:00,32.2 +5/29/2017 14:00,33.3 +5/29/2017 15:00,33.8 +5/29/2017 16:00,32.7 +5/29/2017 17:00,32.7 +5/29/2017 18:00,32.2 +5/29/2017 19:00,30.5 +5/29/2017 20:00,28.3 +5/29/2017 21:00,25 +5/29/2017 22:00,19.4 +5/29/2017 23:00,16.6 +5/30/2017 0:00,15.3 +5/30/2017 1:00,14.7 +5/30/2017 2:00,14.2 +5/30/2017 3:00,13.8 +5/30/2017 4:00,13.9 +5/30/2017 5:00,13.8 +5/30/2017 6:00,17.2 +5/30/2017 7:00,22.7 +5/30/2017 8:00,25.5 +5/30/2017 9:00,27.2 +5/30/2017 10:00,28.3 +5/30/2017 11:00,30 +5/30/2017 12:00,31.6 +5/30/2017 13:00,31.6 +5/30/2017 14:00,32.7 +5/30/2017 15:00,33.8 +5/30/2017 16:00,33.3 +5/30/2017 17:00,31.6 +5/30/2017 18:00,31.1 +5/30/2017 19:00,30 +5/30/2017 20:00,25.5 +5/30/2017 21:00,23.3 +5/30/2017 22:00,23.8 +5/30/2017 23:00,22.2 +5/31/2017 0:00,19.3 +5/31/2017 1:00,17.3 +5/31/2017 2:00,15.2 +5/31/2017 3:00,13.3 +5/31/2017 4:00,13.6 +5/31/2017 5:00,14.4 +5/31/2017 6:00,16.1 +5/31/2017 7:00,20 +5/31/2017 8:00,22.2 +5/31/2017 9:00,23.3 +5/31/2017 10:00,26.1 +5/31/2017 11:00,27.2 +5/31/2017 12:00,27.7 +5/31/2017 13:00,29.4 +5/31/2017 14:00,30.5 +5/31/2017 15:00,31.1 +5/31/2017 16:00,31.6 +5/31/2017 17:00,31.1 +5/31/2017 18:00,30.5 +5/31/2017 19:00,29.4 +5/31/2017 20:00,26.6 +5/31/2017 21:00,21.6 +5/31/2017 22:00,20.8 +5/31/2017 23:00,20 +6/1/2017 0:00,19.2 +6/1/2017 1:00,18.4 +6/1/2017 2:00,17.6 +6/1/2017 3:00,16.8 +6/1/2017 4:00,16 +6/1/2017 5:00,14 +6/1/2017 6:00,14 +6/1/2017 7:00,17 +6/1/2017 8:00,18 +6/1/2017 9:00,19 +6/1/2017 10:00,20 +6/1/2017 11:00,21 +6/1/2017 12:00,22 +6/1/2017 13:00,22 +6/1/2017 14:00,24 +6/1/2017 15:00,25 +6/1/2017 16:00,25 +6/1/2017 17:00,25 +6/1/2017 18:00,25 +6/1/2017 19:00,23 +6/1/2017 20:00,21 +6/1/2017 21:00,15 +6/1/2017 22:00,13 +6/1/2017 23:00,12 +6/2/2017 0:00,13 +6/2/2017 1:00,14 +6/2/2017 2:00,14 +6/2/2017 3:00,12 +6/2/2017 4:00,9 +6/2/2017 5:00,12 +6/2/2017 6:00,16 +6/2/2017 7:00,17 +6/2/2017 8:00,19 +6/2/2017 9:00,21 +6/2/2017 10:00,22 +6/2/2017 11:00,24 +6/2/2017 12:00,26 +6/2/2017 13:00,26 +6/2/2017 14:00,27 +6/2/2017 15:00,27 +6/2/2017 16:00,27 +6/2/2017 17:00,27 +6/2/2017 18:00,27 +6/2/2017 19:00,26 +6/2/2017 20:00,23 +6/2/2017 21:00,18 +6/2/2017 22:00,18 +6/2/2017 23:00,16 +6/3/2017 0:00,16 +6/3/2017 1:00,16 +6/3/2017 2:00,13 +6/3/2017 3:00,11 +6/3/2017 4:00,9 +6/3/2017 5:00,11 +6/3/2017 6:00,16 +6/3/2017 7:00,18 +6/3/2017 8:00,20 +6/3/2017 9:00,22 +6/3/2017 10:00,24 +6/3/2017 11:00,25 +6/3/2017 12:00,27 +6/3/2017 13:00,27 +6/3/2017 14:00,28 +6/3/2017 15:00,28 +6/3/2017 16:00,28 +6/3/2017 17:00,28 +6/3/2017 18:00,28 +6/3/2017 19:00,27 +6/3/2017 20:00,25 +6/3/2017 21:00,23 +6/3/2017 22:00,21 +6/3/2017 23:00,19 +6/4/2017 0:00,19 +6/4/2017 1:00,17 +6/4/2017 2:00,17 +6/4/2017 3:00,15 +6/4/2017 4:00,17 +6/4/2017 5:00,17 +6/4/2017 6:00,19 +6/4/2017 7:00,21 +6/4/2017 8:00,23 +6/4/2017 9:00,23 +6/4/2017 10:00,24 +6/4/2017 11:00,25 +6/4/2017 12:00,27 +6/4/2017 13:00,27 +6/4/2017 14:00,28 +6/4/2017 15:00,29 +6/4/2017 16:00,29 +6/4/2017 17:00,29 +6/4/2017 18:00,28 +6/4/2017 19:00,26 +6/4/2017 20:00,24 +6/4/2017 21:00,22 +6/4/2017 22:00,17 +6/4/2017 23:00,20 +6/5/2017 0:00,18 +6/5/2017 1:00,18 +6/5/2017 2:00,19 +6/5/2017 3:00,19 +6/5/2017 4:00,18 +6/5/2017 5:00,18 +6/5/2017 6:00,20 +6/5/2017 7:00,22 +6/5/2017 8:00,22 +6/5/2017 9:00,24 +6/5/2017 10:00,25 +6/5/2017 11:00,26 +6/5/2017 12:00,28 +6/5/2017 13:00,29 +6/5/2017 14:00,29 +6/5/2017 15:00,28 +6/5/2017 16:00,28 +6/5/2017 17:00,27 +6/5/2017 18:00,24 +6/5/2017 19:00,23 +6/5/2017 20:00,21 +6/5/2017 21:00,18 +6/5/2017 22:00,13 +6/5/2017 23:00,12 +6/6/2017 0:00,10 +6/6/2017 1:00,12 +6/6/2017 2:00,11 +6/6/2017 3:00,8 +6/6/2017 4:00,7 +6/6/2017 5:00,8.7 +6/6/2017 6:00,12 +6/6/2017 7:00,15 +6/6/2017 8:00,16 +6/6/2017 9:00,17 +6/6/2017 10:00,18 +6/6/2017 11:00,19 +6/6/2017 12:00,20 +6/6/2017 13:00,21 +6/6/2017 14:00,21 +6/6/2017 15:00,21 +6/6/2017 16:00,21 +6/6/2017 17:00,19 +6/6/2017 18:00,18 +6/6/2017 19:00,17 +6/6/2017 20:00,15 +6/6/2017 21:00,13 +6/6/2017 22:00,11 +6/6/2017 23:00,11 +6/7/2017 0:00,9 +6/7/2017 1:00,7 +6/7/2017 2:00,8 +6/7/2017 3:00,8 +6/7/2017 4:00,5 +6/7/2017 5:00,5 +6/7/2017 6:00,9 +6/7/2017 7:00,12 +6/7/2017 8:00,13 +6/7/2017 9:00,14 +6/7/2017 10:00,15 +6/7/2017 11:00,14 +6/7/2017 12:00,16 +6/7/2017 13:00,17 +6/7/2017 14:00,18 +6/7/2017 15:00,18 +6/7/2017 16:00,18 +6/7/2017 17:00,18 +6/7/2017 18:00,11 +6/7/2017 19:00,12 +6/7/2017 20:00,12 +6/7/2017 21:00,11 +6/7/2017 22:00,11 +6/7/2017 23:00,9 +6/8/2017 0:00,8 +6/8/2017 1:00,7 +6/8/2017 2:00,8 +6/8/2017 3:00,7 +6/8/2017 4:00,8 +6/8/2017 5:00,7 +6/8/2017 6:00,9 +6/8/2017 7:00,12 +6/8/2017 8:00,13 +6/8/2017 9:00,14 +6/8/2017 10:00,16 +6/8/2017 11:00,17 +6/8/2017 12:00,17 +6/8/2017 13:00,18 +6/8/2017 14:00,18 +6/8/2017 15:00,19 +6/8/2017 16:00,16 +6/8/2017 17:00,12 +6/8/2017 18:00,11 +6/8/2017 19:00,11 +6/8/2017 20:00,11 +6/8/2017 21:00,11 +6/8/2017 22:00,11 +6/8/2017 23:00,11 +6/9/2017 0:00,10 +6/9/2017 1:00,11 +6/9/2017 2:00,11 +6/9/2017 3:00,11 +6/9/2017 4:00,11 +6/9/2017 5:00,11 +6/9/2017 6:00,11 +6/9/2017 7:00,11 +6/9/2017 8:00,12 +6/9/2017 9:00,13 +6/9/2017 10:00,17 +6/9/2017 11:00,18 +6/9/2017 12:00,19 +6/9/2017 13:00,13 +6/9/2017 14:00,14 +6/9/2017 15:00,16 +6/9/2017 16:00,16 +6/9/2017 17:00,17 +6/9/2017 18:00,14 +6/9/2017 19:00,14 +6/9/2017 20:00,14 +6/9/2017 21:00,14 +6/9/2017 22:00,14 +6/9/2017 23:00,13 +6/10/2017 0:00,13 +6/10/2017 1:00,13 +6/10/2017 2:00,13 +6/10/2017 3:00,13 +6/10/2017 4:00,13 +6/10/2017 5:00,12 +6/10/2017 6:00,14 +6/10/2017 7:00,14 +6/10/2017 8:00,17 +6/10/2017 9:00,18 +6/10/2017 10:00,21 +6/10/2017 11:00,22 +6/10/2017 12:00,23 +6/10/2017 13:00,23 +6/10/2017 14:00,24 +6/10/2017 15:00,26 +6/10/2017 16:00,26 +6/10/2017 17:00,25 +6/10/2017 18:00,24 +6/10/2017 19:00,22 +6/10/2017 20:00,19 +6/10/2017 21:00,18 +6/10/2017 22:00,17 +6/10/2017 23:00,16 +6/11/2017 0:00,14 +6/11/2017 1:00,15 +6/11/2017 2:00,14 +6/11/2017 3:00,13 +6/11/2017 4:00,12 +6/11/2017 5:00,11 +6/11/2017 6:00,13 +6/11/2017 7:00,15 +6/11/2017 8:00,17 +6/11/2017 9:00,18 +6/11/2017 10:00,21 +6/11/2017 11:00,22 +6/11/2017 12:00,23 +6/11/2017 13:00,25 +6/11/2017 14:00,26 +6/11/2017 15:00,26 +6/11/2017 16:00,27 +6/11/2017 17:00,27 +6/11/2017 18:00,26 +6/11/2017 19:00,24 +6/11/2017 20:00,21 +6/11/2017 21:00,18 +6/11/2017 22:00,13 +6/11/2017 23:00,13 +6/12/2017 0:00,12 +6/12/2017 1:00,13 +6/12/2017 2:00,12 +6/12/2017 3:00,10 +6/12/2017 4:00,10 +6/12/2017 5:00,11 +6/12/2017 6:00,13 +6/12/2017 7:00,17 +6/12/2017 8:00,19 +6/12/2017 9:00,22 +6/12/2017 10:00,25 +6/12/2017 11:00,27 +6/12/2017 12:00,30 +6/12/2017 13:00,31 +6/12/2017 14:00,32 +6/12/2017 15:00,31 +6/12/2017 16:00,32 +6/12/2017 17:00,32 +6/12/2017 18:00,29 +6/12/2017 19:00,28 +6/12/2017 20:00,20 +6/12/2017 21:00,18 +6/12/2017 22:00,20 +6/12/2017 23:00,17 +6/13/2017 0:00,17 +6/13/2017 1:00,13 +6/13/2017 2:00,14 +6/13/2017 3:00,13 +6/13/2017 4:00,12 +6/13/2017 5:00,12 +6/13/2017 6:00,16 +6/13/2017 7:00,19 +6/13/2017 8:00,22 +6/13/2017 9:00,23 +6/13/2017 10:00,26 +6/13/2017 11:00,28 +6/13/2017 12:00,31 +6/13/2017 13:00,32 +6/13/2017 14:00,33.2 +6/13/2017 15:00,34 +6/13/2017 16:00,33.6 +6/13/2017 17:00,33 +6/13/2017 18:00,29.7 +6/13/2017 19:00,25.8 +6/13/2017 20:00,21 +6/13/2017 21:00,18 +6/13/2017 22:00,17 +6/13/2017 23:00,19 +6/14/2017 0:00,19 +6/14/2017 1:00,19 +6/14/2017 2:00,19 +6/14/2017 3:00,19 +6/14/2017 4:00,19 +6/14/2017 5:00,17 +6/14/2017 6:00,18 +6/14/2017 7:00,22 +6/14/2017 8:00,23 +6/14/2017 9:00,24 +6/14/2017 10:00,27 +6/14/2017 11:00,30 +6/14/2017 12:00,31 +6/14/2017 13:00,34 +6/14/2017 14:00,35 +6/14/2017 15:00,36 +6/14/2017 16:00,35 +6/14/2017 17:00,35 +6/14/2017 18:00,33 +6/14/2017 19:00,31 +6/14/2017 20:00,27 +6/14/2017 21:00,24 +6/14/2017 22:00,19 +6/14/2017 23:00,24 +6/15/2017 0:00,18 +6/15/2017 1:00,20 +6/15/2017 2:00,21 +6/15/2017 3:00,21 +6/15/2017 4:00,20 +6/15/2017 5:00,20 +6/15/2017 6:00,21 +6/15/2017 7:00,22 +6/15/2017 8:00,24 +6/15/2017 9:00,26 +6/15/2017 10:00,28 +6/15/2017 11:00,29.5 +6/15/2017 12:00,31 +6/15/2017 13:00,33 +6/15/2017 14:00,35 +6/15/2017 15:00,36 +6/15/2017 16:00,36 +6/15/2017 17:00,36 +6/15/2017 18:00,36 +6/15/2017 19:00,34 +6/15/2017 20:00,32 +6/15/2017 21:00,24 +6/15/2017 22:00,22 +6/15/2017 23:00,19 +6/16/2017 0:00,19 +6/16/2017 1:00,20 +6/16/2017 2:00,21 +6/16/2017 3:00,19 +6/16/2017 4:00,19 +6/16/2017 5:00,17 +6/16/2017 6:00,19 +6/16/2017 7:00,21 +6/16/2017 8:00,23 +6/16/2017 9:00,24 +6/16/2017 10:00,26 +6/16/2017 11:00,28 +6/16/2017 12:00,30 +6/16/2017 13:00,31 +6/16/2017 14:00,31 +6/16/2017 15:00,29 +6/16/2017 16:00,31 +6/16/2017 17:00,29 +6/16/2017 18:00,28 +6/16/2017 19:00,26 +6/16/2017 20:00,21 +6/16/2017 21:00,18 +6/16/2017 22:00,18 +6/16/2017 23:00,17 +6/17/2017 0:00,16 +6/17/2017 1:00,15 +6/17/2017 2:00,14 +6/17/2017 3:00,14 +6/17/2017 4:00,14 +6/17/2017 5:00,14 +6/17/2017 6:00,15 +6/17/2017 7:00,17 +6/17/2017 8:00,19 +6/17/2017 9:00,19 +6/17/2017 10:00,20 +6/17/2017 11:00,23 +6/17/2017 12:00,24 +6/17/2017 13:00,25 +6/17/2017 14:00,24 +6/17/2017 15:00,22 +6/17/2017 16:00,21 +6/17/2017 17:00,18 +6/17/2017 18:00,17 +6/17/2017 19:00,17 +6/17/2017 20:00,17 +6/17/2017 21:00,17 +6/17/2017 22:00,16 +6/17/2017 23:00,16 +6/18/2017 0:00,16 +6/18/2017 1:00,15.1 +6/18/2017 2:00,14.2 +6/18/2017 3:00,13.3 +6/18/2017 4:00,12.9 +6/18/2017 5:00,13.1 +6/18/2017 6:00,15 +6/18/2017 7:00,18 +6/18/2017 8:00,21.6 +6/18/2017 9:00,22.1 +6/18/2017 10:00,23.7 +6/18/2017 11:00,25.2 +6/18/2017 12:00,25.8 +6/18/2017 13:00,26.3 +6/18/2017 14:00,24.9 +6/18/2017 15:00,24.4 +6/18/2017 16:00,24 +6/18/2017 17:00,22 +6/18/2017 18:00,22 +6/18/2017 19:00,19 +6/18/2017 20:00,17 +6/18/2017 21:00,16 +6/18/2017 22:00,14 +6/18/2017 23:00,12 +6/19/2017 0:00,12 +6/19/2017 1:00,11 +6/19/2017 2:00,12 +6/19/2017 3:00,12 +6/19/2017 4:00,11 +6/19/2017 5:00,12 +6/19/2017 6:00,13.6 +6/19/2017 7:00,15.5 +6/19/2017 8:00,17 +6/19/2017 9:00,21.7 +6/19/2017 10:00,22.4 +6/19/2017 11:00,24.1 +6/19/2017 12:00,25.8 +6/19/2017 13:00,26.5 +6/19/2017 14:00,27.2 +6/19/2017 15:00,26.9 +6/19/2017 16:00,26.6 +6/19/2017 17:00,26.3 +6/19/2017 18:00,25 +6/19/2017 19:00,22.5 +6/19/2017 20:00,19 +6/19/2017 21:00,15.3 +6/19/2017 22:00,12.4 +6/19/2017 23:00,10 +6/20/2017 0:00,11 +6/20/2017 1:00,10 +6/20/2017 2:00,8 +6/20/2017 3:00,8 +6/20/2017 4:00,9 +6/20/2017 5:00,8 +6/20/2017 6:00,12 +6/20/2017 7:00,16 +6/20/2017 8:00,18 +6/20/2017 9:00,20 +6/20/2017 10:00,23 +6/20/2017 11:00,25 +6/20/2017 12:00,27 +6/20/2017 13:00,28 +6/20/2017 14:00,29 +6/20/2017 15:00,29 +6/20/2017 16:00,30 +6/20/2017 17:00,29 +6/20/2017 18:00,29 +6/20/2017 19:00,26 +6/20/2017 20:00,19 +6/20/2017 21:00,20.1 +6/20/2017 22:00,22 +6/20/2017 23:00,19.5 +6/21/2017 0:00,16.7 +6/21/2017 1:00,14.4 +6/21/2017 2:00,12 +6/21/2017 3:00,11 +6/21/2017 4:00,11.1 +6/21/2017 5:00,11.7 +6/21/2017 6:00,14 +6/21/2017 7:00,19 +6/21/2017 8:00,21 +6/21/2017 9:00,24 +6/21/2017 10:00,26 +6/21/2017 11:00,28 +6/21/2017 12:00,29.3 +6/21/2017 13:00,30.2 +6/21/2017 14:00,31 +6/21/2017 15:00,33 +6/21/2017 16:00,31 +6/21/2017 17:00,32 +6/21/2017 18:00,32 +6/21/2017 19:00,28 +6/21/2017 20:00,23 +6/21/2017 21:00,19 +6/21/2017 22:00,17 +6/21/2017 23:00,16 +6/22/2017 0:00,15 +6/22/2017 1:00,13 +6/22/2017 2:00,12 +6/22/2017 3:00,11 +6/22/2017 4:00,11 +6/22/2017 5:00,11 +6/22/2017 6:00,13 +6/22/2017 7:00,18 +6/22/2017 8:00,22 +6/22/2017 9:00,26 +6/22/2017 10:00,27 +6/22/2017 11:00,31 +6/22/2017 12:00,32 +6/22/2017 13:00,34 +6/22/2017 14:00,34 +6/22/2017 15:00,35 +6/22/2017 16:00,36 +6/22/2017 17:00,35 +6/22/2017 18:00,34 +6/22/2017 19:00,33 +6/22/2017 20:00,26 +6/22/2017 21:00,22 +6/22/2017 22:00,24 +6/22/2017 23:00,21 +6/23/2017 0:00,21 +6/23/2017 1:00,22 +6/23/2017 2:00,21 +6/23/2017 3:00,20 +6/23/2017 4:00,21 +6/23/2017 5:00,19 +6/23/2017 6:00,18 +6/23/2017 7:00,17 +6/23/2017 8:00,17 +6/23/2017 9:00,17 +6/23/2017 10:00,18 +6/23/2017 11:00,22 +6/23/2017 12:00,24 +6/23/2017 13:00,27 +6/23/2017 14:00,29 +6/23/2017 15:00,29 +6/23/2017 16:00,31 +6/23/2017 17:00,31 +6/23/2017 18:00,30 +6/23/2017 19:00,29 +6/23/2017 20:00,26 +6/23/2017 21:00,25 +6/23/2017 22:00,21 +6/23/2017 23:00,21 +6/24/2017 0:00,19 +6/24/2017 1:00,16 +6/24/2017 2:00,16 +6/24/2017 3:00,13 +6/24/2017 4:00,16 +6/24/2017 5:00,13 +6/24/2017 6:00,18 +6/24/2017 7:00,20 +6/24/2017 8:00,22 +6/24/2017 9:00,23 +6/24/2017 10:00,25 +6/24/2017 11:00,26 +6/24/2017 12:00,27 +6/24/2017 13:00,28 +6/24/2017 14:00,30 +6/24/2017 15:00,31 +6/24/2017 16:00,31.1 +6/24/2017 17:00,31 +6/24/2017 18:00,30 +6/24/2017 19:00,28 +6/24/2017 20:00,27 +6/24/2017 21:00,24 +6/24/2017 22:00,18 +6/24/2017 23:00,17 +6/25/2017 0:00,17 +6/25/2017 1:00,14 +6/25/2017 2:00,12 +6/25/2017 3:00,12 +6/25/2017 4:00,13 +6/25/2017 5:00,12 +6/25/2017 6:00,16 +6/25/2017 7:00,20 +6/25/2017 8:00,22 +6/25/2017 9:00,25 +6/25/2017 10:00,27 +6/25/2017 11:00,29 +6/25/2017 12:00,32 +6/25/2017 13:00,32 +6/25/2017 14:00,33 +6/25/2017 15:00,34 +6/25/2017 16:00,34 +6/25/2017 17:00,33 +6/25/2017 18:00,32 +6/25/2017 19:00,30 +6/25/2017 20:00,23 +6/25/2017 21:00,21 +6/25/2017 22:00,19 +6/25/2017 23:00,17 +6/26/2017 0:00,16 +6/26/2017 1:00,16 +6/26/2017 2:00,17 +6/26/2017 3:00,14 +6/26/2017 4:00,13 +6/26/2017 5:00,14 +6/26/2017 6:00,18 +6/26/2017 7:00,21 +6/26/2017 8:00,24 +6/26/2017 9:00,27 +6/26/2017 10:00,29 +6/26/2017 11:00,32 +6/26/2017 12:00,34 +6/26/2017 13:00,36 +6/26/2017 14:00,37 +6/26/2017 15:00,37 +6/26/2017 16:00,37 +6/26/2017 17:00,37 +6/26/2017 18:00,38 +6/26/2017 19:00,37 +6/26/2017 20:00,32 +6/26/2017 21:00,32 +6/26/2017 22:00,31 +6/26/2017 23:00,29 +6/27/2017 0:00,27 +6/27/2017 1:00,28 +6/27/2017 2:00,22 +6/27/2017 3:00,19 +6/27/2017 4:00,24 +6/27/2017 5:00,24 +6/27/2017 6:00,23 +6/27/2017 7:00,26 +6/27/2017 8:00,29 +6/27/2017 9:00,31 +6/27/2017 10:00,31 +6/27/2017 11:00,32 +6/27/2017 12:00,34 +6/27/2017 13:00,33 +6/27/2017 14:00,33 +6/27/2017 15:00,35 +6/27/2017 16:00,33 +6/27/2017 17:00,31 +6/27/2017 18:00,29 +6/27/2017 19:00,27 +6/27/2017 20:00,25 +6/27/2017 21:00,24 +6/27/2017 22:00,23 +6/27/2017 23:00,23 +6/28/2017 0:00,22 +6/28/2017 1:00,21 +6/28/2017 2:00,20.1 +6/28/2017 3:00,19.2 +6/28/2017 4:00,18.8 +6/28/2017 5:00,19 +6/28/2017 6:00,21 +6/28/2017 7:00,22 +6/28/2017 8:00,23 +6/28/2017 9:00,23 +6/28/2017 10:00,26 +6/28/2017 11:00,26 +6/28/2017 12:00,26.1 +6/28/2017 13:00,26 +6/28/2017 14:00,27 +6/28/2017 15:00,26 +6/28/2017 16:00,23 +6/28/2017 17:00,21 +6/28/2017 18:00,19 +6/28/2017 19:00,19 +6/28/2017 20:00,19 +6/28/2017 21:00,18 +6/28/2017 22:00,18 +6/28/2017 23:00,18 +6/29/2017 0:00,18 +6/29/2017 1:00,18 +6/29/2017 2:00,18 +6/29/2017 3:00,18 +6/29/2017 4:00,18 +6/29/2017 5:00,18 +6/29/2017 6:00,19 +6/29/2017 7:00,19 +6/29/2017 8:00,21 +6/29/2017 9:00,24 +6/29/2017 10:00,24 +6/29/2017 11:00,25 +6/29/2017 12:00,26 +6/29/2017 13:00,27 +6/29/2017 14:00,28 +6/29/2017 15:00,28 +6/29/2017 16:00,28 +6/29/2017 17:00,28 +6/29/2017 18:00,27 +6/29/2017 19:00,23 +6/29/2017 20:00,20 +6/29/2017 21:00,18 +6/29/2017 22:00,17 +6/29/2017 23:00,17 +6/30/2017 0:00,16 +6/30/2017 1:00,16 +6/30/2017 2:00,14 +6/30/2017 3:00,13 +6/30/2017 4:00,12 +6/30/2017 5:00,11 +6/30/2017 6:00,14 +6/30/2017 7:00,18 +6/30/2017 8:00,19 +6/30/2017 9:00,22 +6/30/2017 10:00,23 +6/30/2017 11:00,23 +6/30/2017 12:00,26 +6/30/2017 13:00,26 +6/30/2017 14:00,27 +6/30/2017 15:00,27 +6/30/2017 16:00,27 +6/30/2017 17:00,27 +6/30/2017 18:00,24 +6/30/2017 19:00,22 +6/30/2017 20:00,19 +6/30/2017 21:00,18 +6/30/2017 22:00,17.7 +6/30/2017 23:00,17.4 +7/1/2017 0:00,17.1 +7/1/2017 1:00,16.9 +7/1/2017 2:00,16.6 +7/1/2017 3:00,16.3 +7/1/2017 4:00,16 +7/1/2017 5:00,14 +7/1/2017 6:00,19 +7/1/2017 7:00,22 +7/1/2017 8:00,23 +7/1/2017 9:00,25 +7/1/2017 10:00,26 +7/1/2017 11:00,28 +7/1/2017 12:00,29 +7/1/2017 13:00,31 +7/1/2017 14:00,31 +7/1/2017 15:00,32 +7/1/2017 16:00,32 +7/1/2017 17:00,32 +7/1/2017 18:00,31 +7/1/2017 19:00,27 +7/1/2017 20:00,24 +7/1/2017 21:00,21 +7/1/2017 22:00,21 +7/1/2017 23:00,21 +7/2/2017 0:00,20 +7/2/2017 1:00,19 +7/2/2017 2:00,18 +7/2/2017 3:00,17 +7/2/2017 4:00,15 +7/2/2017 5:00,16 +7/2/2017 6:00,17 +7/2/2017 7:00,19 +7/2/2017 8:00,21 +7/2/2017 9:00,23 +7/2/2017 10:00,24 +7/2/2017 11:00,26 +7/2/2017 12:00,27 +7/2/2017 13:00,26 +7/2/2017 14:00,28 +7/2/2017 15:00,28 +7/2/2017 16:00,29 +7/2/2017 17:00,29 +7/2/2017 18:00,25 +7/2/2017 19:00,23 +7/2/2017 20:00,20 +7/2/2017 21:00,18 +7/2/2017 22:00,17 +7/2/2017 23:00,18 +7/3/2017 0:00,16 +7/3/2017 1:00,16 +7/3/2017 2:00,13 +7/3/2017 3:00,11 +7/3/2017 4:00,9 +7/3/2017 5:00,10 +7/3/2017 6:00,13 +7/3/2017 7:00,17 +7/3/2017 8:00,19 +7/3/2017 9:00,21 +7/3/2017 10:00,23 +7/3/2017 11:00,24 +7/3/2017 12:00,25 +7/3/2017 13:00,26 +7/3/2017 14:00,27 +7/3/2017 15:00,28 +7/3/2017 16:00,28 +7/3/2017 17:00,29 +7/3/2017 18:00,29 +7/3/2017 19:00,28 +7/3/2017 20:00,27 +7/3/2017 21:00,20 +7/3/2017 22:00,16 +7/3/2017 23:00,16 +7/4/2017 0:00,13 +7/4/2017 1:00,13 +7/4/2017 2:00,12 +7/4/2017 3:00,11 +7/4/2017 4:00,11 +7/4/2017 5:00,11 +7/4/2017 6:00,16 +7/4/2017 7:00,19 +7/4/2017 8:00,22 +7/4/2017 9:00,25 +7/4/2017 10:00,27 +7/4/2017 11:00,29 +7/4/2017 12:00,31 +7/4/2017 13:00,32 +7/4/2017 14:00,33 +7/4/2017 15:00,34 +7/4/2017 16:00,34 +7/4/2017 17:00,34 +7/4/2017 18:00,34 +7/4/2017 19:00,32 +7/4/2017 20:00,28 +7/4/2017 21:00,23 +7/4/2017 22:00,18 +7/4/2017 23:00,17 +7/5/2017 0:00,16 +7/5/2017 1:00,14 +7/5/2017 2:00,14 +7/5/2017 3:00,12 +7/5/2017 4:00,11 +7/5/2017 5:00,12 +7/5/2017 6:00,16 +7/5/2017 7:00,23 +7/5/2017 8:00,26 +7/5/2017 9:00,27 +7/5/2017 10:00,29 +7/5/2017 11:00,31 +7/5/2017 12:00,33 +7/5/2017 13:00,35 +7/5/2017 14:00,35 +7/5/2017 15:00,36 +7/5/2017 16:00,36 +7/5/2017 17:00,35 +7/5/2017 18:00,34 +7/5/2017 19:00,32 +7/5/2017 20:00,30 +7/5/2017 21:00,29 +7/5/2017 22:00,28 +7/5/2017 23:00,27 +7/6/2017 0:00,26 +7/6/2017 1:00,25 +7/6/2017 2:00,24 +7/6/2017 3:00,23 +7/6/2017 4:00,24 +7/6/2017 5:00,23 +7/6/2017 6:00,24 +7/6/2017 7:00,26 +7/6/2017 8:00,27 +7/6/2017 9:00,28 +7/6/2017 10:00,29 +7/6/2017 11:00,31 +7/6/2017 12:00,31 +7/6/2017 13:00,32 +7/6/2017 14:00,31 +7/6/2017 15:00,32 +7/6/2017 16:00,30 +7/6/2017 17:00,30 +7/6/2017 18:00,29 +7/6/2017 19:00,26 +7/6/2017 20:00,21 +7/6/2017 21:00,19 +7/6/2017 22:00,18 +7/6/2017 23:00,19 +7/7/2017 0:00,18 +7/7/2017 1:00,18 +7/7/2017 2:00,16 +7/7/2017 3:00,15 +7/7/2017 4:00,11 +7/7/2017 5:00,11 +7/7/2017 6:00,16 +7/7/2017 7:00,19 +7/7/2017 8:00,21 +7/7/2017 9:00,22 +7/7/2017 10:00,24 +7/7/2017 11:00,26 +7/7/2017 12:00,26 +7/7/2017 13:00,27 +7/7/2017 14:00,28 +7/7/2017 15:00,29 +7/7/2017 16:00,31 +7/7/2017 17:00,31 +7/7/2017 18:00,31 +7/7/2017 19:00,29 +7/7/2017 20:00,26 +7/7/2017 21:00,22 +7/7/2017 22:00,20 +7/7/2017 23:00,23 +7/8/2017 0:00,22 +7/8/2017 1:00,21 +7/8/2017 2:00,20 +7/8/2017 3:00,19 +7/8/2017 4:00,19 +7/8/2017 5:00,18 +7/8/2017 6:00,20 +7/8/2017 7:00,21 +7/8/2017 8:00,23 +7/8/2017 9:00,25 +7/8/2017 10:00,27 +7/8/2017 11:00,29 +7/8/2017 12:00,30 +7/8/2017 13:00,31 +7/8/2017 14:00,32 +7/8/2017 15:00,30 +7/8/2017 16:00,28 +7/8/2017 17:00,24 +7/8/2017 18:00,21 +7/8/2017 19:00,18 +7/8/2017 20:00,17 +7/8/2017 21:00,17 +7/8/2017 22:00,17 +7/8/2017 23:00,17 +7/9/2017 0:00,17 +7/9/2017 1:00,17 +7/9/2017 2:00,18 +7/9/2017 3:00,17 +7/9/2017 4:00,17 +7/9/2017 5:00,16 +7/9/2017 6:00,17 +7/9/2017 7:00,18 +7/9/2017 8:00,19 +7/9/2017 9:00,20 +7/9/2017 10:00,21 +7/9/2017 11:00,22 +7/9/2017 12:00,23 +7/9/2017 13:00,24 +7/9/2017 14:00,26 +7/9/2017 15:00,26 +7/9/2017 16:00,26 +7/9/2017 17:00,26 +7/9/2017 18:00,26 +7/9/2017 19:00,26 +7/9/2017 20:00,23 +7/9/2017 21:00,22 +7/9/2017 22:00,21 +7/9/2017 23:00,20 +7/10/2017 0:00,19 +7/10/2017 1:00,19 +7/10/2017 2:00,19 +7/10/2017 3:00,19 +7/10/2017 4:00,18 +7/10/2017 5:00,18 +7/10/2017 6:00,18 +7/10/2017 7:00,19 +7/10/2017 8:00,21 +7/10/2017 9:00,22 +7/10/2017 10:00,23 +7/10/2017 11:00,24 +7/10/2017 12:00,25 +7/10/2017 13:00,26 +7/10/2017 14:00,26 +7/10/2017 15:00,27 +7/10/2017 16:00,27 +7/10/2017 17:00,27 +7/10/2017 18:00,28 +7/10/2017 19:00,27 +7/10/2017 20:00,26 +7/10/2017 21:00,23 +7/10/2017 22:00,22 +7/10/2017 23:00,21 +7/11/2017 0:00,22 +7/11/2017 1:00,22 +7/11/2017 2:00,20 +7/11/2017 3:00,21 +7/11/2017 4:00,21 +7/11/2017 5:00,19 +7/11/2017 6:00,20 +7/11/2017 7:00,21 +7/11/2017 8:00,24 +7/11/2017 9:00,26 +7/11/2017 10:00,27 +7/11/2017 11:00,28 +7/11/2017 12:00,29 +7/11/2017 13:00,30 +7/11/2017 14:00,32 +7/11/2017 15:00,33 +7/11/2017 16:00,34 +7/11/2017 17:00,33 +7/11/2017 18:00,33 +7/11/2017 19:00,32 +7/11/2017 20:00,29 +7/11/2017 21:00,27 +7/11/2017 22:00,26 +7/11/2017 23:00,24 +7/12/2017 0:00,23 +7/12/2017 1:00,21 +7/12/2017 2:00,21 +7/12/2017 3:00,20 +7/12/2017 4:00,19 +7/12/2017 5:00,18 +7/12/2017 6:00,20 +7/12/2017 7:00,22 +7/12/2017 8:00,24 +7/12/2017 9:00,24 +7/12/2017 10:00,26 +7/12/2017 11:00,28 +7/12/2017 12:00,29 +7/12/2017 13:00,30 +7/12/2017 14:00,31 +7/12/2017 15:00,32 +7/12/2017 16:00,32 +7/12/2017 17:00,32 +7/12/2017 18:00,32 +7/12/2017 19:00,31 +7/12/2017 20:00,29 +7/12/2017 21:00,22 +7/12/2017 22:00,23 +7/12/2017 23:00,22 +7/13/2017 0:00,19 +7/13/2017 1:00,19 +7/13/2017 2:00,18 +7/13/2017 3:00,17 +7/13/2017 4:00,17 +7/13/2017 5:00,17 +7/13/2017 6:00,18 +7/13/2017 7:00,20 +7/13/2017 8:00,22 +7/13/2017 9:00,23 +7/13/2017 10:00,25 +7/13/2017 11:00,27 +7/13/2017 12:00,28 +7/13/2017 13:00,29 +7/13/2017 14:00,29 +7/13/2017 15:00,29 +7/13/2017 16:00,30 +7/13/2017 17:00,31 +7/13/2017 18:00,30 +7/13/2017 19:00,29 +7/13/2017 20:00,28 +7/13/2017 21:00,23 +7/13/2017 22:00,22 +7/13/2017 23:00,20 +7/14/2017 0:00,18 +7/14/2017 1:00,18 +7/14/2017 2:00,14 +7/14/2017 3:00,13 +7/14/2017 4:00,12 +7/14/2017 5:00,12 +7/14/2017 6:00,16 +7/14/2017 7:00,19 +7/14/2017 8:00,22 +7/14/2017 9:00,23 +7/14/2017 10:00,25 +7/14/2017 11:00,27 +7/14/2017 12:00,28 +7/14/2017 13:00,30 +7/14/2017 14:00,31 +7/14/2017 15:00,32 +7/14/2017 16:00,32 +7/14/2017 17:00,33 +7/14/2017 18:00,31 +7/14/2017 19:00,27 +7/14/2017 20:00,24 +7/14/2017 21:00,21 +7/14/2017 22:00,20 +7/14/2017 23:00,17 +7/15/2017 0:00,17 +7/15/2017 1:00,14 +7/15/2017 2:00,13 +7/15/2017 3:00,13 +7/15/2017 4:00,12 +7/15/2017 5:00,12 +7/15/2017 6:00,17 +7/15/2017 7:00,20 +7/15/2017 8:00,23 +7/15/2017 9:00,26 +7/15/2017 10:00,29 +7/15/2017 11:00,33 +7/15/2017 12:00,34 +7/15/2017 13:00,36 +7/15/2017 14:00,36 +7/15/2017 15:00,37 +7/15/2017 16:00,39 +7/15/2017 17:00,38 +7/15/2017 18:00,30 +7/15/2017 19:00,29 +7/15/2017 20:00,24 +7/15/2017 21:00,23 +7/15/2017 22:00,24 +7/15/2017 23:00,21 +7/16/2017 0:00,21 +7/16/2017 1:00,23 +7/16/2017 2:00,21 +7/16/2017 3:00,19 +7/16/2017 4:00,17 +7/16/2017 5:00,17 +7/16/2017 6:00,21 +7/16/2017 7:00,22 +7/16/2017 8:00,23 +7/16/2017 9:00,25 +7/16/2017 10:00,27 +7/16/2017 11:00,28 +7/16/2017 12:00,29 +7/16/2017 13:00,27 +7/16/2017 14:00,31 +7/16/2017 15:00,31 +7/16/2017 16:00,30 +7/16/2017 17:00,31 +7/16/2017 18:00,30 +7/16/2017 19:00,27 +7/16/2017 20:00,23 +7/16/2017 21:00,20 +7/16/2017 22:00,18 +7/16/2017 23:00,16 +7/17/2017 0:00,18 +7/17/2017 1:00,14 +7/17/2017 2:00,14 +7/17/2017 3:00,12 +7/17/2017 4:00,13 +7/17/2017 5:00,13 +7/17/2017 6:00,16 +7/17/2017 7:00,19 +7/17/2017 8:00,22 +7/17/2017 9:00,24 +7/17/2017 10:00,27 +7/17/2017 11:00,28 +7/17/2017 12:00,29 +7/17/2017 13:00,30 +7/17/2017 14:00,31 +7/17/2017 15:00,32 +7/17/2017 16:00,32 +7/17/2017 17:00,32 +7/17/2017 18:00,31 +7/17/2017 19:00,29 +7/17/2017 20:00,25 +7/17/2017 21:00,19 +7/17/2017 22:00,18 +7/17/2017 23:00,16 +7/18/2017 0:00,17 +7/18/2017 1:00,16 +7/18/2017 2:00,13 +7/18/2017 3:00,13 +7/18/2017 4:00,13 +7/18/2017 5:00,12 +7/18/2017 6:00,17 +7/18/2017 7:00,21 +7/18/2017 8:00,24 +7/18/2017 9:00,27 +7/18/2017 10:00,29 +7/18/2017 11:00,33 +7/18/2017 12:00,34 +7/18/2017 13:00,35 +7/18/2017 14:00,37 +7/18/2017 15:00,37 +7/18/2017 16:00,38 +7/18/2017 17:00,38 +7/18/2017 18:00,38 +7/18/2017 19:00,37 +7/18/2017 20:00,34 +7/18/2017 21:00,26 +7/18/2017 22:00,27 +7/18/2017 23:00,24 +7/19/2017 0:00,26 +7/19/2017 1:00,23 +7/19/2017 2:00,22 +7/19/2017 3:00,17 +7/19/2017 4:00,16 +7/19/2017 5:00,15 +7/19/2017 6:00,19 +7/19/2017 7:00,25 +7/19/2017 8:00,27 +7/19/2017 9:00,28 +7/19/2017 10:00,29 +7/19/2017 11:00,31 +7/19/2017 12:00,32 +7/19/2017 13:00,33 +7/19/2017 14:00,34 +7/19/2017 15:00,35 +7/19/2017 16:00,36 +7/19/2017 17:00,36 +7/19/2017 18:00,36 +7/19/2017 19:00,35 +7/19/2017 20:00,29 +7/19/2017 21:00,25 +7/19/2017 22:00,25 +7/19/2017 23:00,24 +7/20/2017 0:00,21 +7/20/2017 1:00,21 +7/20/2017 2:00,17 +7/20/2017 3:00,14 +7/20/2017 4:00,12 +7/20/2017 5:00,12 +7/20/2017 6:00,17 +7/20/2017 7:00,23 +7/20/2017 8:00,24 +7/20/2017 9:00,26 +7/20/2017 10:00,28 +7/20/2017 11:00,29 +7/20/2017 12:00,32 +7/20/2017 13:00,33 +7/20/2017 14:00,34 +7/20/2017 15:00,36 +7/20/2017 16:00,36 +7/20/2017 17:00,36 +7/20/2017 18:00,36 +7/20/2017 19:00,35 +7/20/2017 20:00,31 +7/20/2017 21:00,28 +7/20/2017 22:00,23 +7/20/2017 23:00,23 +7/21/2017 0:00,19 +7/21/2017 1:00,17 +7/21/2017 2:00,17 +7/21/2017 3:00,13 +7/21/2017 4:00,13 +7/21/2017 5:00,13 +7/21/2017 6:00,16 +7/21/2017 7:00,22 +7/21/2017 8:00,24 +7/21/2017 9:00,27 +7/21/2017 10:00,29 +7/21/2017 11:00,31 +7/21/2017 12:00,32 +7/21/2017 13:00,33 +7/21/2017 14:00,36 +7/21/2017 15:00,37 +7/21/2017 16:00,38 +7/21/2017 17:00,34 +7/21/2017 18:00,34 +7/21/2017 19:00,32 +7/21/2017 20:00,32 +7/21/2017 21:00,33 +7/21/2017 22:00,29 +7/21/2017 23:00,32 +7/22/2017 0:00,28 +7/22/2017 1:00,28 +7/22/2017 2:00,26 +7/22/2017 3:00,26 +7/22/2017 4:00,26 +7/22/2017 5:00,25 +7/22/2017 6:00,26 +7/22/2017 7:00,25 +7/22/2017 8:00,27 +7/22/2017 9:00,30 +7/22/2017 10:00,28 +7/22/2017 11:00,28 +7/22/2017 12:00,27 +7/22/2017 13:00,31 +7/22/2017 14:00,31 +7/22/2017 15:00,32 +7/22/2017 16:00,30 +7/22/2017 17:00,28 +7/22/2017 18:00,29 +7/22/2017 19:00,28 +7/22/2017 20:00,23 +7/22/2017 21:00,24 +7/22/2017 22:00,23 +7/22/2017 23:00,19 +7/23/2017 0:00,19 +7/23/2017 1:00,19 +7/23/2017 2:00,19 +7/23/2017 3:00,18 +7/23/2017 4:00,18 +7/23/2017 5:00,18 +7/23/2017 6:00,20 +7/23/2017 7:00,21 +7/23/2017 8:00,22 +7/23/2017 9:00,23 +7/23/2017 10:00,26 +7/23/2017 11:00,27 +7/23/2017 12:00,29 +7/23/2017 13:00,30 +7/23/2017 14:00,32 +7/23/2017 15:00,32 +7/23/2017 16:00,33 +7/23/2017 17:00,34 +7/23/2017 18:00,33 +7/23/2017 19:00,32 +7/23/2017 20:00,30 +7/23/2017 21:00,23 +7/23/2017 22:00,23 +7/23/2017 23:00,21 +7/24/2017 0:00,21 +7/24/2017 1:00,18 +7/24/2017 2:00,15 +7/24/2017 3:00,15 +7/24/2017 4:00,12 +7/24/2017 5:00,13 +7/24/2017 6:00,18 +7/24/2017 7:00,21 +7/24/2017 8:00,23 +7/24/2017 9:00,25 +7/24/2017 10:00,27 +7/24/2017 11:00,28 +7/24/2017 12:00,28 +7/24/2017 13:00,30 +7/24/2017 14:00,31 +7/24/2017 15:00,32 +7/24/2017 16:00,32 +7/24/2017 17:00,32 +7/24/2017 18:00,31 +7/24/2017 19:00,30 +7/24/2017 20:00,27 +7/24/2017 21:00,24 +7/24/2017 22:00,18 +7/24/2017 23:00,21 +7/25/2017 0:00,22 +7/25/2017 1:00,19 +7/25/2017 2:00,17 +7/25/2017 3:00,14 +7/25/2017 4:00,13 +7/25/2017 5:00,12 +7/25/2017 6:00,14 +7/25/2017 7:00,18 +7/25/2017 8:00,22 +7/25/2017 9:00,25 +7/25/2017 10:00,27 +7/25/2017 11:00,28 +7/25/2017 12:00,31 +7/25/2017 13:00,31 +7/25/2017 14:00,32 +7/25/2017 15:00,33 +7/25/2017 16:00,33 +7/25/2017 17:00,33 +7/25/2017 18:00,33 +7/25/2017 19:00,31 +7/25/2017 20:00,24 +7/25/2017 21:00,21 +7/25/2017 22:00,18 +7/25/2017 23:00,17 +7/26/2017 0:00,18 +7/26/2017 1:00,16 +7/26/2017 2:00,14 +7/26/2017 3:00,12 +7/26/2017 4:00,12 +7/26/2017 5:00,12 +7/26/2017 6:00,16 +7/26/2017 7:00,19 +7/26/2017 8:00,22 +7/26/2017 9:00,25 +7/26/2017 10:00,28 +7/26/2017 11:00,29 +7/26/2017 12:00,31 +7/26/2017 13:00,32 +7/26/2017 14:00,34 +7/26/2017 15:00,35 +7/26/2017 16:00,35 +7/26/2017 17:00,33 +7/26/2017 18:00,33 +7/26/2017 19:00,31 +7/26/2017 20:00,23 +7/26/2017 21:00,21 +7/26/2017 22:00,18 +7/26/2017 23:00,16 +7/27/2017 0:00,16 +7/27/2017 1:00,16 +7/27/2017 2:00,17 +7/27/2017 3:00,13 +7/27/2017 4:00,12 +7/27/2017 5:00,12 +7/27/2017 6:00,16 +7/27/2017 7:00,19 +7/27/2017 8:00,23 +7/27/2017 9:00,26 +7/27/2017 10:00,29 +7/27/2017 11:00,31 +7/27/2017 12:00,33 +7/27/2017 13:00,34 +7/27/2017 14:00,36 +7/27/2017 15:00,37 +7/27/2017 16:00,38 +7/27/2017 17:00,36 +7/27/2017 18:00,34 +7/27/2017 19:00,31 +7/27/2017 20:00,27 +7/27/2017 21:00,24 +7/27/2017 22:00,20 +7/27/2017 23:00,18 +7/28/2017 0:00,18 +7/28/2017 1:00,18 +7/28/2017 2:00,16 +7/28/2017 3:00,15 +7/28/2017 4:00,14 +7/28/2017 5:00,15 +7/28/2017 6:00,15 +7/28/2017 7:00,19 +7/28/2017 8:00,24 +7/28/2017 9:00,28 +7/28/2017 10:00,31 +7/28/2017 11:00,33 +7/28/2017 12:00,35 +7/28/2017 13:00,37 +7/28/2017 14:00,38 +7/28/2017 15:00,38 +7/28/2017 16:00,39 +7/28/2017 17:00,39 +7/28/2017 18:00,39 +7/28/2017 19:00,36 +7/28/2017 20:00,28 +7/28/2017 21:00,28 +7/28/2017 22:00,28 +7/28/2017 23:00,24.9 +7/29/2017 0:00,22 +7/29/2017 1:00,21.6 +7/29/2017 2:00,21.3 +7/29/2017 3:00,21 +7/29/2017 4:00,18 +7/29/2017 5:00,16 +7/29/2017 6:00,18 +7/29/2017 7:00,22.2 +7/29/2017 8:00,26 +7/29/2017 9:00,28.1 +7/29/2017 10:00,30 +7/29/2017 11:00,31 +7/29/2017 12:00,33 +7/29/2017 13:00,34 +7/29/2017 14:00,36 +7/29/2017 15:00,37 +7/29/2017 16:00,37 +7/29/2017 17:00,37 +7/29/2017 18:00,37 +7/29/2017 19:00,35 +7/29/2017 20:00,32 +7/29/2017 21:00,28 +7/29/2017 22:00,21 +7/29/2017 23:00,24 +7/30/2017 0:00,22 +7/30/2017 1:00,18 +7/30/2017 2:00,17 +7/30/2017 3:00,17 +7/30/2017 4:00,14 +7/30/2017 5:00,14 +7/30/2017 6:00,16 +7/30/2017 7:00,22 +7/30/2017 8:00,26 +7/30/2017 9:00,28 +7/30/2017 10:00,30 +7/30/2017 11:00,31 +7/30/2017 12:00,33 +7/30/2017 13:00,34 +7/30/2017 14:00,36 +7/30/2017 15:00,37 +7/30/2017 16:00,37 +7/30/2017 17:00,37 +7/30/2017 18:00,37 +7/30/2017 19:00,36 +7/30/2017 20:00,33 +7/30/2017 21:00,26 +7/30/2017 22:00,22 +7/30/2017 23:00,19 +7/31/2017 0:00,20 +7/31/2017 1:00,21 +7/31/2017 2:00,22 +7/31/2017 3:00,18 +7/31/2017 4:00,17 +7/31/2017 5:00,16 +7/31/2017 6:00,18 +7/31/2017 7:00,23 +7/31/2017 8:00,26 +7/31/2017 9:00,29 +7/31/2017 10:00,31 +7/31/2017 11:00,33 +7/31/2017 12:00,34 +7/31/2017 13:00,36 +7/31/2017 14:00,37 +7/31/2017 15:00,37 +7/31/2017 16:00,38 +7/31/2017 17:00,38 +7/31/2017 18:00,38 +7/31/2017 19:00,36 +7/31/2017 20:00,31 +7/31/2017 21:00,24 +7/31/2017 22:00,21.6 +7/31/2017 23:00,19.1 +8/1/2017 0:00,16.7 +8/1/2017 1:00,14.3 +8/1/2017 2:00,11.9 +8/1/2017 3:00,9.4 +8/1/2017 4:00,7 +8/1/2017 5:00,8 +8/1/2017 6:00,10 +8/1/2017 7:00,14 +8/1/2017 8:00,17 +8/1/2017 9:00,21 +8/1/2017 10:00,23 +8/1/2017 11:00,24 +8/1/2017 12:00,27 +8/1/2017 13:00,28 +8/1/2017 14:00,29 +8/1/2017 15:00,30 +8/1/2017 16:00,31 +8/1/2017 17:00,31 +8/1/2017 18:00,30 +8/1/2017 19:00,29 +8/1/2017 20:00,28 +8/1/2017 21:00,26 +8/1/2017 22:00,24 +8/1/2017 23:00,21 +8/2/2017 0:00,19 +8/2/2017 1:00,19 +8/2/2017 2:00,17 +8/2/2017 3:00,16 +8/2/2017 4:00,17 +8/2/2017 5:00,14 +8/2/2017 6:00,16 +8/2/2017 7:00,19 +8/2/2017 8:00,19 +8/2/2017 9:00,22 +8/2/2017 10:00,22 +8/2/2017 11:00,25 +8/2/2017 12:00,26 +8/2/2017 13:00,27 +8/2/2017 14:00,28 +8/2/2017 15:00,28 +8/2/2017 16:00,28 +8/2/2017 17:00,28 +8/2/2017 18:00,26 +8/2/2017 19:00,20 +8/2/2017 20:00,17 +8/2/2017 21:00,15 +8/2/2017 22:00,18 +8/2/2017 23:00,17 +8/3/2017 0:00,14 +8/3/2017 1:00,13 +8/3/2017 2:00,9 +8/3/2017 3:00,9 +8/3/2017 4:00,7 +8/3/2017 5:00,7 +8/3/2017 6:00,10 +8/3/2017 7:00,14 +8/3/2017 8:00,17 +8/3/2017 9:00,19 +8/3/2017 10:00,21 +8/3/2017 11:00,23 +8/3/2017 12:00,25 +8/3/2017 13:00,27 +8/3/2017 14:00,28 +8/3/2017 15:00,28 +8/3/2017 16:00,28 +8/3/2017 17:00,28 +8/3/2017 18:00,28 +8/3/2017 19:00,26 +8/3/2017 20:00,21 +8/3/2017 21:00,21 +8/3/2017 22:00,22 +8/3/2017 23:00,20 +8/4/2017 0:00,21 +8/4/2017 1:00,21 +8/4/2017 2:00,19 +8/4/2017 3:00,18 +8/4/2017 4:00,17 +8/4/2017 5:00,18 +8/4/2017 6:00,17 +8/4/2017 7:00,17 +8/4/2017 8:00,17 +8/4/2017 9:00,19 +8/4/2017 10:00,22 +8/4/2017 11:00,22 +8/4/2017 12:00,23 +8/4/2017 13:00,23 +8/4/2017 14:00,23 +8/4/2017 15:00,23 +8/4/2017 16:00,23 +8/4/2017 17:00,23 +8/4/2017 18:00,22 +8/4/2017 19:00,22 +8/4/2017 20:00,19 +8/4/2017 21:00,17 +8/4/2017 22:00,14 +8/4/2017 23:00,14 +8/5/2017 0:00,13 +8/5/2017 1:00,12 +8/5/2017 2:00,11 +8/5/2017 3:00,9 +8/5/2017 4:00,7 +8/5/2017 5:00,6 +8/5/2017 6:00,9 +8/5/2017 7:00,14 +8/5/2017 8:00,16 +8/5/2017 9:00,18 +8/5/2017 10:00,19 +8/5/2017 11:00,21 +8/5/2017 12:00,22 +8/5/2017 13:00,23.3 +8/5/2017 14:00,24 +8/5/2017 15:00,25 +8/5/2017 16:00,25 +8/5/2017 17:00,24 +8/5/2017 18:00,24 +8/5/2017 19:00,23 +8/5/2017 20:00,22 +8/5/2017 21:00,19 +8/5/2017 22:00,16 +8/5/2017 23:00,15 +8/6/2017 0:00,13 +8/6/2017 1:00,15 +8/6/2017 2:00,16 +8/6/2017 3:00,14 +8/6/2017 4:00,14 +8/6/2017 5:00,15 +8/6/2017 6:00,15 +8/6/2017 7:00,16 +8/6/2017 8:00,17 +8/6/2017 9:00,18 +8/6/2017 10:00,20 +8/6/2017 11:00,21 +8/6/2017 12:00,22 +8/6/2017 13:00,23 +8/6/2017 14:00,24 +8/6/2017 15:00,25 +8/6/2017 16:00,25 +8/6/2017 17:00,25 +8/6/2017 18:00,24 +8/6/2017 19:00,23 +8/6/2017 20:00,18 +8/6/2017 21:00,17 +8/6/2017 22:00,16 +8/6/2017 23:00,15 +8/7/2017 0:00,14 +8/7/2017 1:00,13 +8/7/2017 2:00,13 +8/7/2017 3:00,10.2 +8/7/2017 4:00,8 +8/7/2017 5:00,7 +8/7/2017 6:00,11 +8/7/2017 7:00,14 +8/7/2017 8:00,17 +8/7/2017 9:00,19 +8/7/2017 10:00,21 +8/7/2017 11:00,22 +8/7/2017 12:00,24 +8/7/2017 13:00,24 +8/7/2017 14:00,24 +8/7/2017 15:00,26 +8/7/2017 16:00,26 +8/7/2017 17:00,27 +8/7/2017 18:00,26 +8/7/2017 19:00,26 +8/7/2017 20:00,23 +8/7/2017 21:00,19 +8/7/2017 22:00,17 +8/7/2017 23:00,13 +8/8/2017 0:00,13 +8/8/2017 1:00,13 +8/8/2017 2:00,13 +8/8/2017 3:00,9 +8/8/2017 4:00,8 +8/8/2017 5:00,8 +8/8/2017 6:00,10 +8/8/2017 7:00,16 +8/8/2017 8:00,19 +8/8/2017 9:00,22 +8/8/2017 10:00,24 +8/8/2017 11:00,26 +8/8/2017 12:00,27 +8/8/2017 13:00,28 +8/8/2017 14:00,29 +8/8/2017 15:00,30 +8/8/2017 16:00,29 +8/8/2017 17:00,30 +8/8/2017 18:00,29 +8/8/2017 19:00,28 +8/8/2017 20:00,26 +8/8/2017 21:00,23 +8/8/2017 22:00,16 +8/8/2017 23:00,13 +8/9/2017 0:00,16 +8/9/2017 1:00,12 +8/9/2017 2:00,11 +8/9/2017 3:00,11 +8/9/2017 4:00,10 +8/9/2017 5:00,9 +8/9/2017 6:00,12 +8/9/2017 7:00,18 +8/9/2017 8:00,23 +8/9/2017 9:00,25 +8/9/2017 10:00,28 +8/9/2017 11:00,29 +8/9/2017 12:00,31 +8/9/2017 13:00,34 +8/9/2017 14:00,34 +8/9/2017 15:00,34 +8/9/2017 16:00,34 +8/9/2017 17:00,34 +8/9/2017 18:00,34 +8/9/2017 19:00,33 +8/9/2017 20:00,29 +8/9/2017 21:00,25 +8/9/2017 22:00,24 +8/9/2017 23:00,24 +8/10/2017 0:00,24 +8/10/2017 1:00,21 +8/10/2017 2:00,19 +8/10/2017 3:00,18 +8/10/2017 4:00,17 +8/10/2017 5:00,16 +8/10/2017 6:00,18 +8/10/2017 7:00,23 +8/10/2017 8:00,25 +8/10/2017 9:00,27 +8/10/2017 10:00,28 +8/10/2017 11:00,29 +8/10/2017 12:00,30 +8/10/2017 13:00,31 +8/10/2017 14:00,32 +8/10/2017 15:00,33 +8/10/2017 16:00,34 +8/10/2017 17:00,34 +8/10/2017 18:00,33 +8/10/2017 19:00,29 +8/10/2017 20:00,24 +8/10/2017 21:00,25 +8/10/2017 22:00,24 +8/10/2017 23:00,21 +8/11/2017 0:00,21 +8/11/2017 1:00,19 +8/11/2017 2:00,16 +8/11/2017 3:00,15 +8/11/2017 4:00,12 +8/11/2017 5:00,13 +8/11/2017 6:00,13 +8/11/2017 7:00,19 +8/11/2017 8:00,23 +8/11/2017 9:00,25 +8/11/2017 10:00,27 +8/11/2017 11:00,28 +8/11/2017 12:00,29 +8/11/2017 13:00,29 +8/11/2017 14:00,31 +8/11/2017 15:00,32 +8/11/2017 16:00,31 +8/11/2017 17:00,31 +8/11/2017 18:00,31 +8/11/2017 19:00,28 +8/11/2017 20:00,23 +8/11/2017 21:00,20 +8/11/2017 22:00,17 +8/11/2017 23:00,16 +8/12/2017 0:00,15 +8/12/2017 1:00,17 +8/12/2017 2:00,14 +8/12/2017 3:00,12 +8/12/2017 4:00,12 +8/12/2017 5:00,11 +8/12/2017 6:00,14 +8/12/2017 7:00,17 +8/12/2017 8:00,21 +8/12/2017 9:00,24 +8/12/2017 10:00,27 +8/12/2017 11:00,29 +8/12/2017 12:00,31 +8/12/2017 13:00,32 +8/12/2017 14:00,33 +8/12/2017 15:00,33 +8/12/2017 16:00,33 +8/12/2017 17:00,33 +8/12/2017 18:00,32 +8/12/2017 19:00,30 +8/12/2017 20:00,26 +8/12/2017 21:00,26 +8/12/2017 22:00,23 +8/12/2017 23:00,19 +8/13/2017 0:00,16 +8/13/2017 1:00,15 +8/13/2017 2:00,14 +8/13/2017 3:00,13 +8/13/2017 4:00,13 +8/13/2017 5:00,12 +8/13/2017 6:00,14 +8/13/2017 7:00,19 +8/13/2017 8:00,23 +8/13/2017 9:00,27 +8/13/2017 10:00,29 +8/13/2017 11:00,31 +8/13/2017 12:00,33 +8/13/2017 13:00,34 +8/13/2017 14:00,35 +8/13/2017 15:00,36 +8/13/2017 16:00,37 +8/13/2017 17:00,37 +8/13/2017 18:00,36 +8/13/2017 19:00,33 +8/13/2017 20:00,29 +8/13/2017 21:00,26 +8/13/2017 22:00,21 +8/13/2017 23:00,17 +8/14/2017 0:00,19 +8/14/2017 1:00,15 +8/14/2017 2:00,17 +8/14/2017 3:00,16 +8/14/2017 4:00,16 +8/14/2017 5:00,16 +8/14/2017 6:00,17 +8/14/2017 7:00,24 +8/14/2017 8:00,28 +8/14/2017 9:00,32 +8/14/2017 10:00,33 +8/14/2017 11:00,35 +8/14/2017 12:00,36 +8/14/2017 13:00,37 +8/14/2017 14:00,37 +8/14/2017 15:00,37 +8/14/2017 16:00,38 +8/14/2017 17:00,37 +8/14/2017 18:00,36 +8/14/2017 19:00,31 +8/14/2017 20:00,26 +8/14/2017 21:00,26 +8/14/2017 22:00,23.5 +8/14/2017 23:00,22 +8/15/2017 0:00,18 +8/15/2017 1:00,18 +8/15/2017 2:00,18 +8/15/2017 3:00,17 +8/15/2017 4:00,13 +8/15/2017 5:00,13 +8/15/2017 6:00,14 +8/15/2017 7:00,18 +8/15/2017 8:00,21 +8/15/2017 9:00,23 +8/15/2017 10:00,27 +8/15/2017 11:00,29 +8/15/2017 12:00,30 +8/15/2017 13:00,32 +8/15/2017 14:00,33 +8/15/2017 15:00,33 +8/15/2017 16:00,34 +8/15/2017 17:00,34 +8/15/2017 18:00,34 +8/15/2017 19:00,30.2 +8/15/2017 20:00,26 +8/15/2017 21:00,26 +8/15/2017 22:00,21 +8/15/2017 23:00,23 +8/16/2017 0:00,23 +8/16/2017 1:00,22 +8/16/2017 2:00,19 +8/16/2017 3:00,18 +8/16/2017 4:00,16 +8/16/2017 5:00,14 +8/16/2017 6:00,18 +8/16/2017 7:00,19 +8/16/2017 8:00,22 +8/16/2017 9:00,24 +8/16/2017 10:00,26 +8/16/2017 11:00,27 +8/16/2017 12:00,28 +8/16/2017 13:00,29 +8/16/2017 14:00,29 +8/16/2017 15:00,31 +8/16/2017 16:00,29 +8/16/2017 17:00,29 +8/16/2017 18:00,27 +8/16/2017 19:00,23 +8/16/2017 20:00,19 +8/16/2017 21:00,14 +8/16/2017 22:00,13 +8/16/2017 23:00,14 +8/17/2017 0:00,11 +8/17/2017 1:00,11 +8/17/2017 2:00,10 +8/17/2017 3:00,10 +8/17/2017 4:00,9 +8/17/2017 5:00,8 +8/17/2017 6:00,10 +8/17/2017 7:00,14 +8/17/2017 8:00,17 +8/17/2017 9:00,21 +8/17/2017 10:00,24 +8/17/2017 11:00,28 +8/17/2017 12:00,30 +8/17/2017 13:00,33 +8/17/2017 14:00,33 +8/17/2017 15:00,34 +8/17/2017 16:00,35 +8/17/2017 17:00,33 +8/17/2017 18:00,29 +8/17/2017 19:00,27 +8/17/2017 20:00,23 +8/17/2017 21:00,22 +8/17/2017 22:00,20 +8/17/2017 23:00,19 +8/18/2017 0:00,16 +8/18/2017 1:00,19 +8/18/2017 2:00,17 +8/18/2017 3:00,12 +8/18/2017 4:00,11 +8/18/2017 5:00,9 +8/18/2017 6:00,11 +8/18/2017 7:00,15 +8/18/2017 8:00,19 +8/18/2017 9:00,22 +8/18/2017 10:00,24 +8/18/2017 11:00,26 +8/18/2017 12:00,27 +8/18/2017 13:00,28 +8/18/2017 14:00,29 +8/18/2017 15:00,29 +8/18/2017 16:00,29 +8/18/2017 17:00,26 +8/18/2017 18:00,26 +8/18/2017 19:00,22 +8/18/2017 20:00,20 +8/18/2017 21:00,16 +8/18/2017 22:00,14 +8/18/2017 23:00,13 +8/19/2017 0:00,11 +8/19/2017 1:00,11 +8/19/2017 2:00,10 +8/19/2017 3:00,9 +8/19/2017 4:00,9 +8/19/2017 5:00,9 +8/19/2017 6:00,11 +8/19/2017 7:00,16 +8/19/2017 8:00,19 +8/19/2017 9:00,22 +8/19/2017 10:00,24 +8/19/2017 11:00,27 +8/19/2017 12:00,27 +8/19/2017 13:00,29 +8/19/2017 14:00,29 +8/19/2017 15:00,31 +8/19/2017 16:00,31 +8/19/2017 17:00,31 +8/19/2017 18:00,29 +8/19/2017 19:00,26 +8/19/2017 20:00,24 +8/19/2017 21:00,23 +8/19/2017 22:00,21 +8/19/2017 23:00,20 +8/20/2017 0:00,19 +8/20/2017 1:00,18 +8/20/2017 2:00,17 +8/20/2017 3:00,17 +8/20/2017 4:00,16 +8/20/2017 5:00,15 +8/20/2017 6:00,17 +8/20/2017 7:00,18 +8/20/2017 8:00,20 +8/20/2017 9:00,23 +8/20/2017 10:00,24 +8/20/2017 11:00,24 +8/20/2017 12:00,26 +8/20/2017 13:00,26 +8/20/2017 14:00,26 +8/20/2017 15:00,27 +8/20/2017 16:00,28 +8/20/2017 17:00,27 +8/20/2017 18:00,27 +8/20/2017 19:00,24 +8/20/2017 20:00,21 +8/20/2017 21:00,18 +8/20/2017 22:00,19 +8/20/2017 23:00,19 +8/21/2017 0:00,18 +8/21/2017 1:00,18 +8/21/2017 2:00,16 +8/21/2017 3:00,15 +8/21/2017 4:00,14 +8/21/2017 5:00,13.3 +8/21/2017 6:00,14 +8/21/2017 7:00,15 +8/21/2017 8:00,17.3 +8/21/2017 9:00,19 +8/21/2017 10:00,21 +8/21/2017 11:00,21 +8/21/2017 12:00,22 +8/21/2017 13:00,23 +8/21/2017 14:00,24 +8/21/2017 15:00,25 +8/21/2017 16:00,25 +8/21/2017 17:00,26 +8/21/2017 18:00,25 +8/21/2017 19:00,23 +8/21/2017 20:00,22 +8/21/2017 21:00,19 +8/21/2017 22:00,20 +8/21/2017 23:00,19 +8/22/2017 0:00,17 +8/22/2017 1:00,15 +8/22/2017 2:00,13 +8/22/2017 3:00,15 +8/22/2017 4:00,15 +8/22/2017 5:00,16 +8/22/2017 6:00,17 +8/22/2017 7:00,17 +8/22/2017 8:00,18 +8/22/2017 9:00,19 +8/22/2017 10:00,22 +8/22/2017 11:00,24 +8/22/2017 12:00,26 +8/22/2017 13:00,27 +8/22/2017 14:00,28 +8/22/2017 15:00,28 +8/22/2017 16:00,29 +8/22/2017 17:00,28 +8/22/2017 18:00,28 +8/22/2017 19:00,24 +8/22/2017 20:00,23 +8/22/2017 21:00,21 +8/22/2017 22:00,17 +8/22/2017 23:00,18 +8/23/2017 0:00,16 +8/23/2017 1:00,16 +8/23/2017 2:00,17 +8/23/2017 3:00,16 +8/23/2017 4:00,14 +8/23/2017 5:00,13 +8/23/2017 6:00,14 +8/23/2017 7:00,17 +8/23/2017 8:00,19 +8/23/2017 9:00,22 +8/23/2017 10:00,23 +8/23/2017 11:00,24 +8/23/2017 12:00,27 +8/23/2017 13:00,29 +8/23/2017 14:00,29 +8/23/2017 15:00,31 +8/23/2017 16:00,31 +8/23/2017 17:00,30 +8/23/2017 18:00,28 +8/23/2017 19:00,24 +8/23/2017 20:00,20 +8/23/2017 21:00,19 +8/23/2017 22:00,18 +8/23/2017 23:00,17 +8/24/2017 0:00,15 +8/24/2017 1:00,13 +8/24/2017 2:00,14 +8/24/2017 3:00,16 +8/24/2017 4:00,18 +8/24/2017 5:00,15 +8/24/2017 6:00,16 +8/24/2017 7:00,19 +8/24/2017 8:00,22 +8/24/2017 9:00,24 +8/24/2017 10:00,26 +8/24/2017 11:00,28 +8/24/2017 12:00,29 +8/24/2017 13:00,31 +8/24/2017 14:00,31 +8/24/2017 15:00,32 +8/24/2017 16:00,32 +8/24/2017 17:00,31 +8/24/2017 18:00,27 +8/24/2017 19:00,23 +8/24/2017 20:00,22 +8/24/2017 21:00,20 +8/24/2017 22:00,20 +8/24/2017 23:00,18 +8/25/2017 0:00,18 +8/25/2017 1:00,14 +8/25/2017 2:00,15 +8/25/2017 3:00,15 +8/25/2017 4:00,16 +8/25/2017 5:00,17 +8/25/2017 6:00,16 +8/25/2017 7:00,19 +8/25/2017 8:00,24 +8/25/2017 9:00,26 +8/25/2017 10:00,28 +8/25/2017 11:00,29 +8/25/2017 12:00,31 +8/25/2017 13:00,33 +8/25/2017 14:00,33 +8/25/2017 15:00,33 +8/25/2017 16:00,32 +8/25/2017 17:00,32 +8/25/2017 18:00,32 +8/25/2017 19:00,28 +8/25/2017 20:00,24 +8/25/2017 21:00,23 +8/25/2017 22:00,23 +8/25/2017 23:00,21 +8/26/2017 0:00,21 +8/26/2017 1:00,18 +8/26/2017 2:00,19 +8/26/2017 3:00,18 +8/26/2017 4:00,18 +8/26/2017 5:00,17 +8/26/2017 6:00,17 +8/26/2017 7:00,19 +8/26/2017 8:00,21 +8/26/2017 9:00,23 +8/26/2017 10:00,24 +8/26/2017 11:00,25 +8/26/2017 12:00,27 +8/26/2017 13:00,28 +8/26/2017 14:00,29 +8/26/2017 15:00,29 +8/26/2017 16:00,30 +8/26/2017 17:00,29 +8/26/2017 18:00,29 +8/26/2017 19:00,27 +8/26/2017 20:00,27 +8/26/2017 21:00,25 +8/26/2017 22:00,21 +8/26/2017 23:00,19 +8/27/2017 0:00,21 +8/27/2017 1:00,18 +8/27/2017 2:00,17 +8/27/2017 3:00,16 +8/27/2017 4:00,16 +8/27/2017 5:00,16 +8/27/2017 6:00,13 +8/27/2017 7:00,17 +8/27/2017 8:00,21 +8/27/2017 9:00,22 +8/27/2017 10:00,24 +8/27/2017 11:00,24 +8/27/2017 12:00,27 +8/27/2017 13:00,29 +8/27/2017 14:00,31 +8/27/2017 15:00,32 +8/27/2017 16:00,32 +8/27/2017 17:00,31 +8/27/2017 18:00,29 +8/27/2017 19:00,26 +8/27/2017 20:00,21 +8/27/2017 21:00,19 +8/27/2017 22:00,19 +8/27/2017 23:00,17 +8/28/2017 0:00,19 +8/28/2017 1:00,16 +8/28/2017 2:00,17 +8/28/2017 3:00,17 +8/28/2017 4:00,16 +8/28/2017 5:00,16 +8/28/2017 6:00,16 +8/28/2017 7:00,19 +8/28/2017 8:00,22 +8/28/2017 9:00,24 +8/28/2017 10:00,26 +8/28/2017 11:00,29 +8/28/2017 12:00,31 +8/28/2017 13:00,33 +8/28/2017 14:00,34 +8/28/2017 15:00,34 +8/28/2017 16:00,34 +8/28/2017 17:00,33 +8/28/2017 18:00,31 +8/28/2017 19:00,27 +8/28/2017 20:00,24 +8/28/2017 21:00,22 +8/28/2017 22:00,19 +8/28/2017 23:00,19 +8/29/2017 0:00,17 +8/29/2017 1:00,16 +8/29/2017 2:00,17 +8/29/2017 3:00,15 +8/29/2017 4:00,14 +8/29/2017 5:00,14 +8/29/2017 6:00,14 +8/29/2017 7:00,21 +8/29/2017 8:00,24 +8/29/2017 9:00,27 +8/29/2017 10:00,29.7 +8/29/2017 11:00,32 +8/29/2017 12:00,33 +8/29/2017 13:00,35 +8/29/2017 14:00,36 +8/29/2017 15:00,36 +8/29/2017 16:00,35 +8/29/2017 17:00,35 +8/29/2017 18:00,34 +8/29/2017 19:00,31 +8/29/2017 20:00,28 +8/29/2017 21:00,26 +8/29/2017 22:00,24 +8/29/2017 23:00,23 +8/30/2017 0:00,22 +8/30/2017 1:00,21 +8/30/2017 2:00,21 +8/30/2017 3:00,21 +8/30/2017 4:00,20 +8/30/2017 5:00,19 +8/30/2017 6:00,18 +8/30/2017 7:00,19 +8/30/2017 8:00,20 +8/30/2017 9:00,22 +8/30/2017 10:00,24 +8/30/2017 11:00,26 +8/30/2017 12:00,27 +8/30/2017 13:00,28 +8/30/2017 14:00,30 +8/30/2017 15:00,31 +8/30/2017 16:00,31 +8/30/2017 17:00,31 +8/30/2017 18:00,30 +8/30/2017 19:00,27 +8/30/2017 20:00,25 +8/30/2017 21:00,23 +8/30/2017 22:00,18 +8/30/2017 23:00,21 +8/31/2017 0:00,19 +8/31/2017 1:00,17 +8/31/2017 2:00,18 +8/31/2017 3:00,17 +8/31/2017 4:00,17.6 +8/31/2017 5:00,18 +8/31/2017 6:00,18 +8/31/2017 7:00,19 +8/31/2017 8:00,21 +8/31/2017 9:00,23 +8/31/2017 10:00,25 +8/31/2017 11:00,27 +8/31/2017 12:00,28 +8/31/2017 13:00,29 +8/31/2017 14:00,31 +8/31/2017 15:00,32 +8/31/2017 16:00,32 +8/31/2017 17:00,31 +8/31/2017 18:00,31 +8/31/2017 19:00,29 +8/31/2017 20:00,27 +8/31/2017 21:00,23 +8/31/2017 22:00,20.3 +8/31/2017 23:00,17.6 +9/1/2017 0:00,14.9 +9/1/2017 1:00,12.1 +9/1/2017 2:00,9.4 +9/1/2017 3:00,6.7 +9/1/2017 4:00,4 +9/1/2017 5:00,4 +9/1/2017 6:00,4 +9/1/2017 7:00,8 +9/1/2017 8:00,11 +9/1/2017 9:00,14 +9/1/2017 10:00,17 +9/1/2017 11:00,19 +9/1/2017 12:00,21 +9/1/2017 13:00,22 +9/1/2017 14:00,23 +9/1/2017 15:00,23 +9/1/2017 16:00,23 +9/1/2017 17:00,22 +9/1/2017 18:00,19 +9/1/2017 19:00,14 +9/1/2017 20:00,11 +9/1/2017 21:00,10 +9/1/2017 22:00,11 +9/1/2017 23:00,9 +9/2/2017 0:00,7 +9/2/2017 1:00,8 +9/2/2017 2:00,7 +9/2/2017 3:00,6 +9/2/2017 4:00,6 +9/2/2017 5:00,5 +9/2/2017 6:00,6 +9/2/2017 7:00,9 +9/2/2017 8:00,13 +9/2/2017 9:00,16 +9/2/2017 10:00,18 +9/2/2017 11:00,21 +9/2/2017 12:00,23 +9/2/2017 13:00,24 +9/2/2017 14:00,24 +9/2/2017 15:00,23 +9/2/2017 16:00,21 +9/2/2017 17:00,21 +9/2/2017 18:00,21 +9/2/2017 19:00,17 +9/2/2017 20:00,13 +9/2/2017 21:00,12 +9/2/2017 22:00,12 +9/2/2017 23:00,8 +9/3/2017 0:00,7 +9/3/2017 1:00,6 +9/3/2017 2:00,6 +9/3/2017 3:00,6 +9/3/2017 4:00,6 +9/3/2017 5:00,5 +9/3/2017 6:00,6 +9/3/2017 7:00,9 +9/3/2017 8:00,13 +9/3/2017 9:00,16 +9/3/2017 10:00,18 +9/3/2017 11:00,20 +9/3/2017 12:00,22 +9/3/2017 13:00,23 +9/3/2017 14:00,24 +9/3/2017 15:00,26 +9/3/2017 16:00,26 +9/3/2017 17:00,25 +9/3/2017 18:00,25 +9/3/2017 19:00,21 +9/3/2017 20:00,17 +9/3/2017 21:00,17 +9/3/2017 22:00,12 +9/3/2017 23:00,12 +9/4/2017 0:00,12 +9/4/2017 1:00,12 +9/4/2017 2:00,8 +9/4/2017 3:00,8 +9/4/2017 4:00,6 +9/4/2017 5:00,6 +9/4/2017 6:00,7 +9/4/2017 7:00,10 +9/4/2017 8:00,17 +9/4/2017 9:00,19 +9/4/2017 10:00,21 +9/4/2017 11:00,22 +9/4/2017 12:00,24 +9/4/2017 13:00,26 +9/4/2017 14:00,27 +9/4/2017 15:00,27 +9/4/2017 16:00,28 +9/4/2017 17:00,28 +9/4/2017 18:00,26.5 +9/4/2017 19:00,24 +9/4/2017 20:00,21 +9/4/2017 21:00,16 +9/4/2017 22:00,15 +9/4/2017 23:00,12 +9/5/2017 0:00,11 +9/5/2017 1:00,10 +9/5/2017 2:00,11 +9/5/2017 3:00,8 +9/5/2017 4:00,8 +9/5/2017 5:00,7 +9/5/2017 6:00,9 +9/5/2017 7:00,13 +9/5/2017 8:00,18 +9/5/2017 9:00,23 +9/5/2017 10:00,25 +9/5/2017 11:00,28 +9/5/2017 12:00,30 +9/5/2017 13:00,32 +9/5/2017 14:00,32 +9/5/2017 15:00,31 +9/5/2017 16:00,29 +9/5/2017 17:00,29 +9/5/2017 18:00,26 +9/5/2017 19:00,24 +9/5/2017 20:00,22 +9/5/2017 21:00,21 +9/5/2017 22:00,19 +9/5/2017 23:00,17 +9/6/2017 0:00,16 +9/6/2017 1:00,16 +9/6/2017 2:00,15 +9/6/2017 3:00,14 +9/6/2017 4:00,13 +9/6/2017 5:00,13 +9/6/2017 6:00,13 +9/6/2017 7:00,16 +9/6/2017 8:00,18 +9/6/2017 9:00,20 +9/6/2017 10:00,21 +9/6/2017 11:00,22 +9/6/2017 12:00,23 +9/6/2017 13:00,24 +9/6/2017 14:00,26 +9/6/2017 15:00,26 +9/6/2017 16:00,24 +9/6/2017 17:00,23 +9/6/2017 18:00,20 +9/6/2017 19:00,17 +9/6/2017 20:00,17 +9/6/2017 21:00,16 +9/6/2017 22:00,12 +9/6/2017 23:00,11 +9/7/2017 0:00,13 +9/7/2017 1:00,9 +9/7/2017 2:00,9 +9/7/2017 3:00,7 +9/7/2017 4:00,7 +9/7/2017 5:00,5 +9/7/2017 6:00,4 +9/7/2017 7:00,10 +9/7/2017 8:00,13 +9/7/2017 9:00,16 +9/7/2017 10:00,18 +9/7/2017 11:00,20 +9/7/2017 12:00,21 +9/7/2017 13:00,22 +9/7/2017 14:00,23 +9/7/2017 15:00,23 +9/7/2017 16:00,24 +9/7/2017 17:00,23 +9/7/2017 18:00,21 +9/7/2017 19:00,14 +9/7/2017 20:00,11 +9/7/2017 21:00,12 +9/7/2017 22:00,11 +9/7/2017 23:00,11 +9/8/2017 0:00,8 +9/8/2017 1:00,8 +9/8/2017 2:00,8 +9/8/2017 3:00,7 +9/8/2017 4:00,8 +9/8/2017 5:00,8 +9/8/2017 6:00,6 +9/8/2017 7:00,9 +9/8/2017 8:00,12 +9/8/2017 9:00,16 +9/8/2017 10:00,18 +9/8/2017 11:00,21 +9/8/2017 12:00,23 +9/8/2017 13:00,26 +9/8/2017 14:00,27 +9/8/2017 15:00,28 +9/8/2017 16:00,28 +9/8/2017 17:00,27 +9/8/2017 18:00,24 +9/8/2017 19:00,19 +9/8/2017 20:00,17 +9/8/2017 21:00,14 +9/8/2017 22:00,13 +9/8/2017 23:00,13 +9/9/2017 0:00,9 +9/9/2017 1:00,9 +9/9/2017 2:00,8 +9/9/2017 3:00,7 +9/9/2017 4:00,7 +9/9/2017 5:00,6 +9/9/2017 6:00,5 +9/9/2017 7:00,10 +9/9/2017 8:00,14 +9/9/2017 9:00,18 +9/9/2017 10:00,21 +9/9/2017 11:00,23 +9/9/2017 12:00,26 +9/9/2017 13:00,27 +9/9/2017 14:00,28 +9/9/2017 15:00,30 +9/9/2017 16:00,29 +9/9/2017 17:00,29 +9/9/2017 18:00,27 +9/9/2017 19:00,21 +9/9/2017 20:00,22 +9/9/2017 21:00,21 +9/9/2017 22:00,18 +9/9/2017 23:00,17 +9/10/2017 0:00,14 +9/10/2017 1:00,12 +9/10/2017 2:00,10 +9/10/2017 3:00,8 +9/10/2017 4:00,7 +9/10/2017 5:00,6 +9/10/2017 6:00,7 +9/10/2017 7:00,11 +9/10/2017 8:00,16 +9/10/2017 9:00,18 +9/10/2017 10:00,21 +9/10/2017 11:00,22 +9/10/2017 12:00,24 +9/10/2017 13:00,25 +9/10/2017 14:00,26 +9/10/2017 15:00,26 +9/10/2017 16:00,26 +9/10/2017 17:00,26 +9/10/2017 18:00,24 +9/10/2017 19:00,18 +9/10/2017 20:00,16 +9/10/2017 21:00,12 +9/10/2017 22:00,11 +9/10/2017 23:00,9 +9/11/2017 0:00,8 +9/11/2017 1:00,9 +9/11/2017 2:00,9 +9/11/2017 3:00,8 +9/11/2017 4:00,8 +9/11/2017 5:00,7 +9/11/2017 6:00,6 +9/11/2017 7:00,10 +9/11/2017 8:00,14 +9/11/2017 9:00,18 +9/11/2017 10:00,20 +9/11/2017 11:00,22 +9/11/2017 12:00,23 +9/11/2017 13:00,24 +9/11/2017 14:00,24 +9/11/2017 15:00,25 +9/11/2017 16:00,24 +9/11/2017 17:00,23 +9/11/2017 18:00,21 +9/11/2017 19:00,16 +9/11/2017 20:00,13 +9/11/2017 21:00,12 +9/11/2017 22:00,11 +9/11/2017 23:00,9 +9/12/2017 0:00,8 +9/12/2017 1:00,7 +9/12/2017 2:00,7 +9/12/2017 3:00,6 +9/12/2017 4:00,4 +9/12/2017 5:00,7 +9/12/2017 6:00,5 +9/12/2017 7:00,9 +9/12/2017 8:00,13 +9/12/2017 9:00,17 +9/12/2017 10:00,19 +9/12/2017 11:00,22 +9/12/2017 12:00,24 +9/12/2017 13:00,26 +9/12/2017 14:00,27 +9/12/2017 15:00,28 +9/12/2017 16:00,28 +9/12/2017 17:00,26 +9/12/2017 18:00,21 +9/12/2017 19:00,16 +9/12/2017 20:00,14 +9/12/2017 21:00,15 +9/12/2017 22:00,13 +9/12/2017 23:00,11.2 +9/13/2017 0:00,9 +9/13/2017 1:00,8 +9/13/2017 2:00,9.6 +9/13/2017 3:00,11 +9/13/2017 4:00,10.7 +9/13/2017 5:00,10.1 +9/13/2017 6:00,9.5 +9/13/2017 7:00,11.8 +9/13/2017 8:00,15 +9/13/2017 9:00,18 +9/13/2017 10:00,20 +9/13/2017 11:00,22 +9/13/2017 12:00,25 +9/13/2017 13:00,27 +9/13/2017 14:00,28 +9/13/2017 15:00,29 +9/13/2017 16:00,29 +9/13/2017 17:00,28 +9/13/2017 18:00,26 +9/13/2017 19:00,23 +9/13/2017 20:00,17 +9/13/2017 21:00,14 +9/13/2017 22:00,13 +9/13/2017 23:00,12 +9/14/2017 0:00,11 +9/14/2017 1:00,10 +9/14/2017 2:00,8 +9/14/2017 3:00,9 +9/14/2017 4:00,7 +9/14/2017 5:00,7 +9/14/2017 6:00,7 +9/14/2017 7:00,10 +9/14/2017 8:00,14 +9/14/2017 9:00,18 +9/14/2017 10:00,22 +9/14/2017 11:00,24 +9/14/2017 12:00,26 +9/14/2017 13:00,28 +9/14/2017 14:00,28 +9/14/2017 15:00,30 +9/14/2017 16:00,30 +9/14/2017 17:00,30 +9/14/2017 18:00,25 +9/14/2017 19:00,20 +9/14/2017 20:00,16 +9/14/2017 21:00,14 +9/14/2017 22:00,13 +9/14/2017 23:00,13 +9/15/2017 0:00,11 +9/15/2017 1:00,10 +9/15/2017 2:00,10 +9/15/2017 3:00,8 +9/15/2017 4:00,8 +9/15/2017 5:00,7 +9/15/2017 6:00,7 +9/15/2017 7:00,11 +9/15/2017 8:00,16 +9/15/2017 9:00,19 +9/15/2017 10:00,23 +9/15/2017 11:00,26 +9/15/2017 12:00,27 +9/15/2017 13:00,30 +9/15/2017 14:00,31 +9/15/2017 15:00,32 +9/15/2017 16:00,32 +9/15/2017 17:00,31 +9/15/2017 18:00,28 +9/15/2017 19:00,20 +9/15/2017 20:00,18 +9/15/2017 21:00,14 +9/15/2017 22:00,13 +9/15/2017 23:00,14 +9/16/2017 0:00,13 +9/16/2017 1:00,10 +9/16/2017 2:00,9 +9/16/2017 3:00,9 +9/16/2017 4:00,8 +9/16/2017 5:00,8 +9/16/2017 6:00,8 +9/16/2017 7:00,12 +9/16/2017 8:00,18 +9/16/2017 9:00,21 +9/16/2017 10:00,23 +9/16/2017 11:00,26 +9/16/2017 12:00,26 +9/16/2017 13:00,28 +9/16/2017 14:00,28 +9/16/2017 15:00,29 +9/16/2017 16:00,29 +9/16/2017 17:00,28 +9/16/2017 18:00,26 +9/16/2017 19:00,23 +9/16/2017 20:00,17 +9/16/2017 21:00,14 +9/16/2017 22:00,12 +9/16/2017 23:00,12 +9/17/2017 0:00,10 +9/17/2017 1:00,9 +9/17/2017 2:00,9 +9/17/2017 3:00,8 +9/17/2017 4:00,8 +9/17/2017 5:00,7 +9/17/2017 6:00,7 +9/17/2017 7:00,10 +9/17/2017 8:00,16 +9/17/2017 9:00,19 +9/17/2017 10:00,22 +9/17/2017 11:00,24 +9/17/2017 12:00,26 +9/17/2017 13:00,26 +9/17/2017 14:00,27 +9/17/2017 15:00,28 +9/17/2017 16:00,28 +9/17/2017 17:00,27 +9/17/2017 18:00,26 +9/17/2017 19:00,23 +9/17/2017 20:00,16 +9/17/2017 21:00,14 +9/17/2017 22:00,14 +9/17/2017 23:00,12 +9/18/2017 0:00,11 +9/18/2017 1:00,10 +9/18/2017 2:00,8 +9/18/2017 3:00,9 +9/18/2017 4:00,8 +9/18/2017 5:00,8 +9/18/2017 6:00,7 +9/18/2017 7:00,11 +9/18/2017 8:00,14 +9/18/2017 9:00,18 +9/18/2017 10:00,22 +9/18/2017 11:00,24 +9/18/2017 12:00,26 +9/18/2017 13:00,28 +9/18/2017 14:00,29 +9/18/2017 15:00,31 +9/18/2017 16:00,31 +9/18/2017 17:00,31 +9/18/2017 18:00,28 +9/18/2017 19:00,21 +9/18/2017 20:00,18 +9/18/2017 21:00,16 +9/18/2017 22:00,14 +9/18/2017 23:00,16 +9/19/2017 0:00,12 +9/19/2017 1:00,11 +9/19/2017 2:00,11 +9/19/2017 3:00,10 +9/19/2017 4:00,10 +9/19/2017 5:00,11 +9/19/2017 6:00,12 +9/19/2017 7:00,15 +9/19/2017 8:00,18 +9/19/2017 9:00,20 +9/19/2017 10:00,23 +9/19/2017 11:00,25 +9/19/2017 12:00,27 +9/19/2017 13:00,28 +9/19/2017 14:00,28 +9/19/2017 15:00,28 +9/19/2017 16:00,28 +9/19/2017 17:00,26 +9/19/2017 18:00,21 +9/19/2017 19:00,16 +9/19/2017 20:00,19 +9/19/2017 21:00,13 +9/19/2017 22:00,11 +9/19/2017 23:00,11 +9/20/2017 0:00,9 +9/20/2017 1:00,8 +9/20/2017 2:00,6 +9/20/2017 3:00,6 +9/20/2017 4:00,4 +9/20/2017 5:00,3 +9/20/2017 6:00,4 +9/20/2017 7:00,7 +9/20/2017 8:00,12 +9/20/2017 9:00,16 +9/20/2017 10:00,19 +9/20/2017 11:00,23 +9/20/2017 12:00,26 +9/20/2017 13:00,27 +9/20/2017 14:00,28 +9/20/2017 15:00,29 +9/20/2017 16:00,29 +9/20/2017 17:00,28 +9/20/2017 18:00,23 +9/20/2017 19:00,17 +9/20/2017 20:00,13 +9/20/2017 21:00,12 +9/20/2017 22:00,12 +9/20/2017 23:00,9 +9/21/2017 0:00,8 +9/21/2017 1:00,9 +9/21/2017 2:00,8 +9/21/2017 3:00,8 +9/21/2017 4:00,6 +9/21/2017 5:00,6 +9/21/2017 6:00,6 +9/21/2017 7:00,8 +9/21/2017 8:00,13 +9/21/2017 9:00,16 +9/21/2017 10:00,19 +9/21/2017 11:00,22 +9/21/2017 12:00,24.6 +9/21/2017 13:00,27 +9/21/2017 14:00,29 +9/21/2017 15:00,29 +9/21/2017 16:00,29 +9/21/2017 17:00,30 +9/21/2017 18:00,24 +9/21/2017 19:00,19 +9/21/2017 20:00,14 +9/21/2017 21:00,13 +9/21/2017 22:00,13 +9/21/2017 23:00,12 +9/22/2017 0:00,11 +9/22/2017 1:00,9 +9/22/2017 2:00,10 +9/22/2017 3:00,8 +9/22/2017 4:00,7 +9/22/2017 5:00,8 +9/22/2017 6:00,7 +9/22/2017 7:00,10 +9/22/2017 8:00,15 +9/22/2017 9:00,19 +9/22/2017 10:00,23 +9/22/2017 11:00,26 +9/22/2017 12:00,28 +9/22/2017 13:00,30 +9/22/2017 14:00,31 +9/22/2017 15:00,32 +9/22/2017 16:00,32 +9/22/2017 17:00,31 +9/22/2017 18:00,26 +9/22/2017 19:00,22 +9/22/2017 20:00,18 +9/22/2017 21:00,16 +9/22/2017 22:00,13 +9/22/2017 23:00,13 +9/23/2017 0:00,13 +9/23/2017 1:00,11 +9/23/2017 2:00,9 +9/23/2017 3:00,8 +9/23/2017 4:00,9 +9/23/2017 5:00,8 +9/23/2017 6:00,9 +9/23/2017 7:00,11 +9/23/2017 8:00,17 +9/23/2017 9:00,23 +9/23/2017 10:00,26 +9/23/2017 11:00,29 +9/23/2017 12:00,31 +9/23/2017 13:00,32 +9/23/2017 14:00,32 +9/23/2017 15:00,28 +9/23/2017 16:00,26 +9/23/2017 17:00,23 +9/23/2017 18:00,21 +9/23/2017 19:00,19 +9/23/2017 20:00,18 +9/23/2017 21:00,17 +9/23/2017 22:00,16 +9/23/2017 23:00,16 +9/24/2017 0:00,16 +9/24/2017 1:00,12 +9/24/2017 2:00,12 +9/24/2017 3:00,11 +9/24/2017 4:00,12 +9/24/2017 5:00,11 +9/24/2017 6:00,7 +9/24/2017 7:00,11 +9/24/2017 8:00,14 +9/24/2017 9:00,16 +9/24/2017 10:00,17 +9/24/2017 11:00,18 +9/24/2017 12:00,19 +9/24/2017 13:00,21 +9/24/2017 14:00,21 +9/24/2017 15:00,21 +9/24/2017 16:00,22 +9/24/2017 17:00,22 +9/24/2017 18:00,20 +9/24/2017 19:00,19 +9/24/2017 20:00,19 +9/24/2017 21:00,19 +9/24/2017 22:00,18 +9/24/2017 23:00,17 +9/25/2017 0:00,17 +9/25/2017 1:00,17 +9/25/2017 2:00,17 +9/25/2017 3:00,16 +9/25/2017 4:00,17 +9/25/2017 5:00,16 +9/25/2017 6:00,16 +9/25/2017 7:00,16 +9/25/2017 8:00,17 +9/25/2017 9:00,19 +9/25/2017 10:00,20 +9/25/2017 11:00,21 +9/25/2017 12:00,21 +9/25/2017 13:00,22 +9/25/2017 14:00,22.2 +9/25/2017 15:00,22 +9/25/2017 16:00,21 +9/25/2017 17:00,19 +9/25/2017 18:00,17 +9/25/2017 19:00,14 +9/25/2017 20:00,12 +9/25/2017 21:00,11 +9/25/2017 22:00,8 +9/25/2017 23:00,9 +9/26/2017 0:00,8 +9/26/2017 1:00,8 +9/26/2017 2:00,6 +9/26/2017 3:00,7 +9/26/2017 4:00,5 +9/26/2017 5:00,7 +9/26/2017 6:00,3 +9/26/2017 7:00,7 +9/26/2017 8:00,11 +9/26/2017 9:00,14 +9/26/2017 10:00,15 +9/26/2017 11:00,17 +9/26/2017 12:00,18 +9/26/2017 13:00,18 +9/26/2017 14:00,18 +9/26/2017 15:00,18 +9/26/2017 16:00,18 +9/26/2017 17:00,18 +9/26/2017 18:00,16 +9/26/2017 19:00,15 +9/26/2017 20:00,12 +9/26/2017 21:00,12 +9/26/2017 22:00,11 +9/26/2017 23:00,8 +9/27/2017 0:00,7 +9/27/2017 1:00,9 +9/27/2017 2:00,7 +9/27/2017 3:00,6 +9/27/2017 4:00,4 +9/27/2017 5:00,1 +9/27/2017 6:00,1 +9/27/2017 7:00,3 +9/27/2017 8:00,7 +9/27/2017 9:00,9 +9/27/2017 10:00,13 +9/27/2017 11:00,15 +9/27/2017 12:00,16.1 +9/27/2017 13:00,17 +9/27/2017 14:00,18.2 +9/27/2017 15:00,19 +9/27/2017 16:00,18 +9/27/2017 17:00,18 +9/27/2017 18:00,16 +9/27/2017 19:00,12 +9/27/2017 20:00,7 +9/27/2017 21:00,5 +9/27/2017 22:00,3.5 +9/27/2017 23:00,2.5 +9/28/2017 0:00,1.2 +9/28/2017 1:00,0.5 +9/28/2017 2:00,-0.3 +9/28/2017 3:00,-1.3 +9/28/2017 4:00,-1.5 +9/28/2017 5:00,-2 +9/28/2017 6:00,-2 +9/28/2017 7:00,1 +9/28/2017 8:00,5 +9/28/2017 9:00,7.8 +9/28/2017 10:00,10 +9/28/2017 11:00,13.2 +9/28/2017 12:00,16 +9/28/2017 13:00,18 +9/28/2017 14:00,19 +9/28/2017 15:00,20 +9/28/2017 16:00,20 +9/28/2017 17:00,19 +9/28/2017 18:00,13 +9/28/2017 19:00,11 +9/28/2017 20:00,9 +9/28/2017 21:00,7.7 +9/28/2017 22:00,6.5 +9/28/2017 23:00,5.8 +9/29/2017 0:00,4.8 +9/29/2017 1:00,4.4 +9/29/2017 2:00,3.8 +9/29/2017 3:00,3.2 +9/29/2017 4:00,3.2 +9/29/2017 5:00,3 +9/29/2017 6:00,4 +9/29/2017 7:00,6 +9/29/2017 8:00,13 +9/29/2017 9:00,16 +9/29/2017 10:00,18 +9/29/2017 11:00,21 +9/29/2017 12:00,22 +9/29/2017 13:00,23 +9/29/2017 14:00,24 +9/29/2017 15:00,24 +9/29/2017 16:00,25 +9/29/2017 17:00,23 +9/29/2017 18:00,16 +9/29/2017 19:00,13 +9/29/2017 20:00,13 +9/29/2017 21:00,11.6 +9/29/2017 22:00,11.2 +9/29/2017 23:00,9.8 +9/30/2017 0:00,9.4 +9/30/2017 1:00,10 +9/30/2017 2:00,8.6 +9/30/2017 3:00,8.2 +9/30/2017 4:00,5.8 +9/30/2017 5:00,5.4 +9/30/2017 6:00,4 +9/30/2017 7:00,6 +9/30/2017 8:00,11 +9/30/2017 9:00,14 +9/30/2017 10:00,17 +9/30/2017 11:00,18 +9/30/2017 12:00,21 +9/30/2017 13:00,22 +9/30/2017 14:00,23 +9/30/2017 15:00,23 +9/30/2017 16:00,23 +9/30/2017 17:00,22 +9/30/2017 18:00,15 +9/30/2017 19:00,12 +9/30/2017 20:00,12 +9/30/2017 21:00,10.5 +9/30/2017 22:00,10.2 +9/30/2017 23:00,9.9 +10/1/2017 0:00,9.6 +10/1/2017 1:00,9.2 +10/1/2017 2:00,8.9 +10/1/2017 3:00,8.6 +10/1/2017 4:00,8.3 +10/1/2017 5:00,8.8 +10/1/2017 6:00,8.3 +10/1/2017 7:00,8.8 +10/1/2017 8:00,12.2 +10/1/2017 9:00,14.4 +10/1/2017 10:00,16.1 +10/1/2017 11:00,17.2 +10/1/2017 12:00,18.3 +10/1/2017 13:00,19.4 +10/1/2017 14:00,20 +10/1/2017 15:00,20 +10/1/2017 16:00,20 +10/1/2017 17:00,18.8 +10/1/2017 18:00,17.2 +10/1/2017 19:00,15 +10/1/2017 20:00,15 +10/1/2017 21:00,13.8 +10/1/2017 22:00,12.8 +10/1/2017 23:00,12.2 +10/2/2017 0:00,11.1 +10/2/2017 1:00,11.5 +10/2/2017 2:00,11.2 +10/2/2017 3:00,11.1 +10/2/2017 4:00,12.2 +10/2/2017 5:00,11.6 +10/2/2017 6:00,11.6 +10/2/2017 7:00,10 +10/2/2017 8:00,11.1 +10/2/2017 9:00,11.6 +10/2/2017 10:00,13.3 +10/2/2017 11:00,14.4 +10/2/2017 12:00,16.6 +10/2/2017 13:00,17.7 +10/2/2017 14:00,19.4 +10/2/2017 15:00,20.5 +10/2/2017 16:00,20 +10/2/2017 17:00,20 +10/2/2017 18:00,18.8 +10/2/2017 19:00,16.1 +10/2/2017 20:00,15.5 +10/2/2017 21:00,15 +10/2/2017 22:00,13.8 +10/2/2017 23:00,17.7 +10/3/2017 0:00,16.2 +10/3/2017 1:00,16.2 +10/3/2017 2:00,15.5 +10/3/2017 3:00,15 +10/3/2017 4:00,14.3 +10/3/2017 5:00,13.3 +10/3/2017 6:00,13.3 +10/3/2017 7:00,12.7 +10/3/2017 8:00,15 +10/3/2017 9:00,17.7 +10/3/2017 10:00,18.3 +10/3/2017 11:00,18.8 +10/3/2017 12:00,17.7 +10/3/2017 13:00,12.2 +10/3/2017 14:00,15 +10/3/2017 15:00,18.3 +10/3/2017 16:00,17.7 +10/3/2017 17:00,16.6 +10/3/2017 18:00,15.5 +10/3/2017 19:00,13.3 +10/3/2017 20:00,12.2 +10/3/2017 21:00,11.1 +10/3/2017 22:00,10.6 +10/3/2017 23:00,10.5 +10/4/2017 0:00,9.2 +10/4/2017 1:00,9.2 +10/4/2017 2:00,8.6 +10/4/2017 3:00,8.3 +10/4/2017 4:00,7.9 +10/4/2017 5:00,7.2 +10/4/2017 6:00,7.2 +10/4/2017 7:00,8.3 +10/4/2017 8:00,10.5 +10/4/2017 9:00,13.8 +10/4/2017 10:00,15 +10/4/2017 11:00,16.6 +10/4/2017 12:00,17.2 +10/4/2017 13:00,18.3 +10/4/2017 14:00,18.8 +10/4/2017 15:00,18.8 +10/4/2017 16:00,18.3 +10/4/2017 17:00,17.7 +10/4/2017 18:00,15 +10/4/2017 19:00,12.7 +10/4/2017 20:00,11.4 +10/4/2017 21:00,10.5 +10/4/2017 22:00,5.5 +10/4/2017 23:00,5 +10/5/2017 0:00,3.5 +10/5/2017 1:00,3.4 +10/5/2017 2:00,2.7 +10/5/2017 3:00,2.1 +10/5/2017 4:00,1.6 +10/5/2017 5:00,2.2 +10/5/2017 6:00,2.2 +10/5/2017 7:00,3.8 +10/5/2017 8:00,7.2 +10/5/2017 9:00,9.4 +10/5/2017 10:00,12.2 +10/5/2017 11:00,13.8 +10/5/2017 12:00,15.5 +10/5/2017 13:00,17.2 +10/5/2017 14:00,17.7 +10/5/2017 15:00,18.8 +10/5/2017 16:00,18.1 +10/5/2017 17:00,16.6 +10/5/2017 18:00,13.3 +10/5/2017 19:00,12.2 +10/5/2017 20:00,11.4 +10/5/2017 21:00,11.1 +10/5/2017 22:00,10.5 +10/5/2017 23:00,11.1 +10/6/2017 0:00,10 +10/6/2017 1:00,10.4 +10/6/2017 2:00,10.1 +10/6/2017 3:00,10 +10/6/2017 4:00,10.5 +10/6/2017 5:00,10 +10/6/2017 6:00,10 +10/6/2017 7:00,10 +10/6/2017 8:00,11.6 +10/6/2017 9:00,12.7 +10/6/2017 10:00,15.5 +10/6/2017 11:00,17.7 +10/6/2017 12:00,18.8 +10/6/2017 13:00,19.4 +10/6/2017 14:00,20.5 +10/6/2017 15:00,19.4 +10/6/2017 16:00,19.4 +10/6/2017 17:00,19.4 +10/6/2017 18:00,16.6 +10/6/2017 19:00,15 +10/6/2017 20:00,11.6 +10/6/2017 21:00,12.7 +10/6/2017 22:00,11.6 +10/6/2017 23:00,11.6 +10/7/2017 0:00,9.4 +10/7/2017 1:00,8.7 +10/7/2017 2:00,7.3 +10/7/2017 3:00,6.1 +10/7/2017 4:00,4.4 +10/7/2017 5:00,4.4 +10/7/2017 6:00,3.8 +10/7/2017 7:00,5.5 +10/7/2017 8:00,10 +10/7/2017 9:00,13.8 +10/7/2017 10:00,16.6 +10/7/2017 11:00,18.3 +10/7/2017 12:00,18.8 +10/7/2017 13:00,18.8 +10/7/2017 14:00,20 +10/7/2017 15:00,20 +10/7/2017 16:00,19.4 +10/7/2017 17:00,17.7 +10/7/2017 18:00,15.8 +10/7/2017 19:00,14.4 +10/7/2017 20:00,11.6 +10/7/2017 21:00,10.9 +10/7/2017 22:00,10 +10/7/2017 23:00,9.4 +10/8/2017 0:00,8.1 +10/8/2017 1:00,8.3 +10/8/2017 2:00,7.8 +10/8/2017 3:00,7.5 +10/8/2017 4:00,7.2 +10/8/2017 5:00,8.3 +10/8/2017 6:00,11.6 +10/8/2017 7:00,11.6 +10/8/2017 8:00,12.8 +10/8/2017 9:00,14.4 +10/8/2017 10:00,15.2 +10/8/2017 11:00,15.5 +10/8/2017 12:00,16.1 +10/8/2017 13:00,15 +10/8/2017 14:00,17.7 +10/8/2017 15:00,18.3 +10/8/2017 16:00,17.2 +10/8/2017 17:00,17.2 +10/8/2017 18:00,15.5 +10/8/2017 19:00,15 +10/8/2017 20:00,12.2 +10/8/2017 21:00,11.1 +10/8/2017 22:00,8.6 +10/8/2017 23:00,6.6 +10/9/2017 0:00,6.5 +10/9/2017 1:00,7.8 +10/9/2017 2:00,8.5 +10/9/2017 3:00,9.4 +10/9/2017 4:00,7.2 +10/9/2017 5:00,7.3 +10/9/2017 6:00,7.6 +10/9/2017 7:00,8.8 +10/9/2017 8:00,10.5 +10/9/2017 9:00,12.7 +10/9/2017 10:00,14.4 +10/9/2017 11:00,15.5 +10/9/2017 12:00,17.7 +10/9/2017 13:00,18.8 +10/9/2017 14:00,19.4 +10/9/2017 15:00,20 +10/9/2017 16:00,19.4 +10/9/2017 17:00,18.3 +10/9/2017 18:00,16.6 +10/9/2017 19:00,16.6 +10/9/2017 20:00,15.5 +10/9/2017 21:00,15 +10/9/2017 22:00,15 +10/9/2017 23:00,13.8 +10/10/2017 0:00,13.2 +10/10/2017 1:00,13.9 +10/10/2017 2:00,14.1 +10/10/2017 3:00,14.4 +10/10/2017 4:00,14.5 +10/10/2017 5:00,14.4 +10/10/2017 6:00,14.4 +10/10/2017 7:00,13.8 +10/10/2017 8:00,15 +10/10/2017 9:00,16.1 +10/10/2017 10:00,16.6 +10/10/2017 11:00,18.5 +10/10/2017 12:00,20 +10/10/2017 13:00,20 +10/10/2017 14:00,20.5 +10/10/2017 15:00,21.1 +10/10/2017 16:00,20 +10/10/2017 17:00,21.1 +10/10/2017 18:00,19.4 +10/10/2017 19:00,17.2 +10/10/2017 20:00,15 +10/10/2017 21:00,15 +10/10/2017 22:00,14.4 +10/10/2017 23:00,13.8 +10/11/2017 0:00,12.6 +10/11/2017 1:00,12.8 +10/11/2017 2:00,12.4 +10/11/2017 3:00,12.2 +10/11/2017 4:00,12.3 +10/11/2017 5:00,12.2 +10/11/2017 6:00,11.6 +10/11/2017 7:00,11.6 +10/11/2017 8:00,11.6 +10/11/2017 9:00,12.2 +10/11/2017 10:00,12.2 +10/11/2017 11:00,12.7 +10/11/2017 12:00,13.3 +10/11/2017 13:00,13.3 +10/11/2017 14:00,13.8 +10/11/2017 15:00,16.1 +10/11/2017 16:00,16.1 +10/11/2017 17:00,15.2 +10/11/2017 18:00,13.3 +10/11/2017 19:00,11.6 +10/11/2017 20:00,11.1 +10/11/2017 21:00,10.5 +10/11/2017 22:00,9.4 +10/11/2017 23:00,9.4 +10/12/2017 0:00,8.3 +10/12/2017 1:00,8.7 +10/12/2017 2:00,8.4 +10/12/2017 3:00,8.3 +10/12/2017 4:00,8.4 +10/12/2017 5:00,8.3 +10/12/2017 6:00,8.3 +10/12/2017 7:00,9.4 +10/12/2017 8:00,11.1 +10/12/2017 9:00,12.2 +10/12/2017 10:00,13.3 +10/12/2017 11:00,15.5 +10/12/2017 12:00,17.2 +10/12/2017 13:00,17.7 +10/12/2017 14:00,18.3 +10/12/2017 15:00,18.8 +10/12/2017 16:00,18.3 +10/12/2017 17:00,16.6 +10/12/2017 18:00,13.8 +10/12/2017 19:00,11.6 +10/12/2017 20:00,10.5 +10/12/2017 21:00,8.3 +10/12/2017 22:00,8.8 +10/12/2017 23:00,6.6 +10/13/2017 0:00,4.6 +10/13/2017 1:00,3.9 +10/13/2017 2:00,2.6 +10/13/2017 3:00,1.6 +10/13/2017 4:00,0.5 +10/13/2017 5:00,1.1 +10/13/2017 6:00,1.1 +10/13/2017 7:00,2.2 +10/13/2017 8:00,5.3 +10/13/2017 9:00,8.7 +10/13/2017 10:00,11.6 +10/13/2017 11:00,13.7 +10/13/2017 12:00,15.5 +10/13/2017 13:00,16.8 +10/13/2017 14:00,17.7 +10/13/2017 15:00,18.3 +10/13/2017 16:00,18.3 +10/13/2017 17:00,18.3 +10/13/2017 18:00,15.5 +10/13/2017 19:00,13.3 +10/13/2017 20:00,9.4 +10/13/2017 21:00,7.7 +10/13/2017 22:00,7.2 +10/13/2017 23:00,5.5 +10/14/2017 0:00,3.9 +10/14/2017 1:00,3.7 +10/14/2017 2:00,2.9 +10/14/2017 3:00,2.2 +10/14/2017 4:00,1.6 +10/14/2017 5:00,1.8 +10/14/2017 6:00,2.2 +10/14/2017 7:00,2.7 +10/14/2017 8:00,6.6 +10/14/2017 9:00,8.8 +10/14/2017 10:00,12.7 +10/14/2017 11:00,15 +10/14/2017 12:00,16.6 +10/14/2017 13:00,19.4 +10/14/2017 14:00,20 +10/14/2017 15:00,21.6 +10/14/2017 16:00,21.6 +10/14/2017 17:00,18.8 +10/14/2017 18:00,15 +10/14/2017 19:00,12.7 +10/14/2017 20:00,10.5 +10/14/2017 21:00,10 +10/14/2017 22:00,8.3 +10/14/2017 23:00,8.3 +10/15/2017 0:00,6.8 +10/15/2017 1:00,6.6 +10/15/2017 2:00,5.8 +10/15/2017 3:00,5.3 +10/15/2017 4:00,4.7 +10/15/2017 5:00,3.8 +10/15/2017 6:00,2.7 +10/15/2017 7:00,4.4 +10/15/2017 8:00,7.7 +10/15/2017 9:00,11.6 +10/15/2017 10:00,13.8 +10/15/2017 11:00,16.6 +10/15/2017 12:00,17.7 +10/15/2017 13:00,20 +10/15/2017 14:00,21.1 +10/15/2017 15:00,21.1 +10/15/2017 16:00,20.5 +10/15/2017 17:00,18.8 +10/15/2017 18:00,17.2 +10/15/2017 19:00,13.8 +10/15/2017 20:00,15 +10/15/2017 21:00,15.5 +10/15/2017 22:00,18.3 +10/15/2017 23:00,18.3 +10/16/2017 0:00,17.1 +10/16/2017 1:00,17.4 +10/16/2017 2:00,17 +10/16/2017 3:00,16.8 +10/16/2017 4:00,16.6 +10/16/2017 5:00,16.1 +10/16/2017 6:00,15.6 +10/16/2017 7:00,16.1 +10/16/2017 8:00,17 +10/16/2017 9:00,18.3 +10/16/2017 10:00,19.4 +10/16/2017 11:00,20.5 +10/16/2017 12:00,21.1 +10/16/2017 13:00,21.1 +10/16/2017 14:00,21.6 +10/16/2017 15:00,21.1 +10/16/2017 16:00,20.5 +10/16/2017 17:00,18.8 +10/16/2017 18:00,17.2 +10/16/2017 19:00,15.5 +10/16/2017 20:00,14.4 +10/16/2017 21:00,14.4 +10/16/2017 22:00,14.2 +10/16/2017 23:00,14.4 +10/17/2017 0:00,13.1 +10/17/2017 1:00,13.1 +10/17/2017 2:00,12.6 +10/17/2017 3:00,12.2 +10/17/2017 4:00,12.6 +10/17/2017 5:00,12.7 +10/17/2017 6:00,12.7 +10/17/2017 7:00,12.7 +10/17/2017 8:00,13.3 +10/17/2017 9:00,14.4 +10/17/2017 10:00,15 +10/17/2017 11:00,14.4 +10/17/2017 12:00,13.3 +10/17/2017 13:00,12.7 +10/17/2017 14:00,12.2 +10/17/2017 15:00,12.2 +10/17/2017 16:00,12.2 +10/17/2017 17:00,12.7 +10/17/2017 18:00,13.3 +10/17/2017 19:00,13.3 +10/17/2017 20:00,12.8 +10/17/2017 21:00,12.7 +10/17/2017 22:00,11.1 +10/17/2017 23:00,11.6 +10/18/2017 0:00,10.4 +10/18/2017 1:00,10.6 +10/18/2017 2:00,10.2 +10/18/2017 3:00,10 +10/18/2017 4:00,9 +10/18/2017 5:00,7.7 +10/18/2017 6:00,7.2 +10/18/2017 7:00,6.6 +10/18/2017 8:00,9.4 +10/18/2017 9:00,11.6 +10/18/2017 10:00,12.7 +10/18/2017 11:00,15.5 +10/18/2017 12:00,16.1 +10/18/2017 13:00,16.6 +10/18/2017 14:00,17.2 +10/18/2017 15:00,17.2 +10/18/2017 16:00,16.6 +10/18/2017 17:00,15.5 +10/18/2017 18:00,12.5 +10/18/2017 19:00,10 +10/18/2017 20:00,8.4 +10/18/2017 21:00,7.2 +10/18/2017 22:00,3.8 +10/18/2017 23:00,1.6 +10/19/2017 0:00,0.3 +10/19/2017 1:00,0.3 +10/19/2017 2:00,-0.2 +10/19/2017 3:00,-0.6 +10/19/2017 4:00,-0.1 +10/19/2017 5:00,0.1 +10/19/2017 6:00,0.5 +10/19/2017 7:00,-0.6 +10/19/2017 8:00,3.3 +10/19/2017 9:00,6.6 +10/19/2017 10:00,8.8 +10/19/2017 11:00,11.6 +10/19/2017 12:00,13.8 +10/19/2017 13:00,15 +10/19/2017 14:00,16.1 +10/19/2017 15:00,17.2 +10/19/2017 16:00,17.2 +10/19/2017 17:00,14.4 +10/19/2017 18:00,10 +10/19/2017 19:00,8.8 +10/19/2017 20:00,8.3 +10/19/2017 21:00,7.2 +10/19/2017 22:00,5 +10/19/2017 23:00,3.3 +10/20/2017 0:00,2.1 +10/20/2017 1:00,2.3 +10/20/2017 2:00,1.8 +10/20/2017 3:00,1.6 +10/20/2017 4:00,3.4 +10/20/2017 5:00,4.9 +10/20/2017 6:00,6.6 +10/20/2017 7:00,6.1 +10/20/2017 8:00,7.2 +10/20/2017 9:00,8.3 +10/20/2017 10:00,9.9 +10/20/2017 11:00,11.1 +10/20/2017 12:00,11.1 +10/20/2017 13:00,10 +10/20/2017 14:00,11.1 +10/20/2017 15:00,13.3 +10/20/2017 16:00,13.3 +10/20/2017 17:00,13.8 +10/20/2017 18:00,10 +10/20/2017 19:00,7.7 +10/20/2017 20:00,6.6 +10/20/2017 21:00,6.6 +10/20/2017 22:00,3.8 +10/20/2017 23:00,2.2 +10/21/2017 0:00,1.9 +10/21/2017 1:00,2.9 +10/21/2017 2:00,3.4 +10/21/2017 3:00,4 +10/21/2017 4:00,4.7 +10/21/2017 5:00,5 +10/21/2017 6:00,6.6 +10/21/2017 7:00,6.6 +10/21/2017 8:00,8.3 +10/21/2017 9:00,9.4 +10/21/2017 10:00,10.5 +10/21/2017 11:00,11.1 +10/21/2017 12:00,11.1 +10/21/2017 13:00,10.5 +10/21/2017 14:00,11.6 +10/21/2017 15:00,13.3 +10/21/2017 16:00,12.7 +10/21/2017 17:00,11.6 +10/21/2017 18:00,9.4 +10/21/2017 19:00,6.1 +10/21/2017 20:00,5 +10/21/2017 21:00,3.8 +10/21/2017 22:00,5.5 +10/21/2017 23:00,1.6 +10/22/2017 0:00,0.3 +10/22/2017 1:00,0.5 +10/22/2017 2:00,0 +10/22/2017 3:00,-0.3 +10/22/2017 4:00,-0.6 +10/22/2017 5:00,-1.2 +10/22/2017 6:00,-1.7 +10/22/2017 7:00,-0.9 +10/22/2017 8:00,1.6 +10/22/2017 9:00,6 +10/22/2017 10:00,10 +10/22/2017 11:00,12.1 +10/22/2017 12:00,13.8 +10/22/2017 13:00,15 +10/22/2017 14:00,15 +10/22/2017 15:00,15.5 +10/22/2017 16:00,13.8 +10/22/2017 17:00,12.2 +10/22/2017 18:00,9.4 +10/22/2017 19:00,7.7 +10/22/2017 20:00,5.5 +10/22/2017 21:00,4.4 +10/22/2017 22:00,1.6 +10/22/2017 23:00,1.1 +10/23/2017 0:00,0.1 +10/23/2017 1:00,0.4 +10/23/2017 2:00,0.1 +10/23/2017 3:00,0.1 +10/23/2017 4:00,0 +10/23/2017 5:00,1.1 +10/23/2017 6:00,1.1 +10/23/2017 7:00,2.2 +10/23/2017 8:00,2.7 +10/23/2017 9:00,5 +10/23/2017 10:00,6.6 +10/23/2017 11:00,7.9 +10/23/2017 12:00,8.8 +10/23/2017 13:00,10 +10/23/2017 14:00,10.5 +10/23/2017 15:00,11.1 +10/23/2017 16:00,10.5 +10/23/2017 17:00,10 +10/23/2017 18:00,8.8 +10/23/2017 19:00,7.2 +10/23/2017 20:00,5.5 +10/23/2017 21:00,3.9 +10/23/2017 22:00,2.2 +10/23/2017 23:00,0.5 +10/24/2017 0:00,0 +10/24/2017 1:00,0.9 +10/24/2017 2:00,1.2 +10/24/2017 3:00,1.7 +10/24/2017 4:00,2.2 +10/24/2017 5:00,2.7 +10/24/2017 6:00,2.7 +10/24/2017 7:00,1.6 +10/24/2017 8:00,2.7 +10/24/2017 9:00,8.8 +10/24/2017 10:00,12.7 +10/24/2017 11:00,15 +10/24/2017 12:00,15.5 +10/24/2017 13:00,17.2 +10/24/2017 14:00,16.1 +10/24/2017 15:00,15.5 +10/24/2017 16:00,15.5 +10/24/2017 17:00,14.4 +10/24/2017 18:00,12.2 +10/24/2017 19:00,10.9 +10/24/2017 20:00,10 +10/24/2017 21:00,7.7 +10/24/2017 22:00,7.5 +10/24/2017 23:00,7.7 +10/25/2017 0:00,6.4 +10/25/2017 1:00,6.4 +10/25/2017 2:00,5.9 +10/25/2017 3:00,5.5 +10/25/2017 4:00,5.1 +10/25/2017 5:00,4.4 +10/25/2017 6:00,3.3 +10/25/2017 7:00,3.3 +10/25/2017 8:00,5.5 +10/25/2017 9:00,8.8 +10/25/2017 10:00,11.6 +10/25/2017 11:00,12.2 +10/25/2017 12:00,13.3 +10/25/2017 13:00,13.8 +10/25/2017 14:00,7.7 +10/25/2017 15:00,12.2 +10/25/2017 16:00,12.2 +10/25/2017 17:00,11.6 +10/25/2017 18:00,11.6 +10/25/2017 19:00,11.6 +10/25/2017 20:00,11.6 +10/25/2017 21:00,12.2 +10/25/2017 22:00,12.7 +10/25/2017 23:00,12.5 +10/26/2017 0:00,11.6 +10/26/2017 1:00,12.2 +10/26/2017 2:00,12.1 +10/26/2017 3:00,12.2 +10/26/2017 4:00,13.8 +10/26/2017 5:00,10 +10/26/2017 6:00,8.8 +10/26/2017 7:00,9.1 +10/26/2017 8:00,11 +10/26/2017 9:00,13.3 +10/26/2017 10:00,15.5 +10/26/2017 11:00,16.6 +10/26/2017 12:00,17.7 +10/26/2017 13:00,17.7 +10/26/2017 14:00,18.8 +10/26/2017 15:00,17.7 +10/26/2017 16:00,16.1 +10/26/2017 17:00,13.8 +10/26/2017 18:00,10.5 +10/26/2017 19:00,10.5 +10/26/2017 20:00,10.5 +10/26/2017 21:00,8.8 +10/26/2017 22:00,6.6 +10/26/2017 23:00,2.7 +10/27/2017 0:00,1.2 +10/27/2017 1:00,1.2 +10/27/2017 2:00,0.5 +10/27/2017 3:00,0 +10/27/2017 4:00,0.1 +10/27/2017 5:00,0 +10/27/2017 6:00,0.5 +10/27/2017 7:00,1.1 +10/27/2017 8:00,4.4 +10/27/2017 9:00,7.7 +10/27/2017 10:00,10.7 +10/27/2017 11:00,13.3 +10/27/2017 12:00,13.5 +10/27/2017 13:00,13.3 +10/27/2017 14:00,13.3 +10/27/2017 15:00,13.8 +10/27/2017 16:00,13.3 +10/27/2017 17:00,12.2 +10/27/2017 18:00,8.8 +10/27/2017 19:00,6.1 +10/27/2017 20:00,5 +10/27/2017 21:00,2.7 +10/27/2017 22:00,2.7 +10/27/2017 23:00,0.5 +10/28/2017 0:00,-0.5 +10/28/2017 1:00,-0.1 +10/28/2017 2:00,-0.3 +10/28/2017 3:00,-0.3 +10/28/2017 4:00,-0.3 +10/28/2017 5:00,-0.6 +10/28/2017 6:00,-0.6 +10/28/2017 7:00,0 +10/28/2017 8:00,1.1 +10/28/2017 9:00,2.7 +10/28/2017 10:00,4.4 +10/28/2017 11:00,6.6 +10/28/2017 12:00,7.7 +10/28/2017 13:00,8.8 +10/28/2017 14:00,9.4 +10/28/2017 15:00,8.8 +10/28/2017 16:00,10 +10/28/2017 17:00,9.6 +10/28/2017 18:00,8.3 +10/28/2017 19:00,5.5 +10/28/2017 20:00,4.4 +10/28/2017 21:00,3.3 +10/28/2017 22:00,1.6 +10/28/2017 23:00,0.5 +10/29/2017 0:00,-0.9 +10/29/2017 1:00,-1 +10/29/2017 2:00,-1.7 +10/29/2017 3:00,-2.1 +10/29/2017 4:00,-2.6 +10/29/2017 5:00,-3.4 +10/29/2017 6:00,-4.5 +10/29/2017 7:00,-3.4 +10/29/2017 8:00,-1.2 +10/29/2017 9:00,1.6 +10/29/2017 10:00,5.5 +10/29/2017 11:00,5 +10/29/2017 12:00,5.7 +10/29/2017 13:00,6.1 +10/29/2017 14:00,7.7 +10/29/2017 15:00,7.7 +10/29/2017 16:00,7.2 +10/29/2017 17:00,5.5 +10/29/2017 18:00,3.8 +10/29/2017 19:00,1.1 +10/29/2017 20:00,-0.6 +10/29/2017 21:00,-0.6 +10/29/2017 22:00,-1.7 +10/29/2017 23:00,-2.8 +10/30/2017 0:00,-3.4 +10/30/2017 1:00,-3.9 +10/30/2017 2:00,-4.9 +10/30/2017 3:00,-5.8 +10/30/2017 4:00,-6.7 +10/30/2017 5:00,-6.2 +10/30/2017 6:00,-5.6 +10/30/2017 7:00,-5.6 +10/30/2017 8:00,-3.9 +10/30/2017 9:00,-1.2 +10/30/2017 10:00,1.6 +10/30/2017 11:00,3.3 +10/30/2017 12:00,4.4 +10/30/2017 13:00,6.1 +10/30/2017 14:00,7.2 +10/30/2017 15:00,6.6 +10/30/2017 16:00,6.1 +10/30/2017 17:00,5 +10/30/2017 18:00,2.7 +10/30/2017 19:00,1.1 +10/30/2017 20:00,-1.7 +10/30/2017 21:00,-2.2 +10/30/2017 22:00,-2.8 +10/30/2017 23:00,-4.5 +10/31/2017 0:00,-4.7 +10/31/2017 1:00,-3.4 +10/31/2017 2:00,-2.8 +10/31/2017 3:00,-2 +10/31/2017 4:00,-1.2 +10/31/2017 5:00,-1.2 +10/31/2017 6:00,-1.2 +10/31/2017 7:00,-1.2 +10/31/2017 8:00,-1.2 +10/31/2017 9:00,-1.2 +10/31/2017 10:00,-0.6 +10/31/2017 11:00,2.7 +10/31/2017 12:00,4.4 +10/31/2017 13:00,6.1 +10/31/2017 14:00,6.1 +10/31/2017 15:00,7.2 +10/31/2017 16:00,6.6 +10/31/2017 17:00,5 +10/31/2017 18:00,3.3 +10/31/2017 19:00,1.1 +10/31/2017 20:00,-0.6 +10/31/2017 21:00,-3.4 +10/31/2017 22:00,-1.4 +10/31/2017 23:00,0.4 +11/1/2017 0:00,2.3 +11/1/2017 1:00,4.3 +11/1/2017 2:00,6.2 +11/1/2017 3:00,8.1 +11/1/2017 4:00,10 +11/1/2017 5:00,9 +11/1/2017 6:00,9 +11/1/2017 7:00,9 +11/1/2017 8:00,11 +11/1/2017 9:00,10 +11/1/2017 10:00,12 +11/1/2017 11:00,12 +11/1/2017 12:00,13 +11/1/2017 13:00,13 +11/1/2017 14:00,13 +11/1/2017 15:00,13 +11/1/2017 16:00,14 +11/1/2017 17:00,13 +11/1/2017 18:00,13 +11/1/2017 19:00,12 +11/1/2017 20:00,11 +11/1/2017 21:00,11 +11/1/2017 22:00,11 +11/1/2017 23:00,11 +11/2/2017 0:00,11 +11/2/2017 1:00,11 +11/2/2017 2:00,11 +11/2/2017 3:00,11 +11/2/2017 4:00,10 +11/2/2017 5:00,9 +11/2/2017 6:00,10 +11/2/2017 7:00,11 +11/2/2017 8:00,11 +11/2/2017 9:00,13 +11/2/2017 10:00,14 +11/2/2017 11:00,16 +11/2/2017 12:00,18 +11/2/2017 13:00,18 +11/2/2017 14:00,19 +11/2/2017 15:00,19 +11/2/2017 16:00,18 +11/2/2017 17:00,15 +11/2/2017 18:00,11 +11/2/2017 19:00,10 +11/2/2017 20:00,8 +11/2/2017 21:00,7 +11/2/2017 22:00,6 +11/2/2017 23:00,5 +11/3/2017 0:00,4 +11/3/2017 1:00,4 +11/3/2017 2:00,3 +11/3/2017 3:00,2 +11/3/2017 4:00,2 +11/3/2017 5:00,1.3 +11/3/2017 6:00,1 +11/3/2017 7:00,3 +11/3/2017 8:00,4 +11/3/2017 9:00,6 +11/3/2017 10:00,7 +11/3/2017 11:00,8 +11/3/2017 12:00,11 +11/3/2017 13:00,14 +11/3/2017 14:00,15 +11/3/2017 15:00,14 +11/3/2017 16:00,13 +11/3/2017 17:00,11 +11/3/2017 18:00,8 +11/3/2017 19:00,7 +11/3/2017 20:00,7 +11/3/2017 21:00,5 +11/3/2017 22:00,4 +11/3/2017 23:00,3.3 +11/4/2017 0:00,2.5 +11/4/2017 1:00,2 +11/4/2017 2:00,4 +11/4/2017 3:00,4 +11/4/2017 4:00,4 +11/4/2017 5:00,5 +11/4/2017 6:00,5.5 +11/4/2017 7:00,6 +11/4/2017 8:00,5 +11/4/2017 9:00,6 +11/4/2017 10:00,6 +11/4/2017 11:00,7 +11/4/2017 12:00,8 +11/4/2017 13:00,8 +11/4/2017 14:00,9 +11/4/2017 15:00,9 +11/4/2017 16:00,8 +11/4/2017 17:00,9 +11/4/2017 18:00,9 +11/4/2017 19:00,9 +11/4/2017 20:00,8 +11/4/2017 21:00,8 +11/4/2017 22:00,8 +11/4/2017 23:00,8 +11/5/2017 0:00,8 +11/5/2017 1:00,8 +11/5/2017 2:00,7 +11/5/2017 3:00,7 +11/5/2017 4:00,7 +11/5/2017 5:00,7 +11/5/2017 6:00,7 +11/5/2017 7:00,7 +11/5/2017 8:00,8 +11/5/2017 9:00,8 +11/5/2017 10:00,10 +11/5/2017 11:00,12 +11/5/2017 12:00,13 +11/5/2017 13:00,14 +11/5/2017 14:00,14 +11/5/2017 15:00,14 +11/5/2017 16:00,13 +11/5/2017 17:00,10 +11/5/2017 18:00,9 +11/5/2017 19:00,7 +11/5/2017 20:00,6 +11/5/2017 21:00,2 +11/5/2017 22:00,2 +11/5/2017 23:00,1 +11/6/2017 0:00,-1 +11/6/2017 1:00,0 +11/6/2017 2:00,-1 +11/6/2017 3:00,-1 +11/6/2017 4:00,-1 +11/6/2017 5:00,-2 +11/6/2017 6:00,-1 +11/6/2017 7:00,1 +11/6/2017 8:00,2 +11/6/2017 9:00,4 +11/6/2017 10:00,5 +11/6/2017 11:00,6 +11/6/2017 12:00,6 +11/6/2017 13:00,7 +11/6/2017 14:00,9 +11/6/2017 15:00,9 +11/6/2017 16:00,9 +11/6/2017 17:00,7 +11/6/2017 18:00,4 +11/6/2017 19:00,4 +11/6/2017 20:00,2 +11/6/2017 21:00,1 +11/6/2017 22:00,2 +11/6/2017 23:00,-2 +11/7/2017 0:00,-2 +11/7/2017 1:00,-2 +11/7/2017 2:00,-3 +11/7/2017 3:00,-3 +11/7/2017 4:00,-3 +11/7/2017 5:00,-4 +11/7/2017 6:00,-4 +11/7/2017 7:00,-4 +11/7/2017 8:00,-2 +11/7/2017 9:00,-1 +11/7/2017 10:00,1 +11/7/2017 11:00,4 +11/7/2017 12:00,7 +11/7/2017 13:00,8 +11/7/2017 14:00,9 +11/7/2017 15:00,9 +11/7/2017 16:00,8 +11/7/2017 17:00,4 +11/7/2017 18:00,2 +11/7/2017 19:00,2 +11/7/2017 20:00,-1 +11/7/2017 21:00,-2 +11/7/2017 22:00,-3 +11/7/2017 23:00,-3 +11/8/2017 0:00,-3 +11/8/2017 1:00,-4 +11/8/2017 2:00,-4 +11/8/2017 3:00,-4 +11/8/2017 4:00,-4 +11/8/2017 5:00,-5 +11/8/2017 6:00,-4 +11/8/2017 7:00,-5 +11/8/2017 8:00,-3 +11/8/2017 9:00,-2 +11/8/2017 10:00,-1 +11/8/2017 11:00,0 +11/8/2017 12:00,2 +11/8/2017 13:00,5 +11/8/2017 14:00,6 +11/8/2017 15:00,6 +11/8/2017 16:00,4 +11/8/2017 17:00,3 +11/8/2017 18:00,2 +11/8/2017 19:00,1 +11/8/2017 20:00,0 +11/8/2017 21:00,0 +11/8/2017 22:00,0 +11/8/2017 23:00,1 +11/9/2017 0:00,1 +11/9/2017 1:00,1 +11/9/2017 2:00,0 +11/9/2017 3:00,0 +11/9/2017 4:00,-1 +11/9/2017 5:00,-1 +11/9/2017 6:00,-1 +11/9/2017 7:00,-1 +11/9/2017 8:00,-1 +11/9/2017 9:00,-1 +11/9/2017 10:00,0 +11/9/2017 11:00,0 +11/9/2017 12:00,1 +11/9/2017 13:00,2 +11/9/2017 14:00,2 +11/9/2017 15:00,2 +11/9/2017 16:00,3 +11/9/2017 17:00,2 +11/9/2017 18:00,1 +11/9/2017 19:00,2 +11/9/2017 20:00,1 +11/9/2017 21:00,2 +11/9/2017 22:00,1 +11/9/2017 23:00,1 +11/10/2017 0:00,1 +11/10/2017 1:00,1 +11/10/2017 2:00,1 +11/10/2017 3:00,1 +11/10/2017 4:00,0 +11/10/2017 5:00,0 +11/10/2017 6:00,-1 +11/10/2017 7:00,0 +11/10/2017 8:00,-1 +11/10/2017 9:00,0 +11/10/2017 10:00,0 +11/10/2017 11:00,1 +11/10/2017 12:00,1 +11/10/2017 13:00,2 +11/10/2017 14:00,3 +11/10/2017 15:00,3 +11/10/2017 16:00,3 +11/10/2017 17:00,3 +11/10/2017 18:00,2 +11/10/2017 19:00,2 +11/10/2017 20:00,2 +11/10/2017 21:00,2 +11/10/2017 22:00,2 +11/10/2017 23:00,1 +11/11/2017 0:00,0.8 +11/11/2017 1:00,1 +11/11/2017 2:00,1 +11/11/2017 3:00,1 +11/11/2017 4:00,1 +11/11/2017 5:00,1 +11/11/2017 6:00,1 +11/11/2017 7:00,1 +11/11/2017 8:00,1 +11/11/2017 9:00,1 +11/11/2017 10:00,1 +11/11/2017 11:00,2 +11/11/2017 12:00,3 +11/11/2017 13:00,3 +11/11/2017 14:00,3 +11/11/2017 15:00,3 +11/11/2017 16:00,3 +11/11/2017 17:00,3 +11/11/2017 18:00,2 +11/11/2017 19:00,2 +11/11/2017 20:00,2 +11/11/2017 21:00,2 +11/11/2017 22:00,0 +11/11/2017 23:00,-1 +11/12/2017 0:00,1 +11/12/2017 1:00,-1 +11/12/2017 2:00,-1 +11/12/2017 3:00,-1 +11/12/2017 4:00,0 +11/12/2017 5:00,-1 +11/12/2017 6:00,0 +11/12/2017 7:00,-1 +11/12/2017 8:00,0 +11/12/2017 9:00,0 +11/12/2017 10:00,1 +11/12/2017 11:00,2 +11/12/2017 12:00,3 +11/12/2017 13:00,6 +11/12/2017 14:00,7 +11/12/2017 15:00,8 +11/12/2017 16:00,7 +11/12/2017 17:00,6 +11/12/2017 18:00,4 +11/12/2017 19:00,4 +11/12/2017 20:00,3 +11/12/2017 21:00,3 +11/12/2017 22:00,3 +11/12/2017 23:00,3 +11/13/2017 0:00,3 +11/13/2017 1:00,5 +11/13/2017 2:00,4 +11/13/2017 3:00,5 +11/13/2017 4:00,4 +11/13/2017 5:00,4 +11/13/2017 6:00,2 +11/13/2017 7:00,3 +11/13/2017 8:00,3 +11/13/2017 9:00,9 +11/13/2017 10:00,12 +11/13/2017 11:00,15 +11/13/2017 12:00,14 +11/13/2017 13:00,15 +11/13/2017 14:00,14 +11/13/2017 15:00,14 +11/13/2017 16:00,12 +11/13/2017 17:00,12 +11/13/2017 18:00,11 +11/13/2017 19:00,11 +11/13/2017 20:00,12 +11/13/2017 21:00,14 +11/13/2017 22:00,13 +11/13/2017 23:00,14 +11/14/2017 0:00,14 +11/14/2017 1:00,14 +11/14/2017 2:00,15 +11/14/2017 3:00,16 +11/14/2017 4:00,16 +11/14/2017 5:00,15 +11/14/2017 6:00,16 +11/14/2017 7:00,16 +11/14/2017 8:00,17 +11/14/2017 9:00,17 +11/14/2017 10:00,19 +11/14/2017 11:00,19 +11/14/2017 12:00,19 +11/14/2017 13:00,19 +11/14/2017 14:00,19 +11/14/2017 15:00,19 +11/14/2017 16:00,19 +11/14/2017 17:00,19 +11/14/2017 18:00,16 +11/14/2017 19:00,15 +11/14/2017 20:00,14 +11/14/2017 21:00,15 +11/14/2017 22:00,14 +11/14/2017 23:00,13 +11/15/2017 0:00,8 +11/15/2017 1:00,7 +11/15/2017 2:00,7 +11/15/2017 3:00,8 +11/15/2017 4:00,7 +11/15/2017 5:00,9 +11/15/2017 6:00,12 +11/15/2017 7:00,10 +11/15/2017 8:00,12 +11/15/2017 9:00,15 +11/15/2017 10:00,17 +11/15/2017 11:00,18 +11/15/2017 12:00,18 +11/15/2017 13:00,16 +11/15/2017 14:00,16 +11/15/2017 15:00,15 +11/15/2017 16:00,14 +11/15/2017 17:00,13 +11/15/2017 18:00,14 +11/15/2017 19:00,13 +11/15/2017 20:00,12 +11/15/2017 21:00,12 +11/15/2017 22:00,12 +11/15/2017 23:00,12 +11/16/2017 0:00,11 +11/16/2017 1:00,11 +11/16/2017 2:00,11 +11/16/2017 3:00,11 +11/16/2017 4:00,11 +11/16/2017 5:00,11 +11/16/2017 6:00,11 +11/16/2017 7:00,11 +11/16/2017 8:00,12 +11/16/2017 9:00,12 +11/16/2017 10:00,12 +11/16/2017 11:00,12 +11/16/2017 12:00,12 +11/16/2017 13:00,12 +11/16/2017 14:00,12 +11/16/2017 15:00,11 +11/16/2017 16:00,11 +11/16/2017 17:00,10 +11/16/2017 18:00,9 +11/16/2017 19:00,9 +11/16/2017 20:00,9 +11/16/2017 21:00,9 +11/16/2017 22:00,9 +11/16/2017 23:00,9 +11/17/2017 0:00,9 +11/17/2017 1:00,9 +11/17/2017 2:00,9 +11/17/2017 3:00,8 +11/17/2017 4:00,9 +11/17/2017 5:00,9 +11/17/2017 6:00,8 +11/17/2017 7:00,7 +11/17/2017 8:00,8 +11/17/2017 9:00,9 +11/17/2017 10:00,9 +11/17/2017 11:00,11 +11/17/2017 12:00,11 +11/17/2017 13:00,12 +11/17/2017 14:00,13 +11/17/2017 15:00,13 +11/17/2017 16:00,12 +11/17/2017 17:00,10 +11/17/2017 18:00,6 +11/17/2017 19:00,4 +11/17/2017 20:00,2 +11/17/2017 21:00,2 +11/17/2017 22:00,1 +11/17/2017 23:00,2 +11/18/2017 0:00,1 +11/18/2017 1:00,0 +11/18/2017 2:00,0 +11/18/2017 3:00,0 +11/18/2017 4:00,0.2 +11/18/2017 5:00,0.2 +11/18/2017 6:00,0.6 +11/18/2017 7:00,1 +11/18/2017 8:00,2 +11/18/2017 9:00,4 +11/18/2017 10:00,6 +11/18/2017 11:00,8 +11/18/2017 12:00,7 +11/18/2017 13:00,8 +11/18/2017 14:00,9 +11/18/2017 15:00,7 +11/18/2017 16:00,6 +11/18/2017 17:00,4 +11/18/2017 18:00,3 +11/18/2017 19:00,3 +11/18/2017 20:00,1 +11/18/2017 21:00,2 +11/18/2017 22:00,3 +11/18/2017 23:00,3 +11/19/2017 0:00,3 +11/19/2017 1:00,3 +11/19/2017 2:00,3 +11/19/2017 3:00,3 +11/19/2017 4:00,2 +11/19/2017 5:00,1.8 +11/19/2017 6:00,2 +11/19/2017 7:00,2 +11/19/2017 8:00,3 +11/19/2017 9:00,3 +11/19/2017 10:00,4 +11/19/2017 11:00,5 +11/19/2017 12:00,6 +11/19/2017 13:00,6 +11/19/2017 14:00,6 +11/19/2017 15:00,6 +11/19/2017 16:00,6 +11/19/2017 17:00,6 +11/19/2017 18:00,6 +11/19/2017 19:00,6 +11/19/2017 20:00,6 +11/19/2017 21:00,6 +11/19/2017 22:00,7 +11/19/2017 23:00,7 +11/20/2017 0:00,8 +11/20/2017 1:00,8 +11/20/2017 2:00,7 +11/20/2017 3:00,7 +11/20/2017 4:00,7 +11/20/2017 5:00,7 +11/20/2017 6:00,7 +11/20/2017 7:00,6 +11/20/2017 8:00,8 +11/20/2017 9:00,8 +11/20/2017 10:00,10 +11/20/2017 11:00,12 +11/20/2017 12:00,12 +11/20/2017 13:00,12 +11/20/2017 14:00,12 +11/20/2017 15:00,11.8 +11/20/2017 16:00,11 +11/20/2017 17:00,10 +11/20/2017 18:00,9 +11/20/2017 19:00,9 +11/20/2017 20:00,9 +11/20/2017 21:00,9 +11/20/2017 22:00,8 +11/20/2017 23:00,8 +11/21/2017 0:00,8 +11/21/2017 1:00,9 +11/21/2017 2:00,8 +11/21/2017 3:00,8 +11/21/2017 4:00,9 +11/21/2017 5:00,8 +11/21/2017 6:00,7 +11/21/2017 7:00,8 +11/21/2017 8:00,8.7 +11/21/2017 9:00,10.4 +11/21/2017 10:00,12 +11/21/2017 11:00,11 +11/21/2017 12:00,13 +11/21/2017 13:00,13 +11/21/2017 14:00,12 +11/21/2017 15:00,12 +11/21/2017 16:00,11 +11/21/2017 17:00,9 +11/21/2017 18:00,10 +11/21/2017 19:00,8 +11/21/2017 20:00,8 +11/21/2017 21:00,8 +11/21/2017 22:00,7 +11/21/2017 23:00,7 +11/22/2017 0:00,7 +11/22/2017 1:00,6 +11/22/2017 2:00,5 +11/22/2017 3:00,3 +11/22/2017 4:00,2 +11/22/2017 5:00,3 +11/22/2017 6:00,3 +11/22/2017 7:00,4 +11/22/2017 8:00,4 +11/22/2017 9:00,6 +11/22/2017 10:00,7 +11/22/2017 11:00,10 +11/22/2017 12:00,13 +11/22/2017 13:00,13 +11/22/2017 14:00,13 +11/22/2017 15:00,13 +11/22/2017 16:00,12 +11/22/2017 17:00,11 +11/22/2017 18:00,11 +11/22/2017 19:00,11 +11/22/2017 20:00,10 +11/22/2017 21:00,10 +11/22/2017 22:00,10 +11/22/2017 23:00,9 +11/23/2017 0:00,9 +11/23/2017 1:00,9 +11/23/2017 2:00,8 +11/23/2017 3:00,8 +11/23/2017 4:00,8 +11/23/2017 5:00,8 +11/23/2017 6:00,7 +11/23/2017 7:00,7 +11/23/2017 8:00,7 +11/23/2017 9:00,8 +11/23/2017 10:00,9 +11/23/2017 11:00,11 +11/23/2017 12:00,12 +11/23/2017 13:00,13 +11/23/2017 14:00,13 +11/23/2017 15:00,12 +11/23/2017 16:00,10 +11/23/2017 17:00,8 +11/23/2017 18:00,7 +11/23/2017 19:00,5 +11/23/2017 20:00,6 +11/23/2017 21:00,4 +11/23/2017 22:00,1 +11/23/2017 23:00,-1 +11/24/2017 0:00,-1 +11/24/2017 1:00,-1 +11/24/2017 2:00,-1 +11/24/2017 3:00,-2 +11/24/2017 4:00,-2 +11/24/2017 5:00,-2 +11/24/2017 6:00,-2 +11/24/2017 7:00,-1 +11/24/2017 8:00,-1 +11/24/2017 9:00,1 +11/24/2017 10:00,3 +11/24/2017 11:00,4 +11/24/2017 12:00,6 +11/24/2017 13:00,7 +11/24/2017 14:00,8 +11/24/2017 15:00,7 +11/24/2017 16:00,6 +11/24/2017 17:00,6 +11/24/2017 18:00,5 +11/24/2017 19:00,4 +11/24/2017 20:00,5 +11/24/2017 21:00,4 +11/24/2017 22:00,4 +11/24/2017 23:00,4 +11/25/2017 0:00,4 +11/25/2017 1:00,4.1 +11/25/2017 2:00,4 +11/25/2017 3:00,4 +11/25/2017 4:00,4 +11/25/2017 5:00,4 +11/25/2017 6:00,4 +11/25/2017 7:00,4 +11/25/2017 8:00,4 +11/25/2017 9:00,4 +11/25/2017 10:00,5 +11/25/2017 11:00,5.6 +11/25/2017 12:00,6 +11/25/2017 13:00,7 +11/25/2017 14:00,7 +11/25/2017 15:00,7.3 +11/25/2017 16:00,7 +11/25/2017 17:00,7 +11/25/2017 18:00,7 +11/25/2017 19:00,7 +11/25/2017 20:00,6 +11/25/2017 21:00,6 +11/25/2017 22:00,4 +11/25/2017 23:00,4 +11/26/2017 0:00,4 +11/26/2017 1:00,2 +11/26/2017 2:00,3 +11/26/2017 3:00,3 +11/26/2017 4:00,0 +11/26/2017 5:00,1 +11/26/2017 6:00,2 +11/26/2017 7:00,-2 +11/26/2017 8:00,-2 +11/26/2017 9:00,2 +11/26/2017 10:00,6 +11/26/2017 11:00,7 +11/26/2017 12:00,7 +11/26/2017 13:00,8 +11/26/2017 14:00,9 +11/26/2017 15:00,9 +11/26/2017 16:00,7 +11/26/2017 17:00,4 +11/26/2017 18:00,1 +11/26/2017 19:00,-1 +11/26/2017 20:00,-1 +11/26/2017 21:00,-2 +11/26/2017 22:00,-3 +11/26/2017 23:00,-3 +11/27/2017 0:00,-4 +11/27/2017 1:00,-4 +11/27/2017 2:00,-5 +11/27/2017 3:00,-3 +11/27/2017 4:00,-3 +11/27/2017 5:00,-2.7 +11/27/2017 6:00,-2 +11/27/2017 7:00,-1 +11/27/2017 8:00,-1 +11/27/2017 9:00,-1 +11/27/2017 10:00,-1 +11/27/2017 11:00,-1 +11/27/2017 12:00,0 +11/27/2017 13:00,2 +11/27/2017 14:00,2 +11/27/2017 15:00,3 +11/27/2017 16:00,1 +11/27/2017 17:00,-1 +11/27/2017 18:00,0 +11/27/2017 19:00,-0.2 +11/27/2017 20:00,0 +11/27/2017 21:00,-1 +11/27/2017 22:00,-1 +11/27/2017 23:00,-0.7 +11/28/2017 0:00,-0.5 +11/28/2017 1:00,0 +11/28/2017 2:00,0 +11/28/2017 3:00,0 +11/28/2017 4:00,0 +11/28/2017 5:00,-1 +11/28/2017 6:00,-1 +11/28/2017 7:00,1 +11/28/2017 8:00,1 +11/28/2017 9:00,1 +11/28/2017 10:00,2 +11/28/2017 11:00,2 +11/28/2017 12:00,1 +11/28/2017 13:00,2 +11/28/2017 14:00,2 +11/28/2017 15:00,1 +11/28/2017 16:00,1 +11/28/2017 17:00,1 +11/28/2017 18:00,1 +11/28/2017 19:00,1 +11/28/2017 20:00,1 +11/28/2017 21:00,1 +11/28/2017 22:00,1 +11/28/2017 23:00,1 +11/29/2017 0:00,-1 +11/29/2017 1:00,-0.6 +11/29/2017 2:00,-0.3 +11/29/2017 3:00,0 +11/29/2017 4:00,6 +11/29/2017 5:00,8 +11/29/2017 6:00,7 +11/29/2017 7:00,7 +11/29/2017 8:00,7 +11/29/2017 9:00,7 +11/29/2017 10:00,7.1 +11/29/2017 11:00,7 +11/29/2017 12:00,8 +11/29/2017 13:00,7 +11/29/2017 14:00,8 +11/29/2017 15:00,7 +11/29/2017 16:00,6.8 +11/29/2017 17:00,6 +11/29/2017 18:00,4 +11/29/2017 19:00,4 +11/29/2017 20:00,4 +11/29/2017 21:00,4 +11/29/2017 22:00,4 +11/29/2017 23:00,4 +11/30/2017 0:00,4 +11/30/2017 1:00,3 +11/30/2017 2:00,4 +11/30/2017 3:00,2 +11/30/2017 4:00,4 +11/30/2017 5:00,4 +11/30/2017 6:00,3 +11/30/2017 7:00,5 +11/30/2017 8:00,6 +11/30/2017 9:00,7 +11/30/2017 10:00,8 +11/30/2017 11:00,8 +11/30/2017 12:00,8 +11/30/2017 13:00,9 +11/30/2017 14:00,7 +11/30/2017 15:00,7 +11/30/2017 16:00,7 +11/30/2017 17:00,6 +11/30/2017 18:00,7 +11/30/2017 19:00,8 +11/30/2017 20:00,9 +11/30/2017 21:00,9 +11/30/2017 22:00,8.9 +11/30/2017 23:00,8.7 +12/1/2017 0:00,8.6 +12/1/2017 1:00,8.4 +12/1/2017 2:00,8.3 +12/1/2017 3:00,8.1 +12/1/2017 4:00,8 +12/1/2017 5:00,8 +12/1/2017 6:00,8 +12/1/2017 7:00,8 +12/1/2017 8:00,11 +12/1/2017 9:00,10 +12/1/2017 10:00,11 +12/1/2017 11:00,12 +12/1/2017 12:00,14 +12/1/2017 13:00,14 +12/1/2017 14:00,14 +12/1/2017 15:00,13 +12/1/2017 16:00,12 +12/1/2017 17:00,12 +12/1/2017 18:00,12 +12/1/2017 19:00,11 +12/1/2017 20:00,11 +12/1/2017 21:00,10 +12/1/2017 22:00,9 +12/1/2017 23:00,9 +12/2/2017 0:00,8 +12/2/2017 1:00,7 +12/2/2017 2:00,6 +12/2/2017 3:00,6 +12/2/2017 4:00,4 +12/2/2017 5:00,3 +12/2/2017 6:00,1 +12/2/2017 7:00,-1 +12/2/2017 8:00,-1 +12/2/2017 9:00,0 +12/2/2017 10:00,2 +12/2/2017 11:00,6 +12/2/2017 12:00,6 +12/2/2017 13:00,6 +12/2/2017 14:00,6 +12/2/2017 15:00,6 +12/2/2017 16:00,5 +12/2/2017 17:00,3 +12/2/2017 18:00,3 +12/2/2017 19:00,1 +12/2/2017 20:00,1 +12/2/2017 21:00,1 +12/2/2017 22:00,0 +12/2/2017 23:00,-1 +12/3/2017 0:00,-1 +12/3/2017 1:00,1 +12/3/2017 2:00,2 +12/3/2017 3:00,3 +12/3/2017 4:00,4 +12/3/2017 5:00,4 +12/3/2017 6:00,3 +12/3/2017 7:00,4 +12/3/2017 8:00,4 +12/3/2017 9:00,4 +12/3/2017 10:00,6 +12/3/2017 11:00,6 +12/3/2017 12:00,8 +12/3/2017 13:00,8 +12/3/2017 14:00,8 +12/3/2017 15:00,8 +12/3/2017 16:00,6 +12/3/2017 17:00,4 +12/3/2017 18:00,4 +12/3/2017 19:00,5 +12/3/2017 20:00,5 +12/3/2017 21:00,4 +12/3/2017 22:00,5 +12/3/2017 23:00,5 +12/4/2017 0:00,5 +12/4/2017 1:00,6 +12/4/2017 2:00,6 +12/4/2017 3:00,5 +12/4/2017 4:00,4 +12/4/2017 5:00,5 +12/4/2017 6:00,4 +12/4/2017 7:00,4 +12/4/2017 8:00,4 +12/4/2017 9:00,4 +12/4/2017 10:00,6 +12/4/2017 11:00,6 +12/4/2017 12:00,7 +12/4/2017 13:00,7 +12/4/2017 14:00,7 +12/4/2017 15:00,7 +12/4/2017 16:00,6 +12/4/2017 17:00,6 +12/4/2017 18:00,6 +12/4/2017 19:00,4 +12/4/2017 20:00,5 +12/4/2017 21:00,5 +12/4/2017 22:00,4 +12/4/2017 23:00,4 +12/5/2017 0:00,4 +12/5/2017 1:00,4 +12/5/2017 2:00,4 +12/5/2017 3:00,4 +12/5/2017 4:00,3.6 +12/5/2017 5:00,4 +12/5/2017 6:00,3.3 +12/5/2017 7:00,3 +12/5/2017 8:00,3 +12/5/2017 9:00,2 +12/5/2017 10:00,4 +12/5/2017 11:00,6 +12/5/2017 12:00,7 +12/5/2017 13:00,8 +12/5/2017 14:00,8 +12/5/2017 15:00,7 +12/5/2017 16:00,5 +12/5/2017 17:00,4 +12/5/2017 18:00,4 +12/5/2017 19:00,5 +12/5/2017 20:00,5 +12/5/2017 21:00,6 +12/5/2017 22:00,5 +12/5/2017 23:00,4 +12/6/2017 0:00,3 +12/6/2017 1:00,3 +12/6/2017 2:00,3 +12/6/2017 3:00,2 +12/6/2017 4:00,4 +12/6/2017 5:00,3 +12/6/2017 6:00,5 +12/6/2017 7:00,6 +12/6/2017 8:00,6 +12/6/2017 9:00,7 +12/6/2017 10:00,8 +12/6/2017 11:00,9 +12/6/2017 12:00,9 +12/6/2017 13:00,11 +12/6/2017 14:00,11 +12/6/2017 15:00,10 +12/6/2017 16:00,9 +12/6/2017 17:00,9 +12/6/2017 18:00,7 +12/6/2017 19:00,8 +12/6/2017 20:00,6 +12/6/2017 21:00,5 +12/6/2017 22:00,3 +12/6/2017 23:00,3 +12/7/2017 0:00,4 +12/7/2017 1:00,4 +12/7/2017 2:00,3 +12/7/2017 3:00,3 +12/7/2017 4:00,1 +12/7/2017 5:00,-2 +12/7/2017 6:00,-0.2 +12/7/2017 7:00,2 +12/7/2017 8:00,1 +12/7/2017 9:00,3 +12/7/2017 10:00,5 +12/7/2017 11:00,7 +12/7/2017 12:00,9 +12/7/2017 13:00,9 +12/7/2017 14:00,9 +12/7/2017 15:00,9 +12/7/2017 16:00,8 +12/7/2017 17:00,4 +12/7/2017 18:00,3 +12/7/2017 19:00,0 +12/7/2017 20:00,-1 +12/7/2017 21:00,-1 +12/7/2017 22:00,0 +12/7/2017 23:00,0 +12/8/2017 0:00,1 +12/8/2017 1:00,-1 +12/8/2017 2:00,0 +12/8/2017 3:00,-1 +12/8/2017 4:00,-1 +12/8/2017 5:00,-1 +12/8/2017 6:00,-1 +12/8/2017 7:00,-1 +12/8/2017 8:00,-1 +12/8/2017 9:00,1 +12/8/2017 10:00,2 +12/8/2017 11:00,3 +12/8/2017 12:00,4 +12/8/2017 13:00,4 +12/8/2017 14:00,5 +12/8/2017 15:00,6 +12/8/2017 16:00,6 +12/8/2017 17:00,5 +12/8/2017 18:00,5 +12/8/2017 19:00,6 +12/8/2017 20:00,8 +12/8/2017 21:00,7 +12/8/2017 22:00,7 +12/8/2017 23:00,7 +12/9/2017 0:00,7 +12/9/2017 1:00,6 +12/9/2017 2:00,5 +12/9/2017 3:00,3 +12/9/2017 4:00,2 +12/9/2017 5:00,2 +12/9/2017 6:00,1 +12/9/2017 7:00,2 +12/9/2017 8:00,2 +12/9/2017 9:00,4 +12/9/2017 10:00,7 +12/9/2017 11:00,8 +12/9/2017 12:00,8 +12/9/2017 13:00,8 +12/9/2017 14:00,8 +12/9/2017 15:00,8 +12/9/2017 16:00,6 +12/9/2017 17:00,3 +12/9/2017 18:00,1 +12/9/2017 19:00,1 +12/9/2017 20:00,-1 +12/9/2017 21:00,-2 +12/9/2017 22:00,1 +12/9/2017 23:00,2 +12/10/2017 0:00,2 +12/10/2017 1:00,2 +12/10/2017 2:00,2 +12/10/2017 3:00,2 +12/10/2017 4:00,2 +12/10/2017 5:00,2 +12/10/2017 6:00,1 +12/10/2017 7:00,1 +12/10/2017 8:00,1 +12/10/2017 9:00,2 +12/10/2017 10:00,3 +12/10/2017 11:00,3 +12/10/2017 12:00,4 +12/10/2017 13:00,4 +12/10/2017 14:00,4 +12/10/2017 15:00,3 +12/10/2017 16:00,3 +12/10/2017 17:00,3 +12/10/2017 18:00,2 +12/10/2017 19:00,2 +12/10/2017 20:00,1.9 +12/10/2017 21:00,1.9 +12/10/2017 22:00,2 +12/10/2017 23:00,2 +12/11/2017 0:00,2 +12/11/2017 1:00,2 +12/11/2017 2:00,2 +12/11/2017 3:00,2.2 +12/11/2017 4:00,2 +12/11/2017 5:00,2 +12/11/2017 6:00,2 +12/11/2017 7:00,2 +12/11/2017 8:00,2 +12/11/2017 9:00,2 +12/11/2017 10:00,3 +12/11/2017 11:00,4 +12/11/2017 12:00,4 +12/11/2017 13:00,4 +12/11/2017 14:00,4 +12/11/2017 15:00,4 +12/11/2017 16:00,4 +12/11/2017 17:00,3 +12/11/2017 18:00,3 +12/11/2017 19:00,3 +12/11/2017 20:00,2 +12/11/2017 21:00,2 +12/11/2017 22:00,2 +12/11/2017 23:00,1 +12/12/2017 0:00,1 +12/12/2017 1:00,1 +12/12/2017 2:00,2 +12/12/2017 3:00,1 +12/12/2017 4:00,1 +12/12/2017 5:00,2 +12/12/2017 6:00,1 +12/12/2017 7:00,1 +12/12/2017 8:00,1 +12/12/2017 9:00,2 +12/12/2017 10:00,3 +12/12/2017 11:00,4 +12/12/2017 12:00,5 +12/12/2017 13:00,6 +12/12/2017 14:00,6 +12/12/2017 15:00,5 +12/12/2017 16:00,4 +12/12/2017 17:00,4 +12/12/2017 18:00,4 +12/12/2017 19:00,4 +12/12/2017 20:00,5 +12/12/2017 21:00,7 +12/12/2017 22:00,7 +12/12/2017 23:00,7 +12/13/2017 0:00,8 +12/13/2017 1:00,8 +12/13/2017 2:00,9 +12/13/2017 3:00,8 +12/13/2017 4:00,8 +12/13/2017 5:00,9 +12/13/2017 6:00,11 +12/13/2017 7:00,10 +12/13/2017 8:00,11 +12/13/2017 9:00,12 +12/13/2017 10:00,12 +12/13/2017 11:00,12 +12/13/2017 12:00,12 +12/13/2017 13:00,11 +12/13/2017 14:00,12 +12/13/2017 15:00,13 +12/13/2017 16:00,13 +12/13/2017 17:00,13 +12/13/2017 18:00,12 +12/13/2017 19:00,13 +12/13/2017 20:00,13 +12/13/2017 21:00,13 +12/13/2017 22:00,11 +12/13/2017 23:00,12 +12/14/2017 0:00,10 +12/14/2017 1:00,9 +12/14/2017 2:00,8 +12/14/2017 3:00,8 +12/14/2017 4:00,7 +12/14/2017 5:00,7 +12/14/2017 6:00,7 +12/14/2017 7:00,6 +12/14/2017 8:00,5 +12/14/2017 9:00,6 +12/14/2017 10:00,7 +12/14/2017 11:00,7 +12/14/2017 12:00,7 +12/14/2017 13:00,7 +12/14/2017 14:00,9 +12/14/2017 15:00,7 +12/14/2017 16:00,7 +12/14/2017 17:00,4 +12/14/2017 18:00,3 +12/14/2017 19:00,3 +12/14/2017 20:00,1 +12/14/2017 21:00,1 +12/14/2017 22:00,1 +12/14/2017 23:00,-1 +12/15/2017 0:00,2 +12/15/2017 1:00,1 +12/15/2017 2:00,1 +12/15/2017 3:00,-1 +12/15/2017 4:00,-2 +12/15/2017 5:00,-3 +12/15/2017 6:00,-2 +12/15/2017 7:00,-2 +12/15/2017 8:00,-2 +12/15/2017 9:00,2 +12/15/2017 10:00,5 +12/15/2017 11:00,7 +12/15/2017 12:00,8 +12/15/2017 13:00,8 +12/15/2017 14:00,8 +12/15/2017 15:00,8 +12/15/2017 16:00,7 +12/15/2017 17:00,7 +12/15/2017 18:00,6 +12/15/2017 19:00,6 +12/15/2017 20:00,6 +12/15/2017 21:00,7 +12/15/2017 22:00,7 +12/15/2017 23:00,6 +12/16/2017 0:00,8 +12/16/2017 1:00,7 +12/16/2017 2:00,9 +12/16/2017 3:00,10 +12/16/2017 4:00,10 +12/16/2017 5:00,10 +12/16/2017 6:00,9 +12/16/2017 7:00,9 +12/16/2017 8:00,9 +12/16/2017 9:00,9 +12/16/2017 10:00,10 +12/16/2017 11:00,11 +12/16/2017 12:00,10 +12/16/2017 13:00,13 +12/16/2017 14:00,13 +12/16/2017 15:00,14 +12/16/2017 16:00,13 +12/16/2017 17:00,14 +12/16/2017 18:00,12 +12/16/2017 19:00,12 +12/16/2017 20:00,13 +12/16/2017 21:00,12 +12/16/2017 22:00,14 +12/16/2017 23:00,13 +12/17/2017 0:00,14 +12/17/2017 1:00,13 +12/17/2017 2:00,10 +12/17/2017 3:00,9 +12/17/2017 4:00,7 +12/17/2017 5:00,7 +12/17/2017 6:00,5 +12/17/2017 7:00,4 +12/17/2017 8:00,4 +12/17/2017 9:00,5 +12/17/2017 10:00,4 +12/17/2017 11:00,6 +12/17/2017 12:00,7 +12/17/2017 13:00,8 +12/17/2017 14:00,8 +12/17/2017 15:00,8 +12/17/2017 16:00,6 +12/17/2017 17:00,3 +12/17/2017 18:00,1 +12/17/2017 19:00,-2 +12/17/2017 20:00,-2 +12/17/2017 21:00,-3 +12/17/2017 22:00,-2 +12/17/2017 23:00,-3 +12/18/2017 0:00,-3 +12/18/2017 1:00,-2 +12/18/2017 2:00,-3 +12/18/2017 3:00,-3 +12/18/2017 4:00,-2 +12/18/2017 5:00,-4 +12/18/2017 6:00,-4.3 +12/18/2017 7:00,-4.1 +12/18/2017 8:00,-4 +12/18/2017 9:00,-3 +12/18/2017 10:00,-2 +12/18/2017 11:00,-1 +12/18/2017 12:00,-1 +12/18/2017 13:00,0 +12/18/2017 14:00,0 +12/18/2017 15:00,0 +12/18/2017 16:00,0 +12/18/2017 17:00,-1 +12/18/2017 18:00,-2 +12/18/2017 19:00,6 +12/18/2017 20:00,5 +12/18/2017 21:00,2 +12/18/2017 22:00,2 +12/18/2017 23:00,3 +12/19/2017 0:00,2 +12/19/2017 1:00,2 +12/19/2017 2:00,2 +12/19/2017 3:00,2 +12/19/2017 4:00,1 +12/19/2017 5:00,1 +12/19/2017 6:00,1 +12/19/2017 7:00,0 +12/19/2017 8:00,-2 +12/19/2017 9:00,1 +12/19/2017 10:00,2 +12/19/2017 11:00,2 +12/19/2017 12:00,3 +12/19/2017 13:00,2 +12/19/2017 14:00,3 +12/19/2017 15:00,2 +12/19/2017 16:00,1 +12/19/2017 17:00,1 +12/19/2017 18:00,1 +12/19/2017 19:00,1 +12/19/2017 20:00,1 +12/19/2017 21:00,1 +12/19/2017 22:00,1 +12/19/2017 23:00,1 +12/20/2017 0:00,1 +12/20/2017 1:00,1 +12/20/2017 2:00,1 +12/20/2017 3:00,1 +12/20/2017 4:00,1 +12/20/2017 5:00,1 +12/20/2017 6:00,1 +12/20/2017 7:00,1 +12/20/2017 8:00,1 +12/20/2017 9:00,2 +12/20/2017 10:00,2 +12/20/2017 11:00,3 +12/20/2017 12:00,4 +12/20/2017 13:00,4 +12/20/2017 14:00,4 +12/20/2017 15:00,4 +12/20/2017 16:00,3 +12/20/2017 17:00,3 +12/20/2017 18:00,2 +12/20/2017 19:00,2 +12/20/2017 20:00,3 +12/20/2017 21:00,2 +12/20/2017 22:00,2 +12/20/2017 23:00,2 +12/21/2017 0:00,2 +12/21/2017 1:00,2 +12/21/2017 2:00,3 +12/21/2017 3:00,2 +12/21/2017 4:00,2 +12/21/2017 5:00,2 +12/21/2017 6:00,1 +12/21/2017 7:00,0 +12/21/2017 8:00,-2 +12/21/2017 9:00,0 +12/21/2017 10:00,2 +12/21/2017 11:00,4 +12/21/2017 12:00,5 +12/21/2017 13:00,5 +12/21/2017 14:00,4 +12/21/2017 15:00,5 +12/21/2017 16:00,4 +12/21/2017 17:00,4 +12/21/2017 18:00,4 +12/21/2017 19:00,4 +12/21/2017 20:00,3 +12/21/2017 21:00,3 +12/21/2017 22:00,3 +12/21/2017 23:00,3 +12/22/2017 0:00,3 +12/22/2017 1:00,3 +12/22/2017 2:00,2 +12/22/2017 3:00,2 +12/22/2017 4:00,2 +12/22/2017 5:00,2 +12/22/2017 6:00,2 +12/22/2017 7:00,1 +12/22/2017 8:00,-1 +12/22/2017 9:00,-1 +12/22/2017 10:00,2 +12/22/2017 11:00,4 +12/22/2017 12:00,4.7 +12/22/2017 13:00,5 +12/22/2017 14:00,4 +12/22/2017 15:00,3.3 +12/22/2017 16:00,2 +12/22/2017 17:00,2 +12/22/2017 18:00,1 +12/22/2017 19:00,1 +12/22/2017 20:00,-1 +12/22/2017 21:00,-1 +12/22/2017 22:00,-1 +12/22/2017 23:00,-1 +12/23/2017 0:00,-1 +12/23/2017 1:00,-0.4 +12/23/2017 2:00,0 +12/23/2017 3:00,0 +12/23/2017 4:00,0 +12/23/2017 5:00,0 +12/23/2017 6:00,0 +12/23/2017 7:00,0 +12/23/2017 8:00,0 +12/23/2017 9:00,1 +12/23/2017 10:00,2 +12/23/2017 11:00,2 +12/23/2017 12:00,3 +12/23/2017 13:00,3 +12/23/2017 14:00,4 +12/23/2017 15:00,4 +12/23/2017 16:00,2 +12/23/2017 17:00,-1 +12/23/2017 18:00,-2 +12/23/2017 19:00,-3 +12/23/2017 20:00,-3 +12/23/2017 21:00,-3 +12/23/2017 22:00,-3 +12/23/2017 23:00,-2 +12/24/2017 0:00,-2 +12/24/2017 1:00,-1.9 +12/24/2017 2:00,-2 +12/24/2017 3:00,-2 +12/24/2017 4:00,-2 +12/24/2017 5:00,-2 +12/24/2017 6:00,-2 +12/24/2017 7:00,-3 +12/24/2017 8:00,-5 +12/24/2017 9:00,-4 +12/24/2017 10:00,-3 +12/24/2017 11:00,-2 +12/24/2017 12:00,-2 +12/24/2017 13:00,-1 +12/24/2017 14:00,-1 +12/24/2017 15:00,-1 +12/24/2017 16:00,0 +12/24/2017 17:00,-1 +12/24/2017 18:00,-1 +12/24/2017 19:00,-1 +12/24/2017 20:00,-2 +12/24/2017 21:00,-3 +12/24/2017 22:00,-2 +12/24/2017 23:00,-2 +12/25/2017 0:00,-1 +12/25/2017 1:00,-1 +12/25/2017 2:00,-1 +12/25/2017 3:00,-1 +12/25/2017 4:00,-1 +12/25/2017 5:00,-1.5 +12/25/2017 6:00,-3 +12/25/2017 7:00,-3 +12/25/2017 8:00,-3 +12/25/2017 9:00,-3 +12/25/2017 10:00,-2 +12/25/2017 11:00,-2 +12/25/2017 12:00,-1 +12/25/2017 13:00,-1 +12/25/2017 14:00,-1 +12/25/2017 15:00,-1 +12/25/2017 16:00,-1 +12/25/2017 17:00,-1 +12/25/2017 18:00,-1 +12/25/2017 19:00,-1 +12/25/2017 20:00,-1 +12/25/2017 21:00,-1 +12/25/2017 22:00,-1 +12/25/2017 23:00,-1 +12/26/2017 0:00,-1 +12/26/2017 1:00,-1 +12/26/2017 2:00,-1 +12/26/2017 3:00,-1 +12/26/2017 4:00,-1 +12/26/2017 5:00,-1 +12/26/2017 6:00,-2 +12/26/2017 7:00,-2 +12/26/2017 8:00,-2 +12/26/2017 9:00,-2 +12/26/2017 10:00,-2 +12/26/2017 11:00,-1 +12/26/2017 12:00,-1 +12/26/2017 13:00,0 +12/26/2017 14:00,0 +12/26/2017 15:00,0 +12/26/2017 16:00,-1 +12/26/2017 17:00,-2 +12/26/2017 18:00,-2 +12/26/2017 19:00,-2 +12/26/2017 20:00,-3 +12/26/2017 21:00,-4 +12/26/2017 22:00,-4 +12/26/2017 23:00,-6 +12/27/2017 0:00,-6 +12/27/2017 1:00,-6 +12/27/2017 2:00,-7 +12/27/2017 3:00,-6 +12/27/2017 4:00,-5 +12/27/2017 5:00,-6 +12/27/2017 6:00,-6 +12/27/2017 7:00,-5 +12/27/2017 8:00,-5 +12/27/2017 9:00,-4 +12/27/2017 10:00,-3 +12/27/2017 11:00,-2 +12/27/2017 12:00,-1 +12/27/2017 13:00,-1 +12/27/2017 14:00,0 +12/27/2017 15:00,0 +12/27/2017 16:00,-1 +12/27/2017 17:00,-1 +12/27/2017 18:00,-2 +12/27/2017 19:00,-2 +12/27/2017 20:00,-1 +12/27/2017 21:00,-1 +12/27/2017 22:00,-1 +12/27/2017 23:00,-1.4 +12/28/2017 0:00,-1.7 +12/28/2017 1:00,-2 +12/28/2017 2:00,-1.6 +12/28/2017 3:00,-1.1 +12/28/2017 4:00,-1 +12/28/2017 5:00,-2 +12/28/2017 6:00,-2 +12/28/2017 7:00,-1 +12/28/2017 8:00,-2 +12/28/2017 9:00,-3 +12/28/2017 10:00,-2 +12/28/2017 11:00,0 +12/28/2017 12:00,1 +12/28/2017 13:00,2 +12/28/2017 14:00,1 +12/28/2017 15:00,0 +12/28/2017 16:00,0 +12/28/2017 17:00,-1 +12/28/2017 18:00,-1 +12/28/2017 19:00,-1 +12/28/2017 20:00,-1 +12/28/2017 21:00,-4 +12/28/2017 22:00,-7 +12/28/2017 23:00,-8 +12/29/2017 0:00,-8 +12/29/2017 1:00,-8 +12/29/2017 2:00,-8 +12/29/2017 3:00,-10 +12/29/2017 4:00,-7 +12/29/2017 5:00,-7 +12/29/2017 6:00,-6 +12/29/2017 7:00,-5 +12/29/2017 8:00,-4 +12/29/2017 9:00,-4 +12/29/2017 10:00,-3 +12/29/2017 11:00,-3 +12/29/2017 12:00,-2 +12/29/2017 13:00,-1 +12/29/2017 14:00,-1 +12/29/2017 15:00,-1 +12/29/2017 16:00,-1 +12/29/2017 17:00,-1 +12/29/2017 18:00,-1.1 +12/29/2017 19:00,-1.1 +12/29/2017 20:00,-1 +12/29/2017 21:00,-1 +12/29/2017 22:00,-1 +12/29/2017 23:00,-1 +12/30/2017 0:00,-1 +12/30/2017 1:00,-1 +12/30/2017 2:00,-1 +12/30/2017 3:00,-1 +12/30/2017 4:00,-1 +12/30/2017 5:00,-1 +12/30/2017 6:00,-1.2 +12/30/2017 7:00,-1 +12/30/2017 8:00,-1 +12/30/2017 9:00,-1 +12/30/2017 10:00,-1 +12/30/2017 11:00,-1 +12/30/2017 12:00,0 +12/30/2017 13:00,0 +12/30/2017 14:00,0 +12/30/2017 15:00,0 +12/30/2017 16:00,-1 +12/30/2017 17:00,-1 +12/30/2017 18:00,-1 +12/30/2017 19:00,-1 +12/30/2017 20:00,-1 +12/30/2017 21:00,-1 +12/30/2017 22:00,-1 +12/30/2017 23:00,-1 +12/31/2017 0:00,-1 +12/31/2017 1:00,0 +12/31/2017 2:00,0 +12/31/2017 3:00,0 +12/31/2017 4:00,0 +12/31/2017 5:00,0 +12/31/2017 6:00,0 +12/31/2017 7:00,0 +12/31/2017 8:00,0 +12/31/2017 9:00,1 +12/31/2017 10:00,2 +12/31/2017 11:00,2 +12/31/2017 12:00,2 +12/31/2017 13:00,3 +12/31/2017 14:00,3 +12/31/2017 15:00,3 +12/31/2017 16:00,2 +12/31/2017 17:00,2 +12/31/2017 18:00,1 +12/31/2017 19:00,1 +12/31/2017 20:00,1 +12/31/2017 21:00,1 +12/31/2017 22:00,1 +12/31/2017 23:00,0.5 diff --git a/MarketAgents/CityAgent/city/__init__.py b/MarketAgents/CityAgent/city/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MarketAgents/CityAgent/city/agent.py b/MarketAgents/CityAgent/city/agent.py new file mode 100755 index 0000000..01cbf3e --- /dev/null +++ b/MarketAgents/CityAgent/city/agent.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2015, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +import os +import sys +import logging +from datetime import datetime, timedelta +from dateutil import parser +import numpy as np +import math + +from volttron.platform.vip.agent import Agent, Core, PubSub, RPC, compat +from volttron.platform.agent import utils +from volttron.platform.agent.utils import (get_aware_utc_now, format_timestamp) + +from timer import Timer +from generator import Generator + +utils.setup_logging() +_log = logging.getLogger(__name__) +__version__ = '0.1' + + +class CityAgent(Agent): + def __init__(self, config_path, **kwargs): + Agent.__init__(self, **kwargs) + + self.config_path = config_path + self.config = utils.load_config(config_path) + self.name = self.config.get('name') + self.T = int(self.config.get('T', 24)) + + self.db_topic = self.config.get("db_topic", "tnc") + self.campus_demand_topic = "{}/campus/city/demand".format(self.db_topic) + self.city_supply_topic = "{}/city/campus/supply".format(self.db_topic) + + self.simulation = self.config.get('simulation', False) + self.simulation_start_time = parser.parse(self.config.get('simulation_start_time')) + self.simulation_one_hour_in_seconds = int(self.config.get('simulation_one_hour_in_seconds')) + + Timer.created_time = datetime.now() + Timer.simulation = self.simulation + Timer.sim_start_time = self.simulation_start_time + Timer.sim_one_hr_in_sec = self.simulation_one_hour_in_seconds + + # Initialization + self.error_energy_threshold = float(self.config.get('error_energy_threshold')) + self.error_reserve_threshold = float(self.config.get('error_reserve_threshold')) + self.alpha_energy = float(self.config.get('alpha_energy')) + self.alpha_reserve = float(self.config.get('alpha_reserve')) + self.iteration_threshold = int(self.config.get('iteration_threshold')) + + self.init() + self.grid_supplier = Generator() + + self.power_demand = [] + self.committed_reserves = [] + self.power_supply = [] + self.desired_reserves = [] + + def init(self): + self.iteration = 0 + self.price_energy = np.array([np.ones(self.T) * 40]).T + self.price_reserved = np.array([np.ones(self.T) * 10]).T + + def get_exp_start_time(self): + one_second = timedelta(seconds=1) + if self.simulation: + next_exp_time = datetime.now() + one_second + else: + now = datetime.now() + offset = timedelta(seconds=3*Timer.sim_one_hr_in_sec) + next_exp_time = now + offset + if next_exp_time.day == now.day: + next_exp_time = now + one_second + else: + _log.debug("{} did not run onstart because it's too late. Wait for next hour.".format(self.name)) + next_exp_time = next_exp_time.replace(hour=0, minute=0, second=0, microsecond=0) + return next_exp_time + + def get_next_exp_time(self, cur_exp_time, cur_analysis_time): + one_T_simulation = timedelta(seconds=self.T*self.simulation_one_hour_in_seconds) + one_day = timedelta(days=1) + one_minute = timedelta(minutes=1) + + cur_analysis_time = cur_analysis_time.replace(hour=0, minute=0, second=0, microsecond=0) + if self.simulation: + next_exp_time = cur_exp_time + one_T_simulation + else: + cur_exp_time = cur_exp_time.replace(hour=0, minute=0, second=0, microsecond=0) + next_exp_time = cur_exp_time + one_day + one_minute + + next_analysis_time = cur_analysis_time + one_day + one_minute + + return next_exp_time, next_analysis_time + + @Core.receiver('onstart') + def onstart(self, sender, **kwargs): + # Subscriptions + self.vip.pubsub.subscribe(peer='pubsub', + prefix=self.campus_demand_topic, + callback=self.new_demand_signal) + + # Schedule to run 1st time if now is not too close to the end of day. Otherwise, schedule to run next day. + next_exp_time = self.get_exp_start_time() + next_analysis_time = next_exp_time + if self.simulation: + next_analysis_time = self.simulation_start_time + + _log.debug("{} schedule to run at exp_time: {} analysis_time: {}".format(self.name, + next_exp_time, + next_analysis_time)) + self.core.schedule(next_exp_time, self.schedule_run, + format_timestamp(next_exp_time), + format_timestamp(next_analysis_time)) + + def schedule_run(self, cur_exp_time, cur_analysis_time): + """ + Run when first start or run at beginning of day + :return: + """ + # Re-initialize each run + self.init() + + # Schedule power from supplier and consumer + self.schedule_power(start_of_cycle=True) + + # Schedule to run next day with start_of_cycle = True + cur_exp_time = parser.parse(cur_exp_time) + cur_analysis_time = parser.parse(cur_analysis_time) + next_exp_time, next_analysis_time = self.get_next_exp_time(cur_exp_time, cur_analysis_time) + self.core.schedule(next_exp_time, self.schedule_run, + format_timestamp(next_exp_time), + format_timestamp(next_analysis_time)) + + def send_to_campus(self, converged=False, start_of_cycle=False): + # Campus demand + msg = { + 'ts': format_timestamp(Timer.get_cur_time()), + 'price': self.price_energy[:,-1].tolist(), + 'price_reserved': self.price_reserved[:,-1].tolist(), + 'converged': converged, + 'start_of_cycle': start_of_cycle + } + + _log.info("City {} send to campus: {}".format(self.name, msg)) + self.vip.pubsub.publish(peer='pubsub', + topic=self.city_supply_topic, + message=msg) + + self.price_energy[:, -1] + + def schedule_power(self, start_of_cycle=False): + # Grid supplier + price_i = np.array([self.price_energy[:,-1]]).T + reserve_i = np.array([self.price_reserved[:,-1]]).T + + self.power_supply, self.desired_reserves = \ + self.grid_supplier.generate_bid(self.T, price_i, reserve_i) + + _log.debug("CITY: power supply: {}".format(self.power_supply)) + self.send_to_campus(converged=False, start_of_cycle=start_of_cycle) + + def new_demand_signal(self, peer, sender, bus, topic, headers, message): + _log.debug("At {}, {} receives new demand records: {}".format(Timer.get_cur_time(), + self.name, message)) + self.power_demand = message['power_demand'] + self.committed_reserves = message['committed_reserves'] + + if self.iteration < self.iteration_threshold: + self.iteration += 1 + _log.debug("CITY::Market iteration cycle: {}".format(self.iteration)) + result = self.balance_market() + # Start next iteration if balancing fails + if not result: + self.schedule_power(start_of_cycle=False) + else: + _log.debug("CITY::MARKET converged !!") + self.send_to_campus(converged=True, start_of_cycle=False) + + def balance_market(self): + power_demand = np.array([self.power_demand]).T + committed_reserves = np.array([self.committed_reserves]).T + + power_supply = self.power_supply + desired_reserve = self.desired_reserves + + price_energy = np.array([self.price_energy[:, -1]]).T + price_reserved = np.array([self.price_reserved[:, -1]]).T + + price_energy_new = price_energy - self.alpha_energy * (power_demand + power_supply)/math.sqrt(self.iteration) + price_reserved_new = price_reserved - self.alpha_reserve * (committed_reserves - desired_reserve)/math.sqrt(self.iteration) + + self.price_energy = np.append(self.price_energy, price_energy_new, axis=1) + self.price_reserved = np.append(self.price_reserved, price_reserved_new, axis=1) + + _log.debug("CITY::Price Difference: {}, energy threshold: {}".format(np.linalg.norm(price_energy_new-price_energy), self.error_energy_threshold)) + _log.debug("CITY::Reserve Price Difference: {}, reserve energy threshold:{}".format(np.linalg.norm(price_reserved_new-price_reserved), self.error_reserve_threshold)) + if np.linalg.norm(price_energy_new-price_energy) <= self.error_energy_threshold \ + and np.linalg.norm(price_reserved_new-price_reserved) <= self.error_reserve_threshold: + return True + + return False + + +def main(argv=sys.argv): + try: + utils.vip_main(CityAgent) + except Exception as e: + _log.exception('unhandled exception') + + +if __name__ == '__main__': + # Entry point for script + sys.exit(main()) diff --git a/MarketAgents/CityAgent/city/generator.py b/MarketAgents/CityAgent/city/generator.py new file mode 100755 index 0000000..a9ee687 --- /dev/null +++ b/MarketAgents/CityAgent/city/generator.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2015, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES +# LOSS OF USE, DATA, OR PROFITS OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +import os +import sys +import logging +from datetime import datetime +from dateutil import parser +import numpy as np + + +class Generator: + def __init__(self): + # TODO: Add to config file later... + self.c1 = [3.243, 30] # generation cost + self.r1 = [2, 10] # reserve cost + self.plower1 = 0.3 # lower bound of power production + self.pupper1 = 100 # upper bound of power production + self.ramp = 0.5 # ramp rate constraint + + def generate_bid(self, T, price_energy, price_reserved): + lam = price_energy + rp = price_reserved + + power_supply, reserve_desired = self.generate(T, lam, self.c1, + self.plower1, self.pupper1, + self.ramp, self.r1, rp) + + return power_supply, reserve_desired + + def generate(self, T, lam, c, plower, pupper, ramp, rc, rp): + import cvxpy as cp + + constraints = [] + u = cp.Variable((T, 1)) + r = cp.Variable((T, 1)) + objective = cp.Minimize(cp.sum( + c[0] * u ** 2 + cp.multiply(c[1], u) + rc[0] * r ** 2 + rc[1] * r - cp.multiply(lam, u) - cp.multiply(rp, + r))) + for i in range(0, T): + constraints += [ + u[i] >= plower, + u[i] <= pupper, + r[i] >= 0 + ] + if i < T - 1: + constraints += [ + u[i + 1] - u[i] <= ramp, + u[i] - u[i + 1] <= ramp] + prob = cp.Problem(objective, constraints) + #result = prob.solve(solver=cp.ECOS_BB, verbose=True) + result = prob.solve(verbose=True) + + return u.value, r.value + + +if __name__ == '__main__': + generator = Generator() + + T = 24 + lam = 40 * np.full((T, 1), 1) + rp = 15 * np.full((T, 1), 1) + c = [3.243, 30] + rc = [2, 10] + plower = 0.3 + pupper = 100 + ramp = 0.5 + u, v = generator.generate(T, lam, c, plower, pupper, ramp, rc, rp) + + print(u) + print(v) + diff --git a/MarketAgents/CityAgent/city/timer.py b/MarketAgents/CityAgent/city/timer.py new file mode 100644 index 0000000..9cb04f4 --- /dev/null +++ b/MarketAgents/CityAgent/city/timer.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2017, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + + +from datetime import datetime + + +class Timer: + created_time = None + sim_one_hr_in_sec = 1200 + sim_start_time = None + simulation = False + + @classmethod + def get_cur_time(cls): + """ + Calculate current time based on the amount of time has passed since this object is created + :return: + """ + cur_time = datetime.now() + if cls.simulation: + ratio = 3600 / cls.sim_one_hr_in_sec + time_diff = cur_time - cls.created_time + simulation_time_diff = time_diff * ratio + cur_time = cls.sim_start_time + simulation_time_diff + + return cur_time + + +if __name__ == '__main__': + from dateutil import parser + + Timer.simulation = True + Timer.created_time = parser.parse("2018-07-18 16:01:00.000") + + Timer.sim_start_time = parser.parse("2018-06-22 00:00:00.000") + print(Timer.get_cur_time()) + + Timer.sim_start_time = parser.parse("2018-07-22 00:00:00.000") + print(Timer.get_cur_time()) + + Timer.simulation = False + print(Timer.get_cur_time()) \ No newline at end of file diff --git a/MarketAgents/CityAgent/city_config b/MarketAgents/CityAgent/city_config new file mode 100755 index 0000000..0396cd0 --- /dev/null +++ b/MarketAgents/CityAgent/city_config @@ -0,0 +1,16 @@ +{ + "agentid": "city_agent", + "name": "City_Of_Richland", + "agent_name": "City_Of_Richland", + "T": 24, + "iteration_threshold": 2000, + "alpha_reserve": 1, + "alpha_energy": 1, + "error_energy_threshold": 0.6, #0.01, + "error_reserve_threshold": 0.6, #0.01, + "error_upper_energy_threshold": 1.0, #0.01, + "error_upper_reserve_threshold": 1.0, #0.01, + "simulation": true, + "simulation_start_time": "2019-09-01 00:00:00", + "simulation_one_hour_in_seconds": 1200 +} diff --git a/MarketAgents/CityAgent/setup.py b/MarketAgents/CityAgent/setup.py new file mode 100644 index 0000000..635162c --- /dev/null +++ b/MarketAgents/CityAgent/setup.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2016, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +from os import path +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = '' +for package in find_packages(): + # Because there could be other packages such as tests + if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: + agent_package = package +if not agent_package: + raise RuntimeError('None of the packages under {dir} contain the file ' + '{main_module}'.format(main_module=MAIN_MODULE + '.py', + dir=path.abspath('.'))) + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) diff --git a/MarketAgents/LightingAgent/light/agent.py b/MarketAgents/LightingAgent/light/agent.py index 45368ef..060342e 100644 --- a/MarketAgents/LightingAgent/light/agent.py +++ b/MarketAgents/LightingAgent/light/agent.py @@ -59,7 +59,7 @@ import sys import logging from volttron.platform.agent import utils -from volttron.pnnl.models.light import Light +from volttron.pnnl.models import Model from volttron.pnnl.transactive_base.transactive.transactive import TransactiveBase @@ -71,7 +71,7 @@ OCC = "occ" -class LightAgent(TransactiveBase, Light): +class LightAgent(TransactiveBase, Model): """ Transactive control lighting agent. """ @@ -84,7 +84,7 @@ def __init__(self, config_path, **kwargs): self.agent_name = config.get("agent_name", "light_control") TransactiveBase.__init__(self, config, **kwargs) model_config = config.get("model_parameters", {}) - Light.__init__(self, model_config, **kwargs) + Model.__init__(self, model_config, **kwargs) self.init_markets() def init_predictions(self, output_info): @@ -104,4 +104,4 @@ def main(): try: sys.exit(main()) except KeyboardInterrupt: - pass \ No newline at end of file + pass diff --git a/MarketAgents/LightingAgent/setup.py b/MarketAgents/LightingAgent/setup.py index 635162c..0836108 100644 --- a/MarketAgents/LightingAgent/setup.py +++ b/MarketAgents/LightingAgent/setup.py @@ -75,7 +75,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/MarketAgents/MeterAgent/meter/agent.py b/MarketAgents/MeterAgent/meter/agent.py index 3d7b619..6933578 100644 --- a/MarketAgents/MeterAgent/meter/agent.py +++ b/MarketAgents/MeterAgent/meter/agent.py @@ -63,7 +63,7 @@ from volttron.platform.agent.base_market_agent.poly_line import PolyLine from volttron.platform.agent.base_market_agent.point import Point -from volttron.pnnl.models.meter import Meter +from volttron.pnnl.models import Model # from pnnl.models.firstorderzone import FirstOrderZone @@ -72,7 +72,7 @@ __version__ = "0.2" -class MeterAgent(Aggregator, Meter): +class MeterAgent(Aggregator, Model): """ The SampleElectricMeterAgent serves as a sample of an electric meter that sells electricity for a single building at a fixed price. @@ -86,7 +86,7 @@ def __init__(self, config_path, **kwargs): self.agent_name = config.get("agent_name", "meter") Aggregator.__init__(self, config, **kwargs) model_parms = config.get("model_parameters", {}) - Meter.__init__(self, model_parms, **kwargs) + Model.__init__(self, model_parms, **kwargs) self.price = None self.init_markets() diff --git a/MarketAgents/MeterAgent/setup.py b/MarketAgents/MeterAgent/setup.py index 635162c..0836108 100644 --- a/MarketAgents/MeterAgent/setup.py +++ b/MarketAgents/MeterAgent/setup.py @@ -75,7 +75,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/MarketAgents/RTUAgent/rtu/agent.py b/MarketAgents/RTUAgent/rtu/agent.py index 78e97bd..d2a86ca 100644 --- a/MarketAgents/RTUAgent/rtu/agent.py +++ b/MarketAgents/RTUAgent/rtu/agent.py @@ -61,15 +61,17 @@ from datetime import timedelta as td from volttron.platform.agent import utils -from volttron.pnnl.models.rtu import RTU + +from volttron.pnnl.models import Model from volttron.pnnl.transactive_base.transactive.transactive import TransactiveBase + _log = logging.getLogger(__name__) utils.setup_logging() __version__ = "0.3" -class RTUAgent(TransactiveBase, RTU): +class RTUAgent(TransactiveBase, Model): """ TCC RTU agent """ @@ -81,7 +83,9 @@ def __init__(self, config_path, **kwargs): self.agent_name = config.get("agent_name", "rtu") TransactiveBase.__init__(self, config, **kwargs) model_config = config.get("model_parameters", {}) - RTU.__init__(self, model_config, **kwargs) + + Model.__init__(self, model_config, **kwargs) + self.agent_name = config.get("agent_name") self.init_markets() def init_predictions(self, output_info): diff --git a/MarketAgents/RTUAgent/setup.py b/MarketAgents/RTUAgent/setup.py index 635162c..0836108 100644 --- a/MarketAgents/RTUAgent/setup.py +++ b/MarketAgents/RTUAgent/setup.py @@ -75,7 +75,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/MarketAgents/TCC_agent/setup.py b/MarketAgents/TCC_agent/setup.py new file mode 100644 index 0000000..36563a7 --- /dev/null +++ b/MarketAgents/TCC_agent/setup.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2016, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +from os import path +from setuptools import setup, find_packages + +MAIN_MODULE = 'tcc' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = '' +for package in find_packages(): + # Because there could be other packages such as tests + if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: + agent_package = package +if not agent_package: + raise RuntimeError('None of the packages under {dir} contain the file ' + '{main_module}'.format(main_module=MAIN_MODULE + '.py', + dir=path.abspath('.'))) + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) diff --git a/MarketAgents/TCC_agent/tcc/__init__.py b/MarketAgents/TCC_agent/tcc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MarketAgents/TCC_agent/tcc/tcc.py b/MarketAgents/TCC_agent/tcc/tcc.py new file mode 100644 index 0000000..e9687ba --- /dev/null +++ b/MarketAgents/TCC_agent/tcc/tcc.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2018, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +import sys +import gevent +from dateutil.parser import parse +import logging +from volttron.platform.agent import utils +from volttron.pnnl.transactive_base.transactive.aggregator_base import Aggregator +from volttron.pnnl.transactive_base.transactive.transactive import TransactiveBase +from volttron.platform.agent.base_market_agent.poly_line import PolyLine +from volttron.platform.agent.base_market_agent.point import Point +from volttron.platform.agent.base_market_agent.poly_line_factory import PolyLineFactory +from volttron.pnnl.models import Model +from .tcc_market import PredictionManager +from volttron.platform.vip.agent import Agent, Core +from volttron.platform.agent.math_utils import mean, stdev +from volttron.platform.agent.base_market_agent.buy_sell import BUYER, SELLER + +_log = logging.getLogger(__name__) +utils.setup_logging() +__version__ = "0.1" + + +class TCCAgent(TransactiveBase): + """ + The TESS Agent participates in Electricity Market as consumer of electricity at fixed price. + It participates in internal Chilled Water market as supplier of chilled water at fixed price. + """ + + def __init__(self, config_path, **kwargs): + try: + config = utils.load_config(config_path) + except Exception.StandardError: + config = {} + tcc_config_directory = config.get("tcc_config_directory", "tcc_config.json") + try: + self.tcc_config = utils.load_config(tcc_config_directory) + except: + _log.error("Could not locate tcc_config_directory in tess config file!") + self.core.stop() + self.first_day = True + self.iteration_count = 0 + self.tcc = None + self.agent_name = config.get("agent_name", "tess_agent") + model_config = config.get("model_parameters", {}) + TransactiveBase.__init__(self, config, **kwargs) + self.numHours = 24 + self.cooling_load = [None] * self.numHours + self.init_markets() + self.indices = [None] * self.numHours + self.model = None + self.cooling_load_copy = self.cooling_load[:] + + @Core.receiver('onstart') + def onstart(self, sender, **kwargs): + _log.debug("TCC onstart") + # Subscriptions + self.vip.pubsub.subscribe(peer='pubsub', + prefix='mixmarket/make_tcc_predictions', + callback=self.make_tcc_predictions) + self.tcc = PredictionManager(self.tcc_config) + + def offer_callback(self, timestamp, market_name, buyer_seller): + # Verify that all tcc predictions for day have finished + while self.tcc.calculating_predictions: + _log.debug("SLEEP") + gevent.sleep(0.1) + + _log.debug("PRICES: {}".format(self.market_prices)) + for idx in range(24): + price = self.market_prices[idx] + self.cooling_load[idx] = self.tcc.chilled_water_demand[idx].x(price) + + self.vip.pubsub.publish(peer='pubsub', + topic='tcc/cooling_demand', + message=self.cooling_load, + headers={}) + # Only need to do this once so return on all + # other call backs + if market_name != self.market_name[0]: + return + # Check just for good measure + _log.debug("market_prices = {}".format(self.market_prices)) + _log.debug("oat_predictions = {}".format(self.oat_predictions)) + _log.debug("cooling_load = {}".format(self.cooling_load)) + T_out = [-0.05 * (t - 14.0) ** 2 + 30.0 for t in range(1, 25)] + # do optimization to obtain power and reserve power + self.cooling_load = [None] * self.numHours + for i in range(0, len(self.market_prices)): + self.make_offer(self.market_name[i], buyer_seller, self.tcc.electric_demand[i]) + _log.debug("TCC hour {}-- electric demand {}".format(i, self.tcc.electric_demand[i].points)) + + def make_tcc_predictions(self, peer, sender, bus, topic, headers, message): + """ + Run tcc predictions for building electric and chilled water market. + Message sent by campus agent on topic 'mixmarket/make_tcc_predictions'. + + message = dict; { + "converged": bool - market converged + "prices": array - next days 24 hour hourly demand prices, + "reserved_prices": array - next days 24 hour hourly reserve prices + "start_of_cycle": bool - start of cycle + "hour": int for current hour, + "prediction_date": string - prediction date, + "temp": array - next days 24 hour hourly outdoor temperature predictions + } + """ + self.tcc.calculating_predictions = True + new_cycle = message["start_of_cycle"] + if new_cycle and self.first_day and self.iteration_count > 0: + self.first_day = False + self.iteration_count = 1 + elif new_cycle: + self.iteration_count = 1 + else: + self.iteration_count += 1 + _log.debug("new_cycle %s - first_day %s - iteration_count %s", + new_cycle, self.first_day, self.iteration_count) + oat_predictions = message["temp"] + prices = message["prices"] + _date = parse(message["prediction_date"]) + self.tcc.do_predictions(prices, oat_predictions, _date, new_cycle=new_cycle, first_day=self.first_day) + + def determine_control(self, prices, price): + _log.debug("TCC DO Control!") + for ahu, vav_list in self.tcc.ahus.items(): + # Assumes all devices are on same occupancy schedule. Potential problem + for vav in vav_list: + occupied = self.tcc.market_container.container[vav].check_current_schedule(self.current_datetime) + _log.debug("TCC DO Control! {} -- occupied: {}".format(vav, occupied)) + actuator = self.tcc.market_container.container[vav].model.actuator + point_topic = self.tcc.market_container.container[vav].model.ct_topic + price_array = self.tcc.market_container.container[vav].determine_prices(prices) + value = self.tcc.market_container.container[vav].model.determine_set(price_array, price) + if occupied: + self.vip.rpc.call(actuator, + 'set_point', + self.core.identity, + point_topic, + value).get(timeout=15) + for light in self.tcc.lights: + actuator = self.tcc.market_container.container[light].model.actuator + point_topic = self.tcc.market_container.container[light].model.ct_topic + price_array = self.tcc.market_container.container[vav].determine_prices(prices) + value = self.tcc.market_container.container[light].model.determine_set(price_array, price) + occupied = self.tcc.market_container.container[light].check_current_schedule(self.current_datetime) + _log.debug("TCC DO Control! {} -- occupied: {}".format(light, occupied)) + if occupied: + self.vip.rpc.call(actuator, + 'set_point', + self.core.identity, + point_topic, + value).get(timeout=15) + + def do_actuation(self, price=None): + if not self.market_converged: + _log.debug("Market not converged!") + return + + if self.current_datetime is None: + _log.debug("No time information available, check input topics!") + return + + _hour = self.current_datetime.hour + if self.market_prices is None: + _log.debug("No market prices") + return + + price = self.market_prices[_hour] + prices = self.market_prices + self.determine_control(prices, price) + + def update_state(self, market_index, sched_index, price): + pass + + +def main(): + """Main method called to start the agent.""" + utils.vip_main(TCCAgent, version=__version__) + + +if __name__ == '__main__': + # Entry point for script + try: + sys.exit(main()) + except KeyboardInterrupt: + pass diff --git a/MarketAgents/TCC_agent/tcc/tcc_market.py b/MarketAgents/TCC_agent/tcc/tcc_market.py new file mode 100644 index 0000000..96147e4 --- /dev/null +++ b/MarketAgents/TCC_agent/tcc/tcc_market.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2018, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} +import json +import datetime +from .tcc_models import Model +from volttron.platform.agent import utils +import numpy as np +from volttron.platform.agent.base_market_agent.poly_line import PolyLine +from volttron.platform.agent.base_market_agent.poly_line_factory import PolyLineFactory +from volttron.platform.agent.base_market_agent.point import Point + + +class MarketContainer(object): + def __init__(self): + self.container = {} + + def add_member(self, tag, config_path): + config = utils.load_config(config_path) + self.container[tag] = Model(config, self) + + +class PredictionManager(object): + def __init__(self, config): + self.price_multiplier = config.get("price_multiplier", 1.0) + self.calculating_predictions = False + self.prices = None + self.previous_days_prices = None + self.hourly_electric_demand = [] + self.hourly_chilled_water_demand = [] + + self.electric_demand = {} + self.chilled_water_demand = {} + self.market_container = MarketContainer() + config_directory = config.pop("config_directory") + self.ahus = config.get("AHU", {}) + self.lights = config.get("LIGHT", {}) + + for ahu, vav_list in self.ahus.items(): + for vav in vav_list: + print("VAV: {}".format(vav)) + config_path = "/".join([config_directory, vav + ".config"]) + self.market_container.add_member(vav, config_path) + config_path = "/".join([config_directory, ahu + ".config"]) + self.market_container.add_member(ahu, config_path) + for light in self.lights: + config_path = "/".join([config_directory, light + ".config"]) + self.market_container.add_member(light, config_path) + + def do_predictions(self, prices, oat_predictions, _date, new_cycle=False, first_day=False): + """ + Do 24 hour predictions for all devices. + :param prices: + :param oat_predictions: + :param new_cycle: + :param first_day: + :return: + """ + + # Needs to know first days iterations. Will use + # projected prices on first day. After first day will + # use previous 24-hours prices + self.calculating_predictions = True + if first_day: + self.prices = self.determine_prices(prices) + if self.prices.size and not first_day and new_cycle: + self.previous_days_prices = self.prices + price_range = self.previous_days_prices if self.previous_days_prices is not None else self.prices + self.electric_demand = {} + self.chilled_water_demand = {} + for _hour in range(24): + price = prices[_hour] + oat = oat_predictions[_hour] + self.hourly_electric_demand = [] + self.hourly_chilled_water_demand = [] + for ahu, vav_list in self.ahus.items(): + self.market_container.container[ahu].model.air_demand = [] + for vav in vav_list: + occupied = self.market_container.container[vav].check_schedule(_date, _hour) + air_demand = self.market_container.container[vav].model.create_demand_curve(price_range, price, oat, _hour, occupied, new_cycle) + self.market_container.container[ahu].model.air_demand.append(air_demand) + + self.market_container.container[ahu].model.aggregate_load() + self.hourly_electric_demand.append(self.market_container.container[ahu].model.create_electric_demand()) + self.hourly_chilled_water_demand.append(self.market_container.container[ahu].model.create_chilled_water_demand(oat)) + + for light in self.lights: + occupied = self.market_container.container[light].check_schedule(_date, _hour) + electric_demand = self.market_container.container[light].model.create_demand_curve(price_range, price, occupied) + self.hourly_electric_demand.append(electric_demand) + + self.electric_demand[_hour] = self.aggregate_load(self.hourly_electric_demand) + self.chilled_water_demand[_hour] = self.aggregate_load(self.hourly_chilled_water_demand) + print("hour {} - electric_demand: {}".format(_hour, self.electric_demand[_hour].points)) + print("hour {} - chilled_water: {}".format(_hour, self.chilled_water_demand[_hour].points)) + self.prices = self.determine_prices(prices) + self.calculating_predictions = False + + def determine_prices(self, _prices): + """ + Uses 24 hour prices to determine minimum and maximum price + for construction of demand curve. + :param _prices: + :return: + """ + avg_price = np.mean(_prices) + std_price = np.std(_prices) + price_min = avg_price - self.price_multiplier * std_price + price_max = avg_price + self.price_multiplier * std_price + price_array = np.linspace(price_min, price_max, 11) + return price_array + + @staticmethod + def aggregate_load(curves): + aggregate_curve = PolyLineFactory.combine(curves, 11) + return aggregate_curve + + +# x = PredictionManager() +# message = [{"MixedAirTemperature": 23, "ReturnAirTemperature": 25, "DischargeAirTemperature": 13}, {}] +# x.market_container.container["AHU1"].update_data(None, None, None, None, None, message) +# _prices = np.linspace(0.035, 0.06, 24) +# oat_predictions = [25]*24 +# new_cycle=True +# first_day=True +# x.do_predictions(_prices, oat_predictions, datetime.datetime.now(), new_cycle=new_cycle, first_day=first_day) diff --git a/MarketAgents/TCC_agent/tcc/tcc_models.py b/MarketAgents/TCC_agent/tcc/tcc_models.py new file mode 100644 index 0000000..979865c --- /dev/null +++ b/MarketAgents/TCC_agent/tcc/tcc_models.py @@ -0,0 +1,489 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2018, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} +from dateutil.parser import parse +from volttron.pnnl.models import input_names as data_names +from volttron.platform.agent.base_market_agent.poly_line import PolyLine +from volttron.platform.agent.base_market_agent.poly_line_factory import PolyLineFactory +from volttron.platform.agent.base_market_agent.point import Point +import sys +import numpy as np + + +def clamp(value, x1, x2): + min_value = min(x1, x2) + max_value = max(x1, x2) + return min(max(value, min_value), max_value) + + +class Model(object): + def __init__(self, config, parent): + + outputs = config.get("outputs") + schedule = config.get("schedule", {}) + model_parms = config.get("model_parameters") + self.input_topics = set() + self.inputs = {} + self.parent = parent + self.price_multiplier = config.get("price_multiplier", 1.0) + if model_parms is None: + print("No model parms") + return + model_type = model_parms.get("model_type") + self.schedule = {} + self.init_schedule(schedule) + # Parse inputs from config + # these could be used to extend this standalone code + # for single timestep + inputs = config.get("inputs") + for input_info in inputs: + try: + point = input_info["point"] + mapped = input_info["mapped"] + topic = input_info["topic"] + except KeyError as ex: + print("Exception on init_inputs %s", ex) + sys.exit() + value = input_info.get("initial_value") + self.inputs[mapped] = {point: value} + self.input_topics.add(topic) + if model_type == "vav.firstorderzone": + flexibility = outputs[0].get("flexibility_range") + control_flexibility = outputs[0].get("control_flexibility") + off_setpoint = outputs[0].get("off_setpoint") + ct_topic = outputs[0].get("topic") + actuator = outputs[0].get("actuator") + + min_flow = float(flexibility[1]) + max_flow = float(flexibility[0]) + tmin = float(control_flexibility[0]) + tmax = float(control_flexibility[1]) + + model_parms.update({"min_flow": min_flow}) + model_parms.update({"max_flow": max_flow}) + model_parms.update({"actuator": actuator}) + + model_parms.update({"tmin": tmin}) + model_parms.update({"tmax": tmax}) + model_parms.update({"ct_topic": ct_topic}) + + model_parms.update({"off_setpoint": off_setpoint}) + self.model = FirstOrderZone(model_parms) + if model_type == "ahuchiller.ahuchiller": + self.model = Ahu(model_parms) + + if model_type == "light.simple_profile": + ct_topic = outputs[0].get("topic") + model_parms.update({"ct_topic": ct_topic}) + flexibility = outputs[0].get("flexibility_range") + min_lighting = float(flexibility[1]) + max_lighting = float(flexibility[0]) + off_setpoint = outputs[0].get("off_setpoint") + model_parms.update({"dol_min": min_lighting}) + model_parms.update({"dol_max": max_lighting}) + model_parms.update({"off_setpoint": off_setpoint}) + actuator = outputs[0].get("actuator") + model_parms.update({"actuator": actuator}) + self.model = LightSimple(model_parms) + + def determine_prices(self, _prices): + """ + Uses 24 hour prices to determine minimum and maximum price + for construction of demand curve. + :param _prices: + :return: + """ + avg_price = np.mean(_prices) + std_price = np.std(_prices) + price_min = avg_price - self.price_multiplier * std_price + price_max = avg_price + self.price_multiplier * std_price + price_array = np.linspace(price_min, price_max, 11) + return price_array + + def init_subscriptions(self): + """ + Create topic subscriptions for devices. + :return: + """ + for topic in self.input_topics: + print('Subscribing to: ' + topic) + self.parent.vip.pubsub.subscribe(peer='pubsub', + prefix=topic, + callback=self.update_data) + + def update_data(self, peer, sender, bus, topic, headers, message): + """ + Each time data comes in on message bus from MasterDriverAgent + update the data for access by model. + :param data: dict; key value pairs from master driver. + :return: + """ + data = message[0] + print("inputs: {}".format(self.inputs)) + for name, input_data in self.inputs.items(): + print(name, input_data) + for point, value in input_data.items(): + if point in data: + self.inputs[name][point] = data[point] + self.model.update_inputs(self.inputs) + + def init_schedule(self, schedule): + """ + Parse schedule for use in determining occupancy. + :param schedule: + :return: + """ + if schedule: + for day_str, schedule_info in schedule.items(): + _day = parse(day_str).weekday() + if schedule_info not in ["always_on", "always_off"]: + start = parse(schedule_info["start"]).time() + end = parse(schedule_info["end"]).time() + self.schedule[_day] = {"start": start, "end": end} + else: + self.schedule[_day] = schedule_info + + def check_schedule(self, dt, _hour): + """ + Check if the hour/day for current prediction is scheduled + as occupied. If no schedule is provided all times are + considered as occupied. + :param dt: + :param _hour: + :return: + """ + if not self.schedule: + occupied = True + return occupied + current_schedule = self.schedule[dt.weekday()] + if "always_on" in current_schedule: + occupied = True + return occupied + if "always_off" in current_schedule: + occupied = False + return occupied + _start = current_schedule["start"] + _end = current_schedule["end"] + prediction_time = dt.replace(hour=_hour, minute=0, second=0).time() + if _start <= prediction_time < _end: + occupied = True + else: + occupied = False + return occupied + + def check_current_schedule(self, _dt): + """ + Check if the hour/day for current prediction is scheduled + as occupied. If no schedule is provided all times are + considered as occupied. + :param dt: + :param _hour: + :return: + """ + if not self.schedule: + occupied = True + return occupied + current_schedule = self.schedule[_dt.weekday()] + if "always_on" in current_schedule: + occupied = True + return occupied + if "always_off" in current_schedule: + occupied = False + return occupied + _start = current_schedule["start"] + _end = current_schedule["end"] + if _start <= _dt.time() < _end: + occupied = True + else: + occupied = False + return occupied + +class FirstOrderZone(object): + """VAV firstorder zone tcc model.""" + def __init__(self, model_parms): + self.mDotMin = model_parms['min_flow'] + self.mDotMax = model_parms['max_flow'] + self.tMinAdj = model_parms['tmin'] + self.tMaxAdj = model_parms['tmax'] + try: + self.a1 = model_parms['a1'] + self.a2 = model_parms['a2'] + self.a3 = model_parms['a3'] + self.a4 = model_parms['a4'] + except KeyError: + print("Missing FirstOrderZone model parameter!") + sys.exit() + + self.oat_name = data_names.OAT + self.sfs_name = data_names.SFS + self.zt_name = data_names.ZT + self.zdat_name = data_names.ZDAT + self.zaf_name = data_names.ZAF + + self.off_setpoint = model_parms.get("off_setpoint", 26.0) + self.ct_topic = model_parms['ct_topic'] + self.actuator = model_parms.get("actuator", "platform.actuator") + + self.vav_flag = model_parms.get("vav_flag", True) + if self.vav_flag: + self.get_q = self.getM + else: + self.get_q = self.getdT + + self.sets = np.linspace(self.tMinAdj, self.tMaxAdj, 11) + self.tNomAdj = np.mean(self.sets) + self.tIn = [self.tNomAdj] * 24 + self.tpre = self.tNomAdj + self.name = "FirstOrderZone" + + def getM(self, oat, temp, temp_stpt, index): + M = temp_stpt*self.a1[index]+temp*self.a2[index]+oat*self.a3[index]+self.a4[index] + M = clamp(M, self.mDotMin, self.mDotMax) + return M + + def getdT(self, oat, temp, temp_stpt, index): + dT = temp_stpt * self.a1[index] + temp * self.a2[index] + oat * self.a3[index] + self.a4[index] + return dT + + def create_demand_curve(self, prices, price, oat, _hour, occupied, new_cycle): + curve = PolyLine() + + if new_cycle: + self.tpre = self.tIn[-1] + if _hour == 0: + temp = self.tpre + else: + temp = self.tIn[_hour-1] + for _price in prices: + if occupied: + tset = self.determine_set(prices, _price) + q = self.get_q(oat, temp, tset, _hour) + else: + tset = self.off_setpoint + q = 0 + print("VAV MODEL : {} - {} - {} - {}".format(temp, tset, _hour, q)) + curve.add(Point(q, _price)) + print("VAV MODEL2 : {} - {}".format(_hour, curve.points)) + self.tIn[_hour] = tset + return curve + + def determine_set(self, prices, price): + """ + prices is an list of 11 elements, evenly spaced from the smallest price + to the largest price and corresponds to the y-values of a line. sets + is an np.array of 11 elements, evenly spaced from the control value at + the lowest price to the control value at the highest price and + corresponds to the x-values of a line. Price is the cleared price. + :param sets: np.array; + :param prices: list; + :param price: float + :return: + """ + tset = np.interp(price, prices, self.sets) + return tset + + def update_inputs(self, inputs): + """ + For injection of realtime data from building. Not needed for + day ahead demand predictions. + :param inputs: + :return: + """ + pass + + +class Ahu(object): + """AHU tcc model.""" + def __init__(self, model_parms): + self.air_demand = [] + self.aggregate_air_demand = [] + self.aggregate_chilled_water_demand = [] + equipment_conf = model_parms.get("equipment_configuration") + model_conf = model_parms.get("model_configuration") + self.cpAir = model_conf["cpAir"] + self.c0 = model_conf['c0'] + self.c1 = model_conf['c1'] + self.c2 = model_conf['c2'] + self.c3 = model_conf['c3'] + self.cop = model_conf['COP'] + self.mDotAir = model_conf['mDotAir'] + self.vav_flag = model_conf.get("vav_flag", True) + + self.has_economizer = equipment_conf["has_economizer"] + self.economizer_limit = equipment_conf["economizer_limit"] + self.min_oaf = equipment_conf.get("minimum_oaf", 0.15) + self.vav_flag = equipment_conf.get("variable-volume", True) + self.sat_setpoint = equipment_conf["supply-air sepoint"] + self.tDis = self.sat_setpoint + self.building_chiller = equipment_conf["building chiller"] + self.tset_avg = equipment_conf["nominal zone-setpoint"] + self.power_unit = model_conf.get("unit_power", "kw") + self.mDotAir = model_conf["mDotAir"] + + self.fan_power = 0. + self.coil_load = 0. + self.name = 'AhuChiller' + + self.sfs_name = data_names.SFS + self.mat_name = data_names.MAT + self.dat_name = data_names.DAT + self.saf_name = data_names.SAF + self.oat_name = data_names.OAT + self.rat_name = data_names.RAT + + def update_inputs(self, inputs): + pass + + def input_zone_load(self, q_load): + if self.vav_flag: + self.mDotAir = q_load + else: + self.tDis = q_load + + def calculate_electric_load(self): + return self.calculate_fan_power() + + def calculate_fan_power(self): + if self.power_unit == 'W': + fan_power = (self.c0 + self.c1 * self.mDotAir + self.c2 * pow(self.mDotAir, 2) + self.c3 * pow(self.mDotAir, 3)) + else: + fan_power = self.c0 + self.c1 * self.mDotAir + self.c2 * pow(self.mDotAir, 2) + self.c3 * pow(self.mDotAir, 3) + return fan_power + + def calculate_coil_load(self, oat): + if self.has_economizer: + if oat < self.tDis: + coil_load = 0.0 + elif oat < self.economizer_limit: + coil_load = self.mDotAir * self.cpAir * (self.tDis - oat) + else: + mat = self.tset_avg * (1.0 - self.min_oaf) + self.min_oaf * oat + coil_load = self.mDotAir * self.cpAir * (self.tDis - mat) + else: + mat = self.tset_avg * (1.0 - self.min_oaf) + self.min_oaf * oat + coil_load = self.mDotAir * self.cpAir * (self.tDis - mat) + + if coil_load > 0: # heating mode is not yet supported! + coil_load = 0.0 + return coil_load + + def aggregate_load(self): + aggregate_curve = PolyLineFactory.combine(self.air_demand, 11) + self.aggregate_air_demand = aggregate_curve + + def create_electric_demand(self): + electric_demand_curve = PolyLine() + for point in self.aggregate_air_demand.points: + self.input_zone_load(point.x) + electric_demand_curve.add(Point(price=point.y, quantity=self.calculate_electric_load()/1000.0)) + return electric_demand_curve + + def create_chilled_water_demand(self, oat): + chilled_water_demand = PolyLine() + print("AGG AIR: {}".format(self.aggregate_air_demand.points)) + for point in self.aggregate_air_demand.points: + self.input_zone_load(point.x) + + chilled_water_demand.add(Point(price=point.y, quantity=self.calculate_coil_load(oat))) + return chilled_water_demand + + +class LightSimple(object): + """ + Lighting model for tcc standalone. + """ + def __init__(self, model_parms, **kwargs): + self.rated_power = model_parms["rated_power"] + min_lighting = model_parms.get("dol_min", 0.7) + max_lighting = model_parms.get("dol_min", 0.9) + self.off_setpoint = model_parms.get("off_setpoint") + self.sets = np.linspace(max_lighting, min_lighting, 11) + self.get_q = self.predict + self.ct_topic = model_parms['ct_topic'] + self.actuator = model_parms.get("actuator", "platform.actuator") + + def update_inputs(self, inputs): + pass + + def create_demand_curve(self, prices, _price, occupied): + if occupied: + _set = self.determine_set(prices, _price) + else: + _set = self.off_setpoint + curve = PolyLine() + for _price in prices: + curve.add(Point(self.get_q(_set), _price)) + return curve + + def predict(self, _set): + return _set*self.rated_power/1000.0 + + def determine_set(self, prices, price): + """ + prices is an list of 11 elements, evenly spaced from the smallest price + to the largest price and corresponds to the y-values of a line. sets + is an np.array of 11 elements, evenly spaced from the control value at + the lowest price to the control value at the highest price and + corresponds to the x-values of a line. Price is the cleared price. + :param sets: np.array; + :param prices: list; + :param price: float + :return: + """ + tset = np.interp(price, prices, self.sets) + return tset + diff --git a/MarketAgents/TCC_agent/tcc_config.json b/MarketAgents/TCC_agent/tcc_config.json new file mode 100644 index 0000000..7aa2b5c --- /dev/null +++ b/MarketAgents/TCC_agent/tcc_config.json @@ -0,0 +1,61 @@ +{ + "config_directory": "/home/volttron/transactivecontrol/MarketAgents/config/building1", + "AHU": { + "AHU1": [ + "100_CLGSETP", + "102_CLGSETP", + "118_CLGSETP", + "119_CLGSETP", + "120_CLGSETP", + "121_CLGSETP", + "123A_CLGSETP", + "123B_CLGSETP", + "127A_CLGSETP", + "127B_CLGSETP", + "129_CLGSETP", + "131_CLGSETP", + "136_CLGSETP", + "133_CLGSETP", + "142_CLGSETP", + "143_CLGSETP", + "150_CLGSETP" + ], + "AHU2": [ + "002_CLGSETP" + ], + "AHU3": [ + "104_CLGSETP", + "105_CLGSETP", + "108_CLGSETP", + "112_CLGSETP", + "107_CLGSETP", + "116_CLGSETP" + ], + "AHU4": [ + "004_CLGSETP" + ] + }, + "LIGHT": [ + "102_LIGHT", + "118_LIGHT", + "119_LIGHT", + "120_LIGHT", + "123A_LIGHT", + "123B_LIGHT", + "127A_LIGHT", + "127B_LIGHT", + "129_LIGHT", + "131_LIGHT", + "136_LIGHT", + "133_LIGHT", + "142_LIGHT", + "143_LIGHT", + "150_LIGHT", + "104_LIGHT", + "105_LIGHT", + "108_LIGHT", + "112_LIGHT", + "107_LIGHT", + "116_LIGHT" + ] +} \ No newline at end of file diff --git a/MarketAgents/TCC_agent/tcc_config2.json b/MarketAgents/TCC_agent/tcc_config2.json new file mode 100644 index 0000000..39c6b57 --- /dev/null +++ b/MarketAgents/TCC_agent/tcc_config2.json @@ -0,0 +1,10 @@ +{ + "config_directory": "/home/volttron/transactivecontrol/MarketAgents/config/building1", + "AHU": { + "AHU3": [ + "104_CLGSETP", + "105_CLGSETP" + ] + } +} + \ No newline at end of file diff --git a/MarketAgents/TESSAgent/setup.py b/MarketAgents/TESSAgent/setup.py new file mode 100644 index 0000000..635162c --- /dev/null +++ b/MarketAgents/TESSAgent/setup.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2016, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +from os import path +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = '' +for package in find_packages(): + # Because there could be other packages such as tests + if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: + agent_package = package +if not agent_package: + raise RuntimeError('None of the packages under {dir} contain the file ' + '{main_module}'.format(main_module=MAIN_MODULE + '.py', + dir=path.abspath('.'))) + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) diff --git a/MarketAgents/TESSAgent/tess/__init__.py b/MarketAgents/TESSAgent/tess/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MarketAgents/TESSAgent/tess/agent.py b/MarketAgents/TESSAgent/tess/agent.py new file mode 100644 index 0000000..f935f56 --- /dev/null +++ b/MarketAgents/TESSAgent/tess/agent.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2018, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +import sys +import gevent +from dateutil.parser import parse +import logging +from volttron.platform.agent import utils +from volttron.pnnl.transactive_base.transactive.aggregator_base import Aggregator +from volttron.pnnl.transactive_base.transactive.transactive import TransactiveBase +from volttron.platform.agent.base_market_agent.poly_line import PolyLine +from volttron.platform.agent.base_market_agent.point import Point +from volttron.platform.agent.base_market_agent.poly_line_factory import PolyLineFactory +from volttron.pnnl.models import Model +from volttron.platform.vip.agent import Agent, Core +from volttron.platform.agent.math_utils import mean, stdev +from volttron.platform.agent.base_market_agent.buy_sell import BUYER, SELLER + +_log = logging.getLogger(__name__) +utils.setup_logging() +__version__ = "0.1" + + +class TESSAgent(TransactiveBase, Model): + """ + The TESS Agent participates in Electricity Market as consumer of electricity at fixed price. + It participates in internal Chilled Water market as supplier of chilled water at fixed price. + """ + + def __init__(self, config_path, **kwargs): + try: + config = utils.load_config(config_path) + except Exception.StandardError: + config = {} + tcc_config_directory = config.get("tcc_config_directory", "tcc_config.json") + try: + self.tcc_config = utils.load_config(tcc_config_directory) + except: + _log.error("Could not locate tcc_config_directory in tess config file!") + self.core.stop() + self.first_day = True + self.iteration_count = 0 + self.tcc = None + self.agent_name = config.get("agent_name", "tess_agent") + model_config = config.get("model_parameters", {}) + TransactiveBase.__init__(self, config, **kwargs) + Model.__init__(self, model_config, **kwargs) + self.numHours = 24 + self.cooling_load = [None] * self.numHours + self.init_markets() + self.indices = [None] * self.numHours + self.cooling_load_copy = self.cooling_load[:] + + @Core.receiver('onstart') + def onstart(self, sender, **kwargs): + _log.debug("TESS onstart") + # Subscriptions + self.vip.pubsub.subscribe(peer='pubsub', + prefix='mixmarket/calculate_demand', + callback=self._calculate_demand) + self.vip.pubsub.subscribe(peer='pubsub', + prefix='tcc/cooling_demand', + callback=self.calculate_load) + + def offer_callback(self, timestamp, market_name, buyer_seller): + if market_name != self.market_name[-1]: + return + while None in self.cooling_load: + gevent.sleep(0.1) + _log.debug("TESS: market_prices = {}".format(self.market_prices)) + _log.debug("TESS: reserve_market_prices = {}".format(self.reserve_market_prices)) + _log.debug("TESS: oat_predictions = {}".format(self.oat_predictions)) + _log.debug("TESS: cooling_load = {}".format(self.cooling_load)) + T_out = [-0.05 * (t - 14.0) ** 2 + 30.0 for t in range(1, 25)] + # do optimization to obtain power and reserve power + tess_power_inject, tess_power_reserve = self.model.run_tess_optimization(self.market_prices, + self.reserve_market_prices, + self.oat_predictions, + # T_out, + self.cooling_load) + tess_power_inject = [i * -1 for i in tess_power_inject] + _log.debug("TESS: offer_callback tess_power_inject: {}, tess_power_reserve: {}".format(tess_power_inject, + tess_power_reserve)) + price_min, price_max = self.determine_price_min_max() + _log.debug("TESS: price_min: {}, price_max: {}".format(price_min, price_max)) + for i in range(0, len(self.market_prices)): + electric_demand_curve = PolyLine() + electric_demand_curve.add(Point(tess_power_inject[i], price_min)) + electric_demand_curve.add(Point(tess_power_inject[i], price_max)) + self.make_offer(self.market_name[i], BUYER, electric_demand_curve) + self.vip.pubsub.publish(peer='pubsub', + topic='mixmarket/reserve_demand', + message={ + "reserve_power": list(tess_power_reserve), + "sender": self.agent_name + }) + self.cooling_load = [None] * self.numHours + + + def calculate_load(self, peer, sender, bus, topic, headers, message): + # Verify that all tcc predictions for day have finished + _log.debug("PRICES: {}".format(self.market_prices)) + for idx in range(24): + price = self.market_prices[idx] + self.cooling_load[idx] = message[idx] + + def _calculate_demand(self, peer, sender, bus, topic, headers, message): + """ + + """ + prices = message['prices'] + reserve_prices = message['reserve_prices'] + tess_power_inject, tess_power_reserve = self.model.run_tess_optimization(prices, + reserve_prices, + self.oat_predictions, + self.cooling_load_copy) + tess_power_inject = [i * -1 for i in tess_power_inject] + self.vip.pubsub.publish(peer='pubsub', + topic='mixmarket/tess_bess_demand', + message={ + "power": tess_power_inject, + "reserve_power": tess_power_reserve, + "sender": self.agent_name + }) + + def determine_control(self, sets, prices, price): + # I cannot find this method anywhere + return self.model.calculate_control(self.current_datetime, self.cooling_load_copy) + + def determine_price_min_max(self): + """ + Determine minimum and maximum price from 24-hour look ahead prices. If the TNS + market architecture is not utilized, this function must be overwritten in the child class. + :return: + """ + prices = self.determine_prices() + price_min = prices[0] + price_max = prices[len(prices)-1] + return price_min, price_max + + def update_state(self, market_index, sched_index, price): + pass + + +def main(): + """Main method called to start the agent.""" + utils.vip_main(TESSAgent, version=__version__) + + +if __name__ == '__main__': + # Entry point for script + try: + sys.exit(main()) + except KeyboardInterrupt: + pass diff --git a/MarketAgents/UnControlLoadAgent/setup.py b/MarketAgents/UnControlLoadAgent/setup.py index 635162c..0836108 100644 --- a/MarketAgents/UnControlLoadAgent/setup.py +++ b/MarketAgents/UnControlLoadAgent/setup.py @@ -75,7 +75,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/MarketAgents/UnControlLoadAgent/uncontrol/agent.py b/MarketAgents/UnControlLoadAgent/uncontrol/agent.py index 1bca94e..929a522 100644 --- a/MarketAgents/UnControlLoadAgent/uncontrol/agent.py +++ b/MarketAgents/UnControlLoadAgent/uncontrol/agent.py @@ -105,8 +105,8 @@ def uncontrol_agent(config_path, **kwargs): default_min_price = config.get('default_min_price', 0.01) default_max_price = config.get('default_min_price', 100.0) for i in range(24): - market_name.append('_'.join([base_name, str(i)])) q_uc.append(float(config.get("power_" + str(i), 0))) + market_name.append('_'.join([base_name, str(i)])) verbose_logging = config.get('verbose_logging', True) building_topic = topics.DEVICES_VALUE(campus=config.get("campus", ""), @@ -195,12 +195,7 @@ def conversion_handler(self, conversion, points, point_list): return float(expr.subs(point_list)) def determine_load_index(self, index): - if self.current_hour is None: - return index - elif index + self.current_hour + 1 < 24: - return self.current_hour + index + 1 - else: - return self.current_hour + index + 1 - 24 + return index def aggregate_power(self, peer, sender, bus, topic, headers, message): """ diff --git a/MarketAgents/VAVAgent/setup.py b/MarketAgents/VAVAgent/setup.py index 635162c..0836108 100644 --- a/MarketAgents/VAVAgent/setup.py +++ b/MarketAgents/VAVAgent/setup.py @@ -75,7 +75,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/MarketAgents/VAVAgent/vav/agent.py b/MarketAgents/VAVAgent/vav/agent.py index e67aec8..5bcaa4a 100644 --- a/MarketAgents/VAVAgent/vav/agent.py +++ b/MarketAgents/VAVAgent/vav/agent.py @@ -61,7 +61,7 @@ from datetime import timedelta as td from volttron.platform.agent import utils -from volttron.pnnl.models.vav import VAV +from volttron.pnnl.models import Model from volttron.pnnl.transactive_base.transactive.transactive import TransactiveBase #from decorators import time_cls_methods @@ -72,7 +72,7 @@ #@time_cls_methods -class VAVAgent(TransactiveBase, VAV): +class VAVAgent(TransactiveBase, Model): """ The SampleElectricMeterAgent serves as a sample of an electric meter that sells electricity for a single building at a fixed price. @@ -86,9 +86,10 @@ def __init__(self, config_path, **kwargs): self.agent_name = config.get("agent_name", "vav") TransactiveBase.__init__(self, config, **kwargs) model_config = config.get("model_parameters", {}) - VAV.__init__(self, model_config, **kwargs) + Model.__init__(self, model_config, **kwargs) self.init_markets() + def init_predictions(self, output_info): pass diff --git a/MixMarketServiceAgent/mix_market_service/agent.py b/MixMarketServiceAgent/mix_market_service/agent.py index 075bf1d..c00884d 100644 --- a/MixMarketServiceAgent/mix_market_service/agent.py +++ b/MixMarketServiceAgent/mix_market_service/agent.py @@ -94,8 +94,8 @@ from volttron.platform.agent.base_market_agent.poly_line import PolyLine from volttron.platform.agent.base_market_agent.point import Point -from market_list import MarketList -from market_participant import MarketParticipant +from .market_list import MarketList +from .market_participant import MarketParticipant _tlog = logging.getLogger('transitions.core') _tlog.setLevel(logging.WARNING) @@ -152,6 +152,10 @@ def start_new_cycle(self, peer, sender, bus, topic, headers, message): _log.debug("Clearing prices are [{prices}]".format(prices=str.join(',', [str(p) for p in self.prices]))) + if message["converged"] == True: + _log.debug("Converged do not start mixed market!") + return + gevent.sleep(self.reservation_delay) self.send_collect_reservations_request(utils.get_aware_utc_now()) diff --git a/MixMarketServiceAgent/mix_market_service/market.py b/MixMarketServiceAgent/mix_market_service/market.py index 529dc79..ef2e4f3 100644 --- a/MixMarketServiceAgent/mix_market_service/market.py +++ b/MixMarketServiceAgent/mix_market_service/market.py @@ -50,8 +50,8 @@ from volttron.platform.agent.base_market_agent.buy_sell import BUYER, SELLER from volttron.platform.messaging.topics import MARKET_AGGREGATE, MARKET_CLEAR, MARKET_ERROR, MARKET_RECORD -from offer_manager import OfferManager -from reservation_manager import ReservationManager +from .offer_manager import OfferManager +from .reservation_manager import ReservationManager _tlog = logging.getLogger('transitions.core') _tlog.setLevel(logging.WARNING) @@ -59,7 +59,7 @@ utils.setup_logging() -class MarketFailureError(StandardError): +class MarketFailureError(Exception): """Base class for exceptions in this module.""" def __init__(self, market_name, market_state, object_type, participant): name = role = '' diff --git a/MixMarketServiceAgent/mix_market_service/market_list.py b/MixMarketServiceAgent/mix_market_service/market_list.py index c4faef0..0438458 100644 --- a/MixMarketServiceAgent/mix_market_service/market_list.py +++ b/MixMarketServiceAgent/mix_market_service/market_list.py @@ -39,13 +39,13 @@ import logging from volttron.platform.agent import utils -from market import Market +from .market import Market _log = logging.getLogger(__name__) utils.setup_logging() -class NoSuchMarketError(StandardError): +class NoSuchMarketError(Exception): """Base class for exceptions in this module.""" pass @@ -78,7 +78,7 @@ def clear_reservations(self): self.markets.clear() def collect_offers(self): - for market in self.markets.itervalues(): + for market in list(self.markets.values()): market.collect_offers() def get_market(self, market_name): @@ -89,7 +89,7 @@ def get_market(self, market_name): return market def has_market(self, market_name): - return self.markets.has_key(market_name) + return market_name in self.markets def has_market_formed(self, market_name): market_has_formed = False @@ -99,7 +99,7 @@ def has_market_formed(self, market_name): return market_has_formed def send_market_failure_errors(self): - for market in self.markets.itervalues(): + for market in list(self.markets.values()): # We have already sent unformed market failures if market.has_market_formed(): # If the market has not cleared trying to clear it will send an error. @@ -110,8 +110,8 @@ def market_count(self): return len(self.markets) def unformed_market_list(self): - list = [] - for market in self.markets.itervalues(): - if not market.has_market_formed(): - list.append(market.market_name) - return list + _list = [] + for market in list(self.markets.values()): + if not market.has_market_formed(): + _list.append(market.market_name) + return _list diff --git a/MixMarketServiceAgent/mix_market_service/reservation_manager.py b/MixMarketServiceAgent/mix_market_service/reservation_manager.py index 683fba6..a19dfbf 100644 --- a/MixMarketServiceAgent/mix_market_service/reservation_manager.py +++ b/MixMarketServiceAgent/mix_market_service/reservation_manager.py @@ -37,7 +37,7 @@ # }}} -class MarketReservationError(StandardError): +class MarketReservationError(Exception): """Base class for exceptions in this module.""" pass diff --git a/MixMarketServiceAgent/setup.py b/MixMarketServiceAgent/setup.py index 914cb15..2d527ea 100644 --- a/MixMarketServiceAgent/setup.py +++ b/MixMarketServiceAgent/setup.py @@ -55,7 +55,7 @@ # Find the version number from the main module agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) __version__ = _temp.__version__ # Setup diff --git a/ModelRegressionAgent/config b/ModelRegressionAgent/config new file mode 100644 index 0000000..70f3d6d --- /dev/null +++ b/ModelRegressionAgent/config @@ -0,0 +1,82 @@ +{ + "campus": "", + "building": "", + "device": "", + "subdevices": [], + + "subdevice_points": { + "temp_stpt": "ZoneCoolingTemperatureSetPoint", + "temp": "ZoneTemperature", + "m": "ZoneAirFlow" + }, + "device_points": { + "oat": "OutdoorAirTemperature" + }, + + "historian_vip": "crate.prod", + "run_schedule": "/10080 * * * *", + "run_onstart": True, + + # For each regregression (device/subdevice) data + # With different timestamps will be merged and averaged + # to the following timescale. + # Options: "1Min", "5Min", "15Min", "h" + # for hourly_regression set data_aggregation_frequency='h' + "data_aggregation_frequency": "h", + + "exclude_weekends_holidays": true, + + # Option to create hourly regression result. + "regress_hourly": true, + + # When making predictions for zone temperature (ZT) + # ZT(t+dt) = f(ZT(t), . . . ) + # For this case it is necessary to shift the dependent variable. + # The shift is equal to the data_aggregation_frequency. + # To enable this, set shift_dependent_data to true (default is false). + "shift_dependent_data": false, + + # Number of days of data to use for training + # Option is only valid if one_shot is set to false + # For cron scheduled regression the end of the training data + # will be midnight of the current day + "training_interval": 5, + + # if one_shot is true specify start and end time for regression + "one_shot": false, + "start": "07/01/2019 00:00:00", + "end": "07/15/2019 00:00:00", + "local_tz": "US/Pacific", + + "model_structure": "M = (oat - temp) + (temp - temp_stpt)", + + # key should be left side of model_structure. Value is evaluated based on + # keys in subdevice_points and device_points. + # a more complicated example could be {"ZT": "temp-temp_stpt"} + "model_dependent": { + "M": "m" + }, + "model_independent": { + "(oat - temp)": { + "coefficient_name": "a1", + "lower_bound": 0, + "upper_bound": "infinity" + }, + "(temp - temp_stpt)": { + "coefficient_name": "a2", + "lower_bound": 0, + "upper_bound": "infinity" + }, + "intercept": { + "coefficient_name": "a3", + "lower_bound": 0, + "upper_bound": "infinity" + } + }, + "post_processing": { + "a1": "-a2", + "a2": "a2 - a1", + "a3": "a1", + "a4": "a3" + } +} diff --git a/ModelRegressionAgent/model_regression/__init__.py b/ModelRegressionAgent/model_regression/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ModelRegressionAgent/model_regression/agent.py b/ModelRegressionAgent/model_regression/agent.py new file mode 100644 index 0000000..2c136ed --- /dev/null +++ b/ModelRegressionAgent/model_regression/agent.py @@ -0,0 +1,785 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2019, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +import os +import sys +import logging +from collections import defaultdict, OrderedDict +from datetime import datetime as dt, timedelta as td +from dateutil import parser + +import json +from scipy.optimize import lsq_linear +from volttron.platform.vip.agent import Agent, Core, PubSub, RPC +from volttron.platform.agent import utils +from volttron.platform.agent.utils import (get_aware_utc_now, format_timestamp) +from volttron.platform.scheduling import cron +from volttron.platform.messaging import topics + +import numpy as np +import pandas as pd +import patsy + +from pandas.tseries.offsets import CustomBusinessDay +from pandas.tseries.holiday import USFederalHolidayCalendar as calendar +import scipy +import pytz +import re + +utils.setup_logging() +_log = logging.getLogger(__name__) +UTC_TZ = pytz.timezone('UTC') +WORKING_DIR = os.getcwd() +__version__ = 0.1 +HOLIDAYS = pd.to_datetime(CustomBusinessDay(calendar=calendar()).holidays) + + +def is_weekend_holiday(start, end, tz): + if start.astimezone(tz).date() in HOLIDAYS and \ + end.astimezone(tz).date() in HOLIDAYS: + return True + if start.astimezone(tz).weekday() > 4 and \ + end.astimezone(tz).weekday() > 4: + return True + return False + + +def sort_list(lst): + sorted_list = [] + for item in lst: + if "+" in item: + sorted_list.append(item) + lst.remove(item) + elif "-" in item: + sorted_list.append(item) + lst.remove(item) + elif "*" in item: + sorted_list.append(item) + lst.remove(item) + for item in lst: + sorted_list.append(item) + return sorted_list + + +class Device: + """ + Container to store topics for historian query. + """ + def __init__(self, site, building, + device, subdevice, + device_points, subdevice_points): + """ + Device constructor. + :param site: + :param building: + :param device: + :param subdevice: + :param device_points: + :param subdevice_points: + """ + self.device = device + base_record_list = ["tnc", site, building, device, subdevice, "update_model"] + base_record_list = list(filter(lambda a: a != "", base_record_list)) + self.record_topic = '/'.join(base_record_list) + key_map = defaultdict() + for token, point in subdevice_points.items(): + topic = topics.RPC_DEVICE_PATH(campus=site, + building=building, + unit=device, + path=subdevice, + point=point) + key_map[token] = topic + for token, point in device_points.items(): + topic = topics.RPC_DEVICE_PATH(campus=site, + building=building, + unit=device, + path='', + point=point) + key_map[token] = topic + self.input_data = key_map + + +class Regression: + """ + Regression class contains the functions involved in performing + least squares regression. + """ + def __init__(self, + model_independent, + model_dependent, + model_struc, + regress_hourly, + shift_dependent_data, + post_processing, + debug): + """ + Regression constructor. + :param model_independent: dict; independent regression parameters + :param model_dependent: list; dependent regression variable + :param model_struc: str; formula for regression + :param regress_hourly: bool; If true create hourly regression results + """ + self.debug = debug + self.bounds = {} + self.regression_map = OrderedDict() + self.model_independent = model_independent + self.model_dependent = model_dependent + self.regress_hourly = regress_hourly + self.intercept = None + self.create_regression_map() + self.model_struc = model_struc.replace("=", "~") + self.shift_dependent_data = shift_dependent_data + self.post_processing = post_processing + if not self.validate_regression(): + _log.debug("Regression will fail!") + sys.exit() + if post_processing is not None: + if not self.validate_post_processor(): + _log.warning("Post processing mis-configured! Agent will not attempt post-processing") + self.post_processing = None + + def create_regression_map(self): + """ + Create the regression map {device: regression parameters}. Check the + bounds on regression coefficients and ensure that they are parsed to + type float. + :return: None + """ + self.bounds = {} + regression_map = {} + for token, parameters in self.model_independent.items(): + regression_map.update({token: parameters['coefficient_name']}) + # If the bounds are not present in the configuration file + # then set the regression to be unbounded (-infinity, infinity). + if 'lower_bound' not in parameters: + self.model_independent[token].update({'lower_bound': np.NINF}) + _log.debug('Coefficient: %s setting lower_bound to -infinity.', token) + if 'upper_bound' not in parameters: + self.model_independent[token].update({'upper_bound': np.inf}) + _log.debug('Coefficient: %s setting upper_bound to infinity.', token) + # infinity and -infinity as strings should is set to + # np.NINF and np.inf (unbounded). These are type float. + if self.model_independent[token]['lower_bound'] == '-infinity': + self.model_independent[token]['lower_bound'] = np.NINF + if self.model_independent[token]['upper_bound'] == 'infinity': + self.model_independent[token]['upper_bound'] = np.inf + # If the bounds in configuration file are strings + # then convert them to numeric value (float). If + # a ValueError exception occurs then the string cannot be + # converted and the regression will be unbounded. + try: + if isinstance(self.model_independent[token]['lower_bound'], str): + self.model_independent[token]['lower_bound'] = \ + float(self.model_independent[token]['lower_bound']) + except ValueError: + _log.debug("Could not convert lower_bound from string to float!") + _log.debug("Device: %s -- bound: %s", token, self.model_independent[token]["lower_bound"]) + self.model_independent[token]['lower_bound'] = np.NINF + try: + if isinstance(self.model_independent[token]['upper_bound'], str): + self.model_independent[token]['upper_bound'] = \ + float(self.model_independent[token]['upper_bound']) + except ValueError: + _log.debug("Could not convert lower_bound from string to float!") + _log.debug("Device: %s -- bound: %s", token, self.model_independent[token]["upper_bound"]) + self.model_independent[token]['upper_bound'] = np.inf + # Final check on bounds if they are not float or ints then again + # set the regression to be unbounded. + if not isinstance(self.model_independent[token]["lower_bound"], + (float, int)): + self.model_independent[token]['lower_bound'] = np.NINF + if not isinstance(self.model_independent[token]["upper_bound"], + (float, int)): + self.model_independent[token]['upper_bound'] = np.inf + self.bounds[self.model_independent[token]['coefficient_name']] = [ + self.model_independent[token]['lower_bound'], + self.model_independent[token]['upper_bound'] + ] + + if 'Intercept' in regression_map: + self.intercept = regression_map.pop('Intercept') + elif 'intercept' in regression_map: + self.intercept = regression_map.pop('intercept') + tokens = regression_map.keys() + tokens = sort_list(tokens) + for token in tokens: + self.regression_map[token] = regression_map[token] + + def validate_regression(self): + """ + Return True if model_independent expressions and model_dependent parameters + are in the model_structure and return False if they are not. + :return: bool; + """ + for regression_expr in self.regression_map: + if regression_expr not in self.model_struc: + _log.debug("Key: %s for model_independent is not in the model_structure!", regression_expr) + _log.debug("model_structure will not resolve for regression!") + return False + for regression_parameter in self.model_dependent: + if regression_parameter not in self.model_struc: + _log.debug("Value: %s for model_independent is not in the model_structure!", regression_parameter) + _log.debug("model_structure will not resolve for regression!") + return False + return True + + def regression_main(self, df, device): + """ + Main regression run method called by RegressionAgent. + :param df: pandas DataFrame; Aggregated but unprocessed data. + :param device: str; device name. + :return: + """ + df, formula = self.process_data(df) + results_df = None + # If regress_hourly is True then linear least squares + # will be performed for each hour of the day. Otherwise, + # one set of coefficients will be generated. + num_val = 24 if self.regress_hourly else 1 + for i in range(num_val): + if self.regress_hourly: + # Query data frame for data corresponding to each hour i (0-23). + process_df = df.loc[df['Date'].dt.hour == i] + else: + process_df = df + + if self.debug: + filename = '{}/{}-hourly-{}.csv'.format(WORKING_DIR, device, i) + with open(filename, 'w') as outfile: + process_df.to_csv(outfile, mode='w', index=True) + + coefficient_dict = self.calc_coeffs(process_df, formula) + + current_results = pd.DataFrame.from_dict(coefficient_dict) + _log.debug('Coefficients for index %s -- %s', i, current_results) + if results_df is None: + results_df = current_results + else: + results_df = results_df.append(current_results) + + if self.post_processing is not None: + results_df = self.post_processor(results_df) + + return results_df + + def process_data(self, df): + """ + Evaluate data in df using formula. new_df will have columns + corresponding to the coefficients which will be determined during + linear least squares regression. + :param df: pandas DataFrame; + :return:new_df (pandas DataFrame); formula (str) + """ + formula = self.model_struc + df = df.dropna() + new_df = pd.DataFrame() + # Evaluate independent regression parameters as configured in + # model_structure (model formula). + for independent, coefficient in self.regression_map.items(): + new_df[coefficient] = df.eval(independent) + formula = formula.replace(independent, coefficient) + # Evaluate dependent regression parameters as configured in + # model_structure (model formula). + for token, evaluate in self.model_dependent.items(): + new_df[token] = df.eval(evaluate) + if self.shift_dependent_data: + new_df[token] = new_df[token].shift(periods=1) + + new_df.dropna(inplace=True) + new_df["Date"] = df["Date"] + return new_df, formula + + def calc_coeffs(self, df, formula): + """ + Does linear least squares regression based on evaluated formula + and evaluated input data. + :param df: pandas DataFrame + :param formula: str + :return: fit (pandas Series of regression coefficients) + """ + # create independent/dependent relationship by + # applying formula and data df + coefficient_dict = defaultdict(list) + dependent, independent = patsy.dmatrices(formula, df, return_type='dataframe') + y = dependent[self.model_dependent.keys()[0]] + if not any(x in self.model_independent.keys() for x in ['Intercept', 'intercept']): + x = independent.drop(columns=['Intercept', 'intercept']) + else: + x = independent.rename(columns={'Intercept': self.intercept}) + x = x.rename(columns={'intercept': self.intercept}) + + bounds = [[], []] + for coeff in x.columns: + bounds[0].append(self.bounds[coeff][0]) + bounds[1].append(self.bounds[coeff][1]) + + _log.debug('Bounds: %s *** for Coefficients %s', bounds, x.columns) + result = scipy.optimize.lsq_linear(x, y, bounds=bounds) + coeffs_map = tuple(zip(x.columns, result.x)) + for coefficient, value in coeffs_map: + coefficient_dict[coefficient].append(value) + _log.debug('***Scipy regression: ***') + _log.debug(result.x.tolist()) + + return coefficient_dict + + def post_processor(self, df): + rdf = pd.DataFrame() + for key, value in self.post_processing.items(): + try: + rdf[key] = df.eval(value) + except: + _log.warning("Post processing error on %s", key) + rdf[key] = df[key] + return rdf + + def validate_post_processor(self): + independent_coefficients = set(self.regression_map.values()) + validate_coefficients = set() + for coefficient, processor in self.post_processing.items(): + for key, name in self.regression_map.items(): + if name in processor: + validate_coefficients.add(name) + break + return validate_coefficients == independent_coefficients + + +class RegressionAgent(Agent): + """ + Automated model regression agent. Communicates with volttron + historian to query configurable device data. Inputs data into a + configurable model_structure to generate regression coefficients. + + Intended use is for automated updating of PNNL TCC models for + device flexibility determination. + """ + def __init__(self, config_path, **kwargs): + """ + Constructor for + :param config_path: + :param kwargs: + """ + super(RegressionAgent, self).__init__(**kwargs) + config = utils.load_config(config_path) + self.debug = config.get("debug", True) + # Read equipment configuration parameters + site = config.get('campus', '') + building = config.get('building', '') + device = config.get('device', '') + subdevices = config.get('subdevices', []) + device_points = config.get('device_points') + subdevice_points = config.get('subdevice_points') + + # VIP identity for the VOLTTRON historian + self.data_source = config.get('historian_vip', 'crate.prod') + # External platform for remote RPC call. + self.external_platform = config.get("external_platform", "") + + if device_points is None and subdevice_points is None: + _log.warning('Missing device or subdevice points in config.') + _log.warning("Cannot perform regression! Exiting!") + sys.exit() + if not device and not subdevices: + _log.warning('Missing device topic(s)!') + + model_struc = config.get('model_structure') + model_dependent = config.get('model_dependent') + model_independent = config.get('model_independent') + regress_hourly = config.get('regress_hourly', True) + shift_dependent_data = config.get("shift_dependent_data", False) + post_processing = config.get('post_processing') + # All parameters related to running in simulation - for time keeping only + self.simulation = config.get("simulation", False) + self.simulation_data_topic = config.get("simulation_data_topic", "devices") + simulation_interval = config.get("simulation_regression_interval", 15) + self.simulation_regression_interval = td(days=simulation_interval) + self.simulation_initial_time = None + + if model_struc is None or model_dependent is None or model_independent is None: + _log.exception('At least one of the model fields is missing in config') + sys.exit() + + device_list = subdevices if subdevices else [device] + self.device_list = {} + self.regression_list = {} + for unit in device_list: + self.device_list[unit] = Device(site, building, device, unit, device_points, subdevice_points) + self.regression_list[unit] = Regression(model_independent, + model_dependent, + model_struc, + regress_hourly, + shift_dependent_data, + post_processing, + self.debug) + + # Aggregate data to this value of minutes + self.data_aggregation_frequency = config.get("data_aggregation_frequency", "h") + + # This sets up the cron schedule to run once every 10080 minutes + # Once every 7 days + self.run_schedule = config.get("run_schedule", "*/10080 * * * *") + self.training_interval = int(config.get('training_interval', 5)) + + self.exclude_weekends_holidays = config.get("exclude_weekends_holidays", True) + self.run_onstart = config.get("run_onstart", True) + + self.one_shot = config.get('one_shot', False) + + self.local_tz = pytz.timezone(config.get('local_tz', 'US/Pacific')) + # If one shot is true then start and end should be specified + if self.one_shot: + self.start = config.get('start') + self.end = config.get('end') + + self.coefficient_results = {} + self.exec_start = None + _log.debug("Validate historian running vip: %s - platform %s", + self.data_source, self.external_platform) + + @Core.receiver('onstart') + def onstart(self, sender, **kwargs): + """ + onstart method handles scheduling regression execution. + Either cron schedule for periodic updating of model parameters + or one_shot to run once. + :param sender: str; + :param kwargs: None + :return: None + """ + # TODO: note in function. reschedule do not exit. + #if not self.validate_historian_reachable(): + # _log.debug("Cannot verify historian is running!") + # sys.exit() + + if not self.one_shot: + if not self.simulation: + self.core.schedule(cron(self.run_schedule), self.scheduled_run_process) + else: + self.simulation_setup() + if self.run_onstart: + self.scheduled_run_process() + else: + try: + self.start = parser.parse(self.start) + self.start = self.local_tz.localize(self.start) + self.start = self.start.astimezone(UTC_TZ) + self.end = parser.parse(self.end) + self.end = self.local_tz.localize(self.end) + self.end = self.end.astimezone(UTC_TZ) + except (NameError, ValueError) as ex: + _log.debug('One shot regression: start_time or end_time ' + 'not specified correctly!: *%s*', ex) + self.end = dt.now(self.local_tz).replace(hour=0, + minute=0, + second=0, + microsecond=0) + self.start = self.end - td(days=self.training_interval) + self.main_run_process() + + def simulation_setup(self): + _log.debug("Running with simulation using topic %s", + self.simulation_data_topic) + self.vip.pubsub.subscribe(peer="pubsub", + prefix=self.simulation_data_topic, + callback=self.simulation_time_handler) + + def simulation_time_handler(self, peer, sender, bus, topic, header, message): + current_time = parser.parse(header["Date"]) + _log.debug("Simulation time handler current_time: %s", current_time) + if self.simulation_initial_time is None: + self.simulation_initial_time = current_time + retraining_time_delta = current_time - self.simulation_initial_time + _log.debug("Simulation time handler time delta: %s", + retraining_time_delta) + if retraining_time_delta >= self.simulation_regression_interval: + self.simulation_run_process(current_time) + + def validate_historian_reachable(self): + _log.debug("Validate historian running vip: %s - platform %s", + self.data_source, self.external_platform) + historian_reachable = False + try: + result = self.vip.rpc.call("control", + 'list_agents', + external_platform=self.external_platform).get(timeout=30) + except: + _log.debug("Connection to platform failed, cannot validate historian running!") + # TODO: Update to schedule a rerun + sys.exit() + for agent_dict in result: + if agent_dict["identity"] == self.data_source: + historian_reachable = True + + return historian_reachable + + @Core.receiver('onstop') + def stop(self, sender, **kwargs): + pass + + def scheduled_run_process(self): + self.end = get_aware_utc_now().replace(hour=0, + minute=0, + second=0, microsecond=0) + if self.exclude_weekends_holidays: + training_interval = self.calculate_start_offset() + else: + training_interval = self.training_interval + self.start = self.end - td(days=training_interval) + self.main_run_process() + + def simulation_run_process(self, current_time): + self.end = current_time.replace(hour=0, + minute=0, + second=0, microsecond=0) + if self.exclude_weekends_holidays: + training_interval = self.calculate_start_offset() + else: + training_interval = self.training_interval + self.start = self.end - td(days=training_interval) + self.main_run_process() + self.simulation_initial_time = None + + def calculate_start_offset(self): + """ + The regression interval is a number of days of data + to include in regression ending at midnight of the current day. + If this date interval contains weekends or holidays and + exclude_weekends_holidays is True then the start date must be + made earlier to compensate for the weekends and holidays. + :return: + """ + increment = 0 + for _day in range(1, self.training_interval + 1): + training_date = (self.end - td(days=_day)).astimezone(self.local_tz) + if training_date.date() in HOLIDAYS: + increment += 1 + elif training_date.weekday() > 4 and \ + training_date.weekday() > 4: + increment += 1 + return self.training_interval + increment + + def main_run_process(self): + """ + Main run process for RegressionAgent. Calls data query methods + and regression methods. Stores each devices result. + :return: + """ + + self.exec_start = utils.get_aware_utc_now() + _log.debug('Start regression - UTC converted: {}'.format(self.start)) + _log.debug('End regression UTC converted: {}'.format(self.end)) + + # iterate for each device or subdevice in the device list + for name, device in self.device_list.items(): + self.exec_start = utils.get_aware_utc_now() + df = self.query_historian(device.input_data) + df = self.localize_df(df, name) + result = self.regression_list[name].regression_main(df, name) + result.reset_index() + result = result.to_dict(orient='list') + self.coefficient_results[device.record_topic] = result + if self.debug: + with open('{}/{}_results.json'.format(WORKING_DIR, name), 'w+') as outfile: + json.dump(result, outfile, indent=4, separators=(',', ': ')) + _log.debug('*** Finished outputting coefficients ***') + self.publish_coefficients() + exec_end = utils.get_aware_utc_now() + exec_dif = exec_end - self.exec_start + _log.debug("Regression for %s duration: %s", device, exec_dif) + + def publish_coefficients(self): + """ + Publish coefficients for each device. + :return: + """ + for topic, message in self.coefficient_results.items(): + self.vip.pubsub.publish("pubsub", topic, {}, message).get(timeout=10) + + def query_historian(self, device_info): + """ + Query VOLTTRON historian for all points in device_info + for regression period. All data will be combined and aggregated + to a common interval (i.e., 1Min). + :param device_info: dict; {regression token: query topic} + :return: + """ + aggregated_df = None + rpc_start = self.start + rpc_end = rpc_start + td(hours=8) + # get data via query to historian + # Query loop for device will continue until start > end + # or all data for regression period is obtained. + while rpc_start < self.end.astimezone(pytz.UTC): + df = None + # If exclude_weekend_holidays is True then do not query for + # these times. Reduces rpc calls and message bus traffic. + if self.exclude_weekends_holidays: + if is_weekend_holiday(rpc_start, rpc_end, self.local_tz): + rpc_start = rpc_start + td(hours=8) + rpc_end = rpc_start + td(minutes=479) + if rpc_end > self.end.astimezone(UTC_TZ): + rpc_end = self.end.astimezone(UTC_TZ) + continue + + for token, topic in device_info.items(): + rpc_start_str = format_timestamp(rpc_start) + rpc_end_str = format_timestamp(rpc_end) + _log.debug("RPC start {} - RPC end {} - topic {}".format(rpc_start_str, rpc_end_str, topic)) + # Currently historian is limited to 1000 records per query. + result = self.vip.rpc.call(self.data_source, + 'query', + topic=topic, + start=rpc_start_str, + end=rpc_end_str, + order='FIRST_TO_LAST', + count=1000, + external_platform=self.external_platform).get(timeout=300) + _log.debug(result) + if not bool(result['values']): + _log.debug('ERROR: empty RPC return for ' + 'coefficient *%s* at %s', token, rpc_start) + continue + # TODO: check if enough data is present and compensate for significant missing data + data = pd.DataFrame(result['values'], columns=['Date', token]) + data['Date'] = pd.to_datetime(data['Date']) + + # Data is aggregated to some common frequency. + # This is important if data has different seconds/minutes. + # For minute trended data this is set to 1Min. + data = data.groupby([pd.Grouper(key='Date', freq=self.data_aggregation_frequency)]).mean() + df = data if df is None else pd.merge(df, data, how='outer', left_index=True, right_index=True) + + if aggregated_df is None: + aggregated_df = df + else: + aggregated_df = aggregated_df.append(df) + + # Currently 8 hours is the maximum interval that the historian + # will support for one minute data. 1000 max records can be + # returned per query and each query has 2 fields timestamp, value. + # Note: If trending is at sub-minute interval this logic would + # need to be revised to account for this or the count in historian + # could be increased. + rpc_start = rpc_start + td(hours=8) + if rpc_start + td(minutes=479) <= self.end.astimezone(pytz.UTC): + rpc_end = rpc_start + td(minutes=479) # + else: + rpc_end = self.end.astimezone(pytz.UTC) + return aggregated_df + + def localize_df(self, df, device): + """ + Data from the VOLTTRON historian will be in UTC timezone. + Regressions typically are meaningful for localtime as TCC + agents utilize local time for predictions and control. + :param df: + :param device: + :return: + """ + df = df.reset_index() + try: + # Convert UTC time to local time in configuration file. + df['Date'] = df['Date'].dt.tz_convert(self.local_tz) + except Exception as e: + _log.error('Failed to convert Date column to localtime - {}'.format(e)) + if self.debug: + filename = '{}/{}-{} - {}.csv'.format(WORKING_DIR, self.start, self.end, device) + try: + with open(filename, 'w+') as outfile: + df.to_csv(outfile, mode='a', index=True) + _log.debug('*** Finished outputting data ***') + except Exception as e: + _log.error('File output failed, check whether the dataframe is empty - {}'.format(e)) + + # Weekends and holidays will only be present if + # one_shot is true. For scheduled regression those + # days are excluded from query to historian. + if self.exclude_weekends_holidays: + holiday = CustomBusinessDay(calendar=calendar()).onOffset + match = df["Date"].map(holiday) + df = df[match] + return df + + @RPC.export + def get_coefficients(self, device_id, **kwargs): + """ + TCC agent can do RPC call to get latest regression coefficients. + :param device_id: str; device/subdevice + :param kwargs: + :return: + """ + if self.coefficient_results: + try: + result = self.coefficient_results[device_id] + except KeyError as ex: + _log.debug("device_id provided is not known: %s", device_id) + result = None + else: + _log.debug("No regression results exist: %s", device_id) + result = None + return result + + +def main(argv=sys.argv): + '''Main method called by the eggsecutable.''' + try: + utils.vip_main(RegressionAgent) + except Exception as e: + _log.exception('unhandled exception - {}'.format(e)) + + +if __name__ == '__main__': + # Entry point for script + sys.exit(main()) diff --git a/ModelRegressionAgent/setup.py b/ModelRegressionAgent/setup.py new file mode 100644 index 0000000..c1a0ea2 --- /dev/null +++ b/ModelRegressionAgent/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = 'model_regression' + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], -1) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) \ No newline at end of file diff --git a/TNSAgent/tns/building_agent.py b/TNSAgent/tns/building_agent.py index 604e7d7..b0257c3 100755 --- a/TNSAgent/tns/building_agent.py +++ b/TNSAgent/tns/building_agent.py @@ -299,7 +299,7 @@ def start_mixmarket(self, start_of_cycle): self.vip.pubsub.publish(peer='pubsub', topic='mixmarket/start_new_cycle', message={"prices": self.prices[-24:], - "hour": now.hour}) + "Date": format_timestamp(now)}) else: temps = [x.value for x in weather_service.predictedValues] temps = temps[-24:] @@ -308,7 +308,7 @@ def start_mixmarket(self, start_of_cycle): topic='mixmarket/start_new_cycle', message={"prices": self.prices[-24:], "temp": temps, - "hour": now.hour}) + "Date": format_timestamp(now)}) def balance_market(self, run_cnt): market = self.markets[0] # Assume only 1 TNS market per node diff --git a/base_market_agent/poly_line.py b/base_market_agent/poly_line.py index 77a9705..5f4ad5a 100644 --- a/base_market_agent/poly_line.py +++ b/base_market_agent/poly_line.py @@ -284,20 +284,35 @@ def intersection(pl_1, pl_2): p1_qmax = max([point[0] for point in pl_1]) p1_qmin = min([point[0] for point in pl_1]) + p2_qmax = max([point[0] for point in pl_2]) + p2_qmin = min([point[0] for point in pl_2]) + p1_pmax = max([point[1] for point in pl_1]) p2_pmax = max([point[1] for point in pl_2]) + p1_pmin = min([point[1] for point in pl_1]) p2_pmin = min([point[1] for point in pl_2]) # The lines don't intersect, add the auxillary information - if p1_pmax < p2_pmax and p1_pmax < p2_pmin: + if p1_pmax <= p2_pmax and p1_pmax <=p2_pmin: quantity = p1_qmin - price = p1_pmax - elif p2_pmin < p1_pmin and p2_pmax < p1_pmin: + price = p2_pmax + + elif p2_pmin <=p1_pmin and p2_pmax <=p1_pmin: quantity = p1_qmax - price = p1_pmin + price = p2_pmin + + elif p2_qmax >= p1_qmin and p2_qmax >= p1_qmax: + quantity = np.mean([point[0] for point in pl_1]) + price = np.mean([point[1] for point in pl_1]) + + elif p2_qmin <= p1_qmin and p2_qmin <= p1_qmax: + quantity = p2_qmax + price = p1_pmax + else: - quantity = None price = None + quantity = None + return quantity, price @staticmethod @@ -366,4 +381,3 @@ def compare(demand_curve, supply_curve): aux['SPx,DPn'] = cmp(supply_max_price, demand_min_price) aux['SPx,DPx'] = cmp(supply_max_price, demand_max_price) return aux - diff --git a/pnnl/ModelicaAgent/__init__.py b/pnnl/ModelicaAgent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pnnl/ModelicaAgent/config_PID b/pnnl/ModelicaAgent/config_PID new file mode 100644 index 0000000..d177f0e --- /dev/null +++ b/pnnl/ModelicaAgent/config_PID @@ -0,0 +1,36 @@ +{ + 'model': 'IBPSA.Utilities.IO.RESTClient.Examples.PIDTest', + 'model_runtime': 361, + 'result_file': 'PIDTest', + 'mos_file_path': '/home/volttron/dymola/run_PID.mos', + "advance_simulation_topic": "modelica/advance", + 'inputs' : { + 'control_setpoint' : { + 'name' : 'control_setpoint', + 'topic' : 'building/device', + 'field' : 'control_setpoint' + + }, + 'control_output' : { + 'name' : 'control_output', + 'topic' : 'building/device', + 'field' : 'control_output' + + } + }, + 'outputs' : { + 'measurement' : { + 'name' : 'measurement', + 'topic' : 'building/device', + 'field' : 'measurement', + 'meta' : {'type': 'Double', 'unit': 'none'} + }, + 'setpoint' : { + 'name' : 'setpoint', + 'topic' : 'building/device', + 'field' : 'control_setpoint', + 'meta' : {'type': 'Double', 'unit': 'none'} + } + + } +} \ No newline at end of file diff --git a/pnnl/ModelicaAgent/modelica_agent/__init__.py b/pnnl/ModelicaAgent/modelica_agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pnnl/ModelicaAgent/modelica_agent/agent.py b/pnnl/ModelicaAgent/modelica_agent/agent.py new file mode 100644 index 0000000..6c9662c --- /dev/null +++ b/pnnl/ModelicaAgent/modelica_agent/agent.py @@ -0,0 +1,562 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2017, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +import logging +import socket +import sys +import json +from collections import defaultdict +from gevent import monkey, sleep +from volttron.platform.agent import utils +from volttron.platform.vip.agent import Agent, Core, RPC +from volttron.platform.scheduling import periodic + +monkey.patch_socket() +utils.setup_logging() +log = logging.getLogger(__name__) +SUCCESS = 'SUCCESS' +FAILURE = 'FAILURE' + + +class SocketServer: + """ + Socket server class that facilitates communication with Modelica. + """ + def __init__(self, port, host): + """ + Contstructor for SocketServer. + :param port: int; port to listen. + :param host: str; IP address, defaults to '127.0.0.1' + """ + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # Bind socket server to IP and Port + self.sock.bind((host, port)) + self.client = None + self.received_data = None + self.size = 4096 + log.debug('Bound to %s on %s' % (port, host)) + + def run(self): + """ + Calls listen method. + :return: + """ + self.listen() + + def listen(self): + """ + Execution loop for SocketServer. + Facilitates transmission of data from Modelica + :return: + """ + self.sock.listen(10) + log.debug('server now listening') + while True: + # Reopen connection to receive communication as the + # connection is closed by SocketServer after each transmittal. + self.client, addr = self.sock.accept() + log.debug('Connected with %s:%s', addr[0], addr[1]) + data = self.receive_data() + # Python3 will send the data as a byte not a string + data = data.decode('utf-8') + log.debug('Modelica data %s', data) + if data: + data = json.loads(data) + self.received_data = data + self.on_receive_data(data) + + def receive_data(self): + """ + Client resource receives data payload from Modelica. + :return: data where input data is a list output data is a + dictionary. + """ + if self.client is not None and self.sock is not None: + try: + data = self.client.recv(self.size) + except Exception: + log.error('We got an error trying to read a message') + data = None + return data + + def on_receive_data(self, data): + """ + on_recieve_data stub. + :param data: data payload from Modelica. + :return: + """ + log.debug('Received %s', data) + + def stop(self): + """ + On stop close the socket. + :return: + """ + if self.sock is not None: + self.sock.close() + + +class ModelicaAgent(Agent): + """ + Modelica agent allows co-simulation with Modelica and + Facilitates data exchange between VOLLTRON and Modelica model. + """ + def __init__(self, config_path, **kwargs): + """ + Constructor for ModelicaAgent + :param config_path: str; path to config file + :param kwargs: + """ + super().__init__(**kwargs) + config = utils.load_config(config_path) + # Set IP and port that SocketServer will bind + self.remote_ip = config.get('remote_ip', '127.0.0.1') + self.remote_port = config.get('remote_port', 8888) + self.socket_server = None + # Read outputs dictionary and inputs dictionary + outputs = config.get('outputs', {}) + inputs = config.get('inputs', {}) + self.advance_topic = config.get("advance_simulation_topic") + + # Initialize input related parameters. + self.control_map = {} + self.control_topic_map = {} + self.controls_list_master = set() + self.controls_list = [] + self.control_proceed = set() + self.current_control = None + self.time_step_interval = config.get('timestep_interval', 30) + self.time_step = 1 + + # Initialize output related parameters. + self.data_map = {} + self.data_map_master = None + self.output_data = defaultdict(list) + self.output_map = None + + self.create_control_map(inputs) + self.create_output_map(outputs) + model = config.get("model") + run_time = config.get("model_runtime", 500) + result_file = config.get('result_file', 'result') + mos_file_path = config.get('mos_file_path', 'run.mos') + self.create_mos_file(model, run_time, result_file, mos_file_path) + self.run_time = run_time + + def create_mos_file(self, model, run_time, result_file, mos_file_path): + write = 'simulateModel("{}", stopTime={}, method="dassl", resultFile="{}");\n'.format(model, run_time, result_file) + write_list = [write, "\n", "exit();"] + _file = open(mos_file_path, "w") + _file.writelines(write_list) + _file.close() + + def create_output_map(self, outputs): + """ + Create the data_map necessary for tracking if all outputs + from Modelica are received for a timestep. + :param outputs: + :return: + """ + # data_map contains topic, field (aka volttron point name), + # and meta data associated with each unique Modelica point name. + self.data_map = outputs + for name, info in outputs.items(): + topic = info['topic'] + self.output_data[topic] = [{}, {}] + # Modelica sends the measurements one at a time. When + # the agent receives a measurement it removes the point from the + # data_map but data_map_master is never manipulated after it is created + self.data_map_master = dict(self.data_map) + log.debug('data %s', self.data_map) + + def create_control_map(self, inputs): + """ + Create the control_topic_map, control_map, and cotnrols_list. + control_topic_map - volttron topic to Modelica point name map + control_map - Modelica point name to current input to Modelica + controls_list - list of all inputs. Empty when all setpoints have been + received + information. + :param inputs: dictionary where key is Modelica point name and value + is input information. + :return: + """ + for name, info in inputs.items(): + topic = '/'.join([info['topic'], info['field']]) + self.control_topic_map[topic] = name + self.control_map[name] = { + 'value': 0, + 'nextSampleTime': 1, + 'enable': False + } + self.controls_list_master = set(self.control_map.keys()) + self.controls_list = list(self.controls_list_master) + log.debug('Control map %s', self.control_map) + + @Core.receiver('onstart') + def start(self, sender, **kwargs): + """ + ON agent start call function to instantiate the socket server. + :param sender: not used + :param kwargs: not used + :return: + """ + if self.advance_topic is not None: + if isinstance(self.advance_topic, str) and self.advance_topic: + self.vip.pubsub.subscribe(peer='pubsub', + prefix=self.advance_topic, + callback=self.advance_simulation) + self.start_socket_server() + + def start_socket_server(self): + """ + Instantiate the SocketServer to the configured IP + and port. Spawn an this as a gevent loop + executing the run method of the SocketServer. + :return: + """ + self.socket_server = SocketServer(port=self.remote_port, + host=self.remote_ip) + self.socket_server.on_receive_data = self.receive_modelica_data + self.core.spawn(self.socket_server.run) + + def receive_modelica_data(self, data): + """ + Receive a data payload from Modelica. + A data payload dictionary is output data for to publish to message bus. + A data payload lis indicates a control signal can be sent to + Modelica [point(str), time(int)]. + :param data: data payload from Modelica. + :return: + """ + self.data = data + log.debug('Modelica Agent receive data %s - %s', data, type(data)) + if isinstance(data, dict): + self.publish_modelica_data(data) + else: + self.current_control = data + name = data[0] + self.control_proceed.add(name) + # If the controls_list is empty then all + # set points have been received. + if not self.controls_list: + # Since Modelica sends each control list one at a time + # and expects a subsequent answer in the correct order + # an additional list of points is created to know when + # all messages have been sent to Modelica and it is time + # to reinitialize the controls_list. + self.send_control_signal(self.current_control) + if self.control_proceed == self.controls_list_master: + self.reinit_control_lists() + + def reinit_control_lists(self): + """ + Reinitialize the controls_list for tracking if all setpoints have + been received. + :return: + """ + log.debug('Reinitialize controls list') + self.controls_list = list(self.controls_list_master) + self.control_proceed = set() + self.current_control = None + + def send_control_signal(self, control): + """ + Send the control signal to Modelica. + :param control: + :return: + """ + msg = {} + name = control[0] + _time = control[1] + # This is not required but for simplicity + # this agent uses a uniform value for the + # nextSampleTime parameter for all inputs to Modelica. + next_sample_time = _time + self.time_step_interval + if next_sample_time > self.run_time: + next_sample_time = self.run_time + self.control_map[name]['nextSampleTime'] = next_sample_time + msg[name] = self.control_map[name] + msg = json.dumps(msg) + msg = msg + '\0' + log.debug('Send control input to Modelica: %s', msg) + # For Python3 this must be byte encoded. + # For Python2 a string would be used. + msg = msg.encode() + # Send the input to Modelica via the SocketServer. + self.socket_server.client.send(msg) + + def publish_modelica_data(self, data): + """ + This function publishes the Modelica output data once all + outputs for a timestep have been received from Modelica. + Uses an all publish per device. This means that outputs + can be configured with the same topic and will be published + as topic/all consistent with the MasterDriver topic/data format. + :param data: dictionary where key is Modelica point name and value is + data information. + :return: + """ + log.debug('Modelica publish method %s', data) + self.construct_data_payload(data) + for key in data: + self.data_map.pop(key) + # data_map will be empty when all data for a timestep + # is received. + if self.data_map: + return + # Once all data is received iterate over the output_data + # built in construct_data_payload The key is the device publish + # topic and the value is the data payload in the same format that the + # MasterDriverAgent uses. + for topic, value in self.output_data.items(): + self.data_map = dict(self.data_map_master) + headers = {'Timestep': self.time_step} + publish_topic = "/".join([topic, "all"]) + log.debug('Publish - topic %s ----- payload %s', topic, value) + self.vip.pubsub.publish('pubsub', + publish_topic, + headers=headers, + message=value) + if self.time_step >= self.run_time: + log.debug("Simulation has finished!") + self.exit() + + def construct_data_payload(self, data): + """ + This function uses the data_map information + to assemble the data payload for each device. + :param data: + :return: + """ + for key, payload in data.items(): + data_map = self.data_map_master[key] + topic = data_map['topic'] + name = data_map['field'] + value = payload['value'] + meta = data_map['meta'] + self.time_step = payload['time'] + self.output_data[topic][0].update({name: value}) + self.output_data[topic][1].update({name: meta}) + + def exit(self): + self.stop() + sys.exit() + + @Core.receiver('onstop') + def stop(self, sender, **kwargs): + """ + Call when agent is stopped close socket. + :param sender: + :param kwargs: + :return: + """ + if self.socket_server: + self.socket_server.stop() + self.socket_server = None + + @RPC.export + def get_point(self, topic, **kwargs): + """RPC method + + Gets the value of a specific point on a device_name. + Does not require the device_name be scheduled. + + :param topic: The topic of the point to grab in the + format / + :param **kwargs: These get dropped on the floor + :type topic: str + :returns: point value + :rtype: any base python type + + """ + try: + topic_list = topic.split("/") + device_topic = "/".join(topic_list[:-1]) + point = topic_list[-1] + # Retrieve data payload from the output_data. + data_payload = self.output_data[device_topic] + value = data_payload[0][point] + except KeyError as ex: + # No match for outputs + value = None + log.debug('Error on get_point %s', topic) + return value + + @RPC.export + def set_point(self, requester_id, topic, value, **kwargs): + """RPC method + + Sets the value of a specific point on a device. + + :param requester_id: String value deprecated. + :param topic: The topic of the point to set in the + format / + :param value: Value to set point to. + :param **kwargs: These get dropped on the floor + :type topic: str + :type requester_id: str + :type value: any basic python type + :returns: value supplied + :rtype: any base python type + + """ + log.debug('Modelica agent handle_set') + log.debug('topic: %s -- value: %s', topic, value) + try: + # Retrieve modelica point name from the control_topic_map. + name = self.control_topic_map[topic] + # Set the value and enable to true for active control + # next time a message is sent to Modelica. + self.control_map[name]['value'] = value + self.control_map[name]['enable'] = True + except KeyError as ex: + # No match for outputs + log.debug('Topic does not match any know control ' + 'points: %s --- %s', topic, ex) + try: + # Remove the point from the controls list + # once this list is empty we know we have received all the + # setpoints we are expecting and will send the messge to Modelica. + self.controls_list.remove(name) + log.debug('Controls list %s', self.controls_list) + if not self.controls_list: + self.send_control_signal(self.current_control) + except ValueError as ex: + log.warning('Received duplicate set ' + 'point for topic: %s - name: %s', topic, name) + if not self.controls_list: + self.send_control_signal(self.current_control) + return value + + @RPC.export + def revert_point(self, requester_id, topic, **kwargs): + """RPC method + + Reverts the value of a specific point on a device to a default state. + Does not require the device be scheduled. + + :param requester_id: Identifier given when requesting schedule. + :param topic: The topic of the point to revert in the + format / + :param **kwargs: These get dropped on the floor + :type topic: str + :type requester_id: str + + """ + log.debug('Modelica agent revert_point') + log.debug('topic: %s', topic) + try: + # Retrieve modelica point name from the control_topic_map. + name = self.control_topic_map[topic] + # Set the value and enable to true for active control + # next time a message is sent to Modelica. + self.control_map[name]['enable'] = False + except KeyError as ex: + # No match for outputs + log.debug('Topic does not match any know ' + 'control points: %s --- %s', topic, ex) + return FAILURE + try: + # Remove the point from the controls list + # once this list is empty we know we have received all the + # setpoints we are expecting and will send the messge to Modelica. + self.controls_list.remove(name) + log.debug('Controls list %s', self.controls_list) + if not self.controls_list: + self.send_control_signal(self.current_control) + except ValueError as ex: + log.warning('Received duplicate set point ' + 'for topic: %s - name: %s', topic, name) + if not self.controls_list: + self.send_control_signal(self.current_control) + return SUCCESS + + def advance_simulation(self, peer, sender, bus, topic, headers, message): + """ + Pubsub callback to advance simulation to next timestep. + Will use current value for any setpoints that have not been received. + :param peer: + :param sender: sender is not used + :param bus: + :param topic: str + :param headers: empty, i.e., not used + :param message: empty, i.e., not used + :return: + """ + # if current_control is None then Modelica is not accepting inputs + while self.current_control is None: + log.warning('Received advance but Modelica is ' + 'not primed to take inputs!') + log.warning('Keep checking for Modelica to accept inputs!') + sleep(1) + self.controls_list = [] + self.send_control_signal(self.current_control) + + +def main(argv=sys.argv): + """Main method called by the eggsecutable.""" + try: + utils.vip_main(ModelicaAgent) + except Exception as ex: + log.exception(ex) + + +if __name__ == '__main__': + # Entry point for script + sys.exit(main()) diff --git a/pnnl/ModelicaAgent/setup.py b/pnnl/ModelicaAgent/setup.py new file mode 100644 index 0000000..8fc6f3f --- /dev/null +++ b/pnnl/ModelicaAgent/setup.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2016, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +#}}} + +from setuptools import setup, find_packages + + +#get environ for agent name/identifier +packages = find_packages('.') +package = packages[0] + +setup( + name = package, + version = "0.1", + install_requires = ['volttron'], + packages = packages, + entry_points = { + 'setuptools.installation': [ + 'eggsecutable = ' + package + '.agent:main', + ] + } +) + diff --git a/pnnl/ModelicaTest/__init__.py b/pnnl/ModelicaTest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pnnl/ModelicaTest/config_PID b/pnnl/ModelicaTest/config_PID new file mode 100644 index 0000000..db35fc3 --- /dev/null +++ b/pnnl/ModelicaTest/config_PID @@ -0,0 +1,35 @@ +{ + 'model': 'IBPSA.Utilities.IO.RESTClient.Examples.PIDTest', + 'model_runtime': 361, + 'result_file': 'PIDTest', + 'mos_file_path': '/home/volttron/dymola/run_PID.mos', + 'inputs' : { + 'control_setpoint' : { + 'name' : 'control_setpoint', + 'topic' : 'building/device', + 'field' : 'control_setpoint' + + }, + 'control_output' : { + 'name' : 'control_output', + 'topic' : 'building/device', + 'field' : 'control_output' + + } + }, + 'outputs' : { + 'measurement' : { + 'name' : 'measurement', + 'topic' : 'building/device', + 'field' : 'measurement', + 'meta' : {'type': 'Double', 'unit': 'none'} + }, + 'setpoint' : { + 'name' : 'setpoint', + 'topic' : 'building/device', + 'field' : 'control_setpoint', + 'meta' : {'type': 'Double', 'unit': 'none'} + } + + } +} \ No newline at end of file diff --git a/pnnl/ModelicaTest/config_ove b/pnnl/ModelicaTest/config_ove new file mode 100644 index 0000000..15683cc --- /dev/null +++ b/pnnl/ModelicaTest/config_ove @@ -0,0 +1,11 @@ +{ + 'inputs' : { + 'control' : { + 'name' : 'control', + 'topic' : 'building/device', + 'field' : 'control' + + } + }, + 'outputs' : {} +} \ No newline at end of file diff --git a/pnnl/ModelicaTest/modelica_agent/__init__.py b/pnnl/ModelicaTest/modelica_agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pnnl/ModelicaTest/modelica_agent/agent.py b/pnnl/ModelicaTest/modelica_agent/agent.py new file mode 100644 index 0000000..994ff1e --- /dev/null +++ b/pnnl/ModelicaTest/modelica_agent/agent.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: + +# Copyright (c) 2017, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation +# are those of the authors and should not be interpreted as representing +# official policies, either expressed or implied, of the FreeBSD +# Project. +# +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization that +# has cooperated in the development of these materials, makes any +# warranty, express or implied, or assumes any legal liability or +# responsibility for the accuracy, completeness, or usefulness or any +# information, apparatus, product, software, or process disclosed, or +# represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does not +# necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +import logging +import socket +import sys +import json +from collections import defaultdict +from gevent import monkey, sleep +from volttron.platform.agent import utils +from volttron.platform.vip.agent import Agent, Core, RPC +from volttron.platform.scheduling import periodic + +monkey.patch_socket() +utils.setup_logging() +log = logging.getLogger(__name__) + + +class ModelicaTest(Agent): + """ + Modelica test. + """ + def __init__(self, config_path, **kwargs): + """ + Constructor for ModelicaAgent + :param config_path: str; path to config file + :param kwargs: + """ + super().__init__(**kwargs) + config = utils.load_config(config_path) + # Set IP and port that SocketServer will bind + + # Read outputs dictionary and inputs dictionary + inputs = config.get('inputs', {}) + self.advance_simulation_topic = config.get("advance_simulation_topic") + self.advance_time = config.get("advance_interval", 30) + if self.advance_simulation is not None: + self.core.periodic(self.advance_time, self.advance_simulation, wait=self.advance_time) + + def advance_simulation(self): + """ + Testing function. + :return: + """ + self.vip.pubsub.publish('pubsub', + self.advance_simulation_topic, + headers={}, + message={}) + + @Core.receiver('onstart') + def start(self, sender, **kwargs): + """ + ON agent start call function to instantiate the socket server. + :param sender: not used + :param kwargs: not used + :return: + """ + pass + + +def main(argv=sys.argv): + """Main method called by the eggsecutable.""" + try: + utils.vip_main(ModelicaTest) + except Exception as ex: + log.exception(ex) + + +if __name__ == '__main__': + # Entry point for script + sys.exit(main()) diff --git a/pnnl/ModelicaTest/setup.py b/pnnl/ModelicaTest/setup.py new file mode 100644 index 0000000..8fc6f3f --- /dev/null +++ b/pnnl/ModelicaTest/setup.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2016, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are those +# of the authors and should not be interpreted as representing official policies, +# either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +#}}} + +from setuptools import setup, find_packages + + +#get environ for agent name/identifier +packages = find_packages('.') +package = packages[0] + +setup( + name = package, + version = "0.1", + install_requires = ['volttron'], + packages = packages, + entry_points = { + 'setuptools.installation': [ + 'eggsecutable = ' + package + '.agent:main', + ] + } +) + diff --git a/pnnl/energyplusagent/energyplus/agent.py b/pnnl/energyplusagent/energyplus/agent.py index 8d5d225..f235162 100644 --- a/pnnl/energyplusagent/energyplus/agent.py +++ b/pnnl/energyplusagent/energyplus/agent.py @@ -55,7 +55,7 @@ # under Contract DE-AC05-76RL01830 # }}} -from __future__ import absolute_import + import logging import os @@ -107,9 +107,11 @@ def __init__(self, config_path, **kwargs): def update_kwargs_from_config(self, **kwargs): signature = getcallargs(super(PubSubAgent, self).__init__) for arg in signature: - if self.config.has_key('properties'): + if 'properties' in self.config: + #if self.config.has_key('properties'): properties = self.config.get('properties') - if isinstance(properties, dict) and properties.has_key(arg): + if isinstance(properties, dict) and arg in properties: + #if isinstance(properties, dict) and properties.has_key(arg): kwargs[arg] = properties.get(arg) return kwargs @@ -138,7 +140,9 @@ def create_ordered_output(self, output): last_key = None ordered_out = collections.OrderedDict() for key, value in output.items(): - if not value.has_key('publish_last'): + if 'publish_last' not in value: + #if not value.has_key('publish_last'): + log.debug("OUTPUT: {} - {}".format(key, value)) ordered_out[key] = value else: last_key = key @@ -160,7 +164,8 @@ def output(self, *args): def input_output(self, dct, *args): if len(args) >= 1: key = args[0] - if dct.has_key(key): + if key in dct: + #if dct.has_key(key): if len(args) >= 2: field = args[1] if len(args) >= 3: @@ -171,12 +176,14 @@ def input_output(self, dct, *args): return None def subscribe(self): - for key, obj in self.input().iteritems(): - if obj.has_key('topic'): + for key, obj in self.input().items(): + if 'topic' in obj: + #if obj.has_key('topic'): callback = self.on_match_topic topic = obj.get('topic') key_caps = 'onMatch' + key[0].upper() + key[1:] - if obj.has_key('callback'): + if 'callback' in obj: + #if obj.has_key('callback'): callbackstr = obj.get('callback') if hasattr(self, callbackstr) and callable(getattr(self, callbackstr, None)): callback = getattr(self, callbackstr) @@ -223,13 +230,15 @@ def publish(self, *args): topics = collections.OrderedDict() for arg in args: obj = self.output(arg) if type(arg) == str else arg - if obj.has_key('topic') and obj.has_key('value'): + if 'topic' in obj and 'value' in obj: + #if obj.has_key('topic') and obj.has_key('value'): topic = obj.get('topic', None) value = obj.get('value', None) field = obj.get('field', None) metadata = obj.get('meta', {}) if topic is not None and value is not None: - if not topics.has_key(topic): + if topic not in topics: + #if not topics.has_key(topic): topics[topic] = {'values': None, 'fields': None} if field is not None: if topics[topic]['fields'] is None: @@ -240,7 +249,7 @@ def publish(self, *args): if topics[topic]['values'] is None: topics[topic]['values'] = [] topics[topic]['values'].append([value, metadata]) - for topic, obj in topics.iteritems(): + for topic, obj in topics.items(): if obj['values'] is not None: for value in obj['values']: out = value @@ -270,7 +279,8 @@ def update_topic(self, peer, sender, bus, topic, headers, message): return for obj in objs: value = message[0] - if type(value) is dict and obj.has_key('field') and value.has_key(obj.get('field')): + if type(value) is dict and 'field' in obj and obj.get('field') in value: + #if type(value) is dict and obj.has_key('field') and value.has_key(obj.get('field')): value = value.get(obj.get('field')) obj['value'] = value obj['message'] = message[0] @@ -288,13 +298,18 @@ def on_update_complete(self): pass def clear_last_update(self): - for obj in self.input().itervalues(): - if obj.has_key('topic'): + for obj in self.input().values(): + if 'topic' in obj: + #if obj.has_key('topic'): obj['last_update'] = None def get_inputs_from_topic(self, topic): objs = [] - for obj in self.input().itervalues(): + for obj in self.input().values(): + if obj.get('topic') == topic: + objs.append(obj) + topic = "/".join(["devices", topic, "all"]) + for obj in self.output().values(): if obj.get('topic') == topic: objs.append(obj) if len(objs): @@ -305,10 +320,12 @@ def find_best_match(self, topic): topic = topic.strip('/') device_name, point_name = topic.rsplit('/', 1) objs = self.get_inputs_from_topic(device_name) + if objs is not None: for obj in objs: # we have matches to the , so get the first one has a field matching - if obj.has_key('field') and obj.get('field', None) == point_name: + if 'field' in obj and obj.get('field', None) == point_name: + #if obj.has_key('field') and obj.get('field', None) == point_name: return obj objs = self.get_inputs_from_topic(topic) if objs is not None and len(objs): # we have exact matches to the topic @@ -331,10 +348,13 @@ def update_complete(self): self.on_update_complete() def all_topics_updated(self): - for obj in self.input().itervalues(): - if obj.has_key('topic'): - if (obj.has_key('blocking') and obj.get('blocking')) or not obj.has_key('blocking'): - if obj.has_key('last_update'): + for obj in self.input().values(): + if 'topic' in obj: + #if obj.has_key('topic'): + if ('blocking' in obj and obj.get('blocking')) or 'blocking' not in obj: + #if (obj.has_key('blocking') and obj.get('blocking')) or not obj.has_key('blocking'): + if 'last_update' in obj: + #if obj.has_key('last_update'): if obj.get('last_update') is None: return False else: @@ -384,7 +404,7 @@ def __wrapper(*args, **kwargs): __wrapper.__name__ = function.__name__ __wrapper.__self__ = function.__self__ - return __wrapper + return __wrapper class SocketServer(): @@ -578,11 +598,12 @@ def send_eplus_msg(self): if self.socket_server: args = self.input() msg = '%r %r %r 0 0 %r' % (self.vers, self.flag, self.eplus_inputs, self.time) - for obj in args.itervalues(): + for obj in args.values(): if obj.get('name', None) and obj.get('type', None): msg = msg + ' ' + str(obj.get('value')) self.sent = msg + '\n' log.info('Sending message to EnergyPlus: ' + msg) + self.sent = self.sent.encode() self.socket_server.send(self.sent) def recv_eplus_msg(self, msg): @@ -625,19 +646,24 @@ def run_periodic(self): self.send_eplus_msg() def parse_eplus_msg(self, msg): + msg = msg.decode("utf-8") msg = msg.rstrip() - log.info('Received message from EnergyPlus: ' + msg) + log.info('Received message from EnergyPlus: ' + str(msg)) arry = msg.split() + arry = [float(item) for item in arry] + log.info('Received message from EnergyPlus: ' + str(arry)) slot = 6 self.sim_flag = arry[1] output = self.output() + log.info('Outputs: ' + str(output)) input = self.input() if self.realtime and self.rt_periodic is None: timestep = 60. / (self.timestep*self.time_scale)*60. self.rt_periodic = self.core.periodic(timestep, self.run_periodic, wait=timestep) - if self.sim_flag != '0': + if self.sim_flag != 0.0: + _log.debug("FLAG: {} - {}".format(self.sim_flag, type(self.sim_flag))) if self.sim_flag == '1': self.exit('Simulation reached end: ' + self.sim_flag) elif self.sim_flag == '-1': @@ -662,28 +688,29 @@ def parse_eplus_msg(self, msg): slot += 1 slot = 6 for key in output: + log.debug("Outputs1: {}".format(key)) if self.output(key, 'name') and self.output(key, 'type'): try: self.output(key, 'value', float(arry[slot])) except: - print slot + print(slot) self.exit('Unable to convert received value to double.') if self.output(key, 'type').lower().find('currentmonthv') != -1: self.month = float(arry[slot]) - print 'month ' + str(self.month) + print(('month ' + str(self.month))) elif self.output(key, 'type').lower().find('currentdayofmonthv') != -1: self.day = float(arry[slot]) - print 'day ' + str(self.day) + print(('day ' + str(self.day))) elif self.output(key, 'type').lower().find('currenthourv') != -1: self.hour = float(arry[slot]) - print 'hour ' + str(self.hour) + print(('hour ' + str(self.hour))) elif self.output(key, 'type').lower().find('currentminutev') != -1: self.minute = float(arry[slot]) - print 'minute ' + str(self.minute) + print(('minute ' + str(self.minute))) elif self.output(key, 'field'): if self.output(key, 'field').lower().find('operation') != -1: self.operation = float(arry[slot]) - print 'operation (1:on, 0: off) ' + str(self.operation) + print(('operation (1:on, 0: off) ' + str(self.operation))) slot += 1 def exit(self, msg): @@ -710,14 +737,16 @@ def write_variable_file(self, path): fh.write('\n') fh.write('\n') fh.write('\n') - for obj in self.output().itervalues(): - if obj.has_key('name') and obj.has_key('type'): + for obj in self.output().values(): + if 'name' in obj and 'type' in obj: + #if obj.has_key('name') and obj.has_key('type'): self.eplus_outputs = self.eplus_outputs + 1 fh.write(' \n') fh.write(' \n' % (obj.get('name'), obj.get('type'))) fh.write(' \n') - for obj in self.input().itervalues(): - if obj.has_key('name') and obj.has_key('type'): + for obj in self.input().values(): + if 'name' in obj and 'type' in obj: + #if obj.has_key('name') and obj.has_key('type'): self.eplus_inputs = self.eplus_inputs + 1 fh.write(' \n') fh.write(' \n' % (obj.get('type'), obj.get('name'))) @@ -843,7 +872,8 @@ def revert_point(self, requester_id, topic, **kwargs): """ obj = self.find_best_match(topic) - if obj and obj.has_key('default'): + if obj and 'default' in obj: + #if obj and obj.has_key('default'): value = obj.get('default') log.debug("Reverting topic " + topic + " to " + str(value)) external = False @@ -874,7 +904,8 @@ def revert_device(self, requester_id, device_name, **kwargs): point_name = obj.get('field', None) topic = device_name + "/" + point_name if point_name else device_name external = False - if obj.has_key('default'): + if 'default' in obj: + #if obj.has_key('default'): value = obj.get('default') log.debug("Reverting " + topic + " to " + str(value)) self.update_topic_rpc(requester_id, topic, value, external) @@ -894,11 +925,13 @@ def update_topic_rpc(self, requester_id, topic, value, external): def advance_simulation(self, peer, sender, bus, topic, headers, message): log.info('Advancing simulation.') - for obj in self.input().itervalues(): + for obj in self.input().values(): set_topic = obj['topic'] + '/' + obj['field'] - if obj.has_key('external') and obj['external']: + if 'external' in obj and obj['external']: + #if obj.has_key('external') and obj['external']: external = True - value = obj['value'] if obj.has_key('value') else obj['default'] + value = obj['value'] if 'value' in obj else obj['default'] + #value = obj['value'] if obj.has_key('value') else obj['default'] else: external = False value = obj['default'] diff --git a/pnnl/models/__init__.py b/pnnl/models/__init__.py index e69de29..8f39a22 100644 --- a/pnnl/models/__init__.py +++ b/pnnl/models/__init__.py @@ -0,0 +1,25 @@ +import importlib +import logging +from volttron.platform.agent import utils + +_log = logging.getLogger(__name__) +utils.setup_logging() +__version__ = "0.1" + +__all__ = ['Model'] + +class Model(object): + def __init__(self, config, **kwargs): + base_module = "volttron.pnnl.models." + try: + model_type = config["model_type"] + except KeyError as e: + _log.exception("Missing Model Type key: {}".format(e)) + raise e + _file, model_type = model_type.split(".") + module = importlib.import_module(base_module + _file) + model_class = getattr(module, model_type) + self.model = model_class(config, self) + + def get_q(self, _set, sched_index, market_index, occupied): + q = self.model.predict(_set, sched_index, market_index, occupied) diff --git a/pnnl/models/ahu.py b/pnnl/models/ahu.py new file mode 100644 index 0000000..5c48329 --- /dev/null +++ b/pnnl/models/ahu.py @@ -0,0 +1,127 @@ +import logging +import importlib + +from volttron.platform.agent import utils + +_log = logging.getLogger(__name__) +utils.setup_logging() + +class ahu(object): + SFS = "sfs" + MAT = "mat" + DAT = "dat" + SAF = "saf" + OAT = "oat" + RAT = "rat" + + def __init__(self, config, parent, **kwargs): + self.parent = parent + equipment_conf = config.get("equipment_configuration") + model_conf = config.get("model_configuration") + self.cpAir = model_conf["cpAir"] + self.c0 = model_conf["c0"] + self.c1 = model_conf["c1"] + self.c2 = model_conf["c2"] + self.c3 = model_conf["c3"] + self.power_unit = model_conf.get("unit_power", "kw") + self.mDotAir = model_conf.get("mDotAir", 0.0) + + self.name = 'AHU' + + self.has_economizer = equipment_conf["has_economizer"] + self.economizer_limit = equipment_conf["economizer_limit"] + self.min_oaf = equipment_conf.get("minimum_oaf", 0.15) + self.vav_flag = equipment_conf.get("variable-volume", True) + self.sat_setpoint = equipment_conf["supply-air sepoint"] + self.building_chiller = equipment_conf["building chiller"] + self.tset_avg = equipment_conf["nominal zone-setpoint"] + self.tDis = self.sat_setpoint + self.parent.supply_commodity = "ZoneAirFlow" + + self.fan_power = 0. + self.mDotAir = 0. + self.coil_load = 0. + + self.get_input_value = parent.get_input_value + self.smc_interval = parent.single_market_contol_interval + self.parent = parent + self.sfs_name = parent.SFS + self.mat_name = parent.MAT + self.dat_name = parent.DAT + self.saf_name = parent.SAF + self.oat_name = parent.OAT + self.rat_name = parent.RAT + + self.sfs = None + self.mat = None + self.dat = None + self.saf = None + self.oat = None + self.rat = None + + def update_data(self): + self.sfs = self.get_input_value(self.sfs_name) + self.mat = self.get_input_value(self.mat_name) + self.dat = self.get_input_value(self.dat_name) + self.saf = self.get_input_value(self.saf_name) + self.oat = self.get_input_value(self.oat_name) + self.rat = self.get_input_value(self.rat_name) + + def input_zone_load(self, q_load): + if self.vav_flag: + self.mDotAir = q_load + else: + self.tDis = q_load + self.dat = q_load + + def calculate_fan_power(self): + if self.power_unit == 'W': + fan_power = (self.c0 + self.c1 * self.mDotAir + self.c2 * pow(self.mDotAir, 2) + self.c3 * pow(self.mDotAir, + 3)) * 1000. # watts + else: + fan_power = self.c0 + self.c1 * self.mDotAir + self.c2 * pow(self.mDotAir, 2) + self.c3 * pow(self.mDotAir, + 3) # kW + return fan_power + + def calculate_cooling_load(self, oat): + if self.has_economizer: + if oat < self.tDis: + coil_load = 0.0 + elif oat < self.economizer_limit: + coil_load = self.mDotAir * self.cpAir * (self.tDis - oat) + else: + mat = self.tset_avg * (1.0 - self.min_oaf) + self.min_oaf * oat + coil_load = self.mDotAir * self.cpAir * (self.tDis - mat) + else: + mat = self.tset_avg * (1.0 - self.min_oaf) + self.min_oaf * oat + coil_load = self.mDotAir * self.cpAir * (self.tDis - mat) + + if coil_load > 0: # heating mode is not yet supported! + coil_load = 0.0 + return coil_load + + def calculate_electric_load(self): + return self.calculate_fan_power() + + def single_market_coil_load(self): + try: + coil_load = self.mDotAir * self.cpAir * (self.dat - self.mat) + except: + _log.debug("AHU for single market requires dat and mat measurements!") + coil_load = 0. + return coil_load + + def calculate_coil_load(self, oat): + oat = oat if oat is not None else self.oat + if self.building_chiller and oat is not None: + if self.smc_interval is not None: + coil_load = self.single_market_coil_load() + else: + coil_load = self.calculate_cooling_load(oat) + else: + _log.debug("AHUChiller building does not have chiller or no oat!") + coil_load = 0.0 + return abs(coil_load) + + def predict(self, set, sched_index, market_index, occupied): + return None diff --git a/pnnl/models/ahuchiller.py b/pnnl/models/ahuchiller.py index 3d25cd5..e36ea35 100644 --- a/pnnl/models/ahuchiller.py +++ b/pnnl/models/ahuchiller.py @@ -135,3 +135,6 @@ def calculate_total_power(self, oat): _log.debug("AHUChiller building does not have chiller or no oat!") self.coil_load = 0.0 return abs(self.coil_load)/self.cop/0.9 + max(self.fan_power, 0) + + def predict(self, set, sched_index, market_index, occupied): + return None \ No newline at end of file diff --git a/pnnl/models/bess.py b/pnnl/models/bess.py new file mode 100644 index 0000000..c07499c --- /dev/null +++ b/pnnl/models/bess.py @@ -0,0 +1,201 @@ +import itertools +import numpy as np +import pandas +import pulp +import logging +import importlib +import variable_group +from variable_group import VariableGroup, RANGE +from volttron.platform.agent import utils + +BESS_AVAILABLE = True + +_log = logging.getLogger(__name__) +utils.setup_logging() + +class Bess(object): + def __init__(self, config, parent, **kwargs): + # BESS CONSTANTS --- should be config parameters + self.MESA1_C0 = config.get("MESA1_C0", -1.44E-02) # unit: 1/h + self.MESA1_C_p = config.get("MESA1_C_p", -9.38E-04) # unit: 1/kWh + self.MESA1_C_n = config.get("MESA1_C_n", -9.22E-04) # unit: 1/kWht_UB_range + self.p_max = config.get("p_max", 1.0) + self.init_soc = config.get("init_soc", 0.8) + self.final_soc = config.get("final_soc", -1.0) + self.power_inject = None + self.power_reserve = None + self.ESS = None + self.get_input_value = parent.get_input_value + + def update_data(self): + self.ESS = self.get_input_value('ess') + _log.debug("Bess model update_data: {}".format(self.ESS)) + + def build_bess_constraints(self, energy_price, reserve_price): + """ + Build constraints and objective function for BESS + :param energy_price: Energy price from city market + :param reserve_price: Reserve price from city market + :return: + """ + numHours = len(energy_price) + energy_price = np.array(energy_price) + reserve_price = np.array(reserve_price) + if numHours != len(reserve_price): + raise (TypeError("The lengths of energy_price, reserve_price should match.")) + + print("EnergyPrice: {}".format(energy_price)) + print("ReservePrice: {}".format(reserve_price)) + print("p_max: {}".format(self.p_max)) + print("init_soc: {}".format(self.init_soc)) + print("final_soc: {}".format(self.final_soc)) + + numHours_range = (range(numHours),) + n2_range = (range(numHours + 1),) + + def constant(x): + def _constant(*args, **kwargs): + return x + + return _constant + + constant_zero = constant(0.0) + constant_pos_one = constant(1.0) + + bess_p_p = VariableGroup("bess_p_p", indexes=numHours_range, lower_bound_func=constant_zero, + base_name="Power injection (MW)") # Power injection into the grid, or battery discharging (non-negative) + bess_p_n = VariableGroup("bess_p_n", indexes=numHours_range, upper_bound_func=constant_zero, + base_name="Power withdrawal (MW)") # Power withdrawal from the grid, or battery charging (non-positive) + bess_b = VariableGroup("bess_b", indexes=numHours_range, is_binary_var=True, + base_name="Power withdrawal (MW)") # 1 == Discharging, 0 == Charging + bess_p = VariableGroup("bess_p", indexes=numHours_range, + base_name="Net power transfer (MW)") # power transfer from battery to grid + #+ve => discharging + #-ve => charging + bess_l = VariableGroup("bess_l", indexes=n2_range, lower_bound_func=constant_zero, + upper_bound_func=constant_pos_one, + base_name="State of charge") # State of charge (SoC) in [0, 1] + bess_r_p = VariableGroup("bess_r_p", indexes=numHours_range, + base_name="Reserve power (MW)") # Reserve power (non-negative) + + # CONSTRAINTS + constraints = [] + + def add_constraint(name, indexes, constraint_func): + name_base = name + for _ in range(len(indexes)): + name_base += "_{}" + + for index in itertools.product(*indexes): + name = name_base.format(*index) + c = constraint_func(index) + + constraints.append((c, name)) + + name = 'bess_engy_intial_condition' + + def checkInitSoC(index): + return bess_l[index] == self.init_soc + + c = checkInitSoC(0) + constraints.append((c, name)) + + if self.final_soc >= 0: + name = 'bess_engy_final_condition' + checkFinalSoC = bess_l[numHours] == self.final_soc + constraints.append((checkFinalSoC, name)) + + numHours_index = (range(numHours),) + + def engy_dynamics_func(index): + k = index[0] + return bess_l[k + 1] - bess_l[k] == self.MESA1_C0 + ( + self.MESA1_C_p * bess_p_p[k] + self.MESA1_C_n * bess_p_n[k]) * 1000 # 1000 kW / MW + + add_constraint("bess_engy_dynamics", numHours_index, engy_dynamics_func) + + def discharge_cap_func(index): + k = index[0] + return bess_p_p[k] <= self.p_max * bess_b[k] + + add_constraint("bess_discharge_cap", numHours_index, discharge_cap_func) + + def charge_cap_func(index): + k = index[0] + return -self.p_max * (1 - bess_b[k]) <= bess_p_n[k] + + add_constraint("bess_charge_cap", numHours_index, charge_cap_func) + + def pow_transfer_func(index): + k = index[0] + return bess_p[k] == bess_p_p[k] + bess_p_n[k] + + add_constraint("bess_pow_transfer", numHours_index, pow_transfer_func) + + def reg_up_nng_func(index): + k = index[0] + return bess_r_p[k] >= 0.0 + + add_constraint("bess_reg_up_nng", numHours_index, reg_up_nng_func) + + def reg_up_cap_func(index): + k = index[0] + return bess_p[k] + bess_r_p[k] <= self.p_max + + add_constraint("bess_reg_up_cap", numHours_index, reg_up_cap_func) + + def reg_SoC_cap_func(index): + k = index[0] + return bess_l[k + 1] + (self.MESA1_C_p * bess_r_p[k]) * 1000 >= 0 + + add_constraint("bess_reg_SoC_cap", numHours_index, reg_SoC_cap_func) + + objective_components = [] + + objective_components.append(energy_price * bess_p[RANGE] + reserve_price * bess_r_p[RANGE]) + + return constraints, objective_components + + def calculate_control(self, current_date_time, name=None): + hour = current_date_time.time().hour + value = 0 + if self.power_inject is not None: + value = self.power_inject[hour] + return value + + def run_bess_optimization(self, energy_price, reserve_price): + """ + Optimization method for BESS + :param energy_price: + :param reserve_price: + :return: + """ + # build constraints and objective function for TESS + tess_constraints, tess_objective_components = self.build_bess_constraints(energy_price, reserve_price) + prob = pulp.LpProblem("BESS Optimization", pulp.LpMaximize) + prob += pulp.lpSum(tess_objective_components), "Objective Function" + #prob.writeLP("pyversion.lp") + for c in tess_constraints: + prob += c + time_limit = 5 + # TBD: time_limit + #_log.debug("{}".format(prob)) + try: + prob.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=time_limit)) + except Exception as e: + _log.error("PULP failed!") + + _log.debug("Pulp LP Status: {}".format(pulp.LpStatus[prob.status])) + _log.debug("Pulp LP Objective Value: {}".format(pulp.value(prob.objective))) + + result = {} + for var in prob.variables(): + result[var.name] = var.varValue + N = len(energy_price) + bess_power_inject = [result['bess_p_{}'.format(x)] for x in range(N)] + bess_power_reserve = [result['bess_r_p_{}'.format(x)] for x in range(N)] + bess_soc = [result['bess_l_{}'.format(x)] for x in range(N + 1)] + _log.debug("bess_power_inject: {}".format(bess_power_inject)) + _log.debug("bess_power_reserve: {}".format(bess_power_reserve)) + _log.debug("bess_l: {}".format(bess_soc)) + return bess_power_inject, bess_power_reserve, bess_soc diff --git a/pnnl/models/input_names.py b/pnnl/models/input_names.py new file mode 100644 index 0000000..c9a5711 --- /dev/null +++ b/pnnl/models/input_names.py @@ -0,0 +1,28 @@ +# AHU/RTU supply-fan status +SFS = "sfs" +# AHU/RTU mixed-air temperature +MAT = "mat" +# AHU/RTU discharge-air temperature +DAT = "dat" +# AHU/RTU supply-air flow rate +SAF = "saf" +# AHU/RTU outdoor-air temperature +OAT = "oat" +# AHU/RTU return-air temperature +RAT = "rat" + +# zone temperature +ZT = "zt" +# zone discharge-air temperature +ZDAT = "zdat" +# zone supply-air flow rate +ZAF = "zaf" +# zone clooling temperature set point +CSP = "csp" +# zone heating temperature set point +HSP = "hsp" + +# Lighting dimming level +DOL = "dol" +# Lighting occupancy mode +OCC = "occ" \ No newline at end of file diff --git a/pnnl/models/light.py b/pnnl/models/light.py index 518c800..183db25 100644 --- a/pnnl/models/light.py +++ b/pnnl/models/light.py @@ -1,42 +1,47 @@ import logging -import importlib -import pandas as pd from volttron.platform.agent import utils -from datetime import timedelta as td +import volttron.pnnl.models.input_names as data_names from volttron.pnnl.models.utils import clamp _log = logging.getLogger(__name__) utils.setup_logging() -class Light(object): - DOL = "dol" - OCC = "occ" - - def __init__(self, config, **kwargs): - model_type = config.get("model_type", "simple") - _log.debug("Light Agent Model: {}".format(model_type)) - module = importlib.import_module("volttron.pnnl.models.light") - model_class = getattr(module, model_type) - self.model = model_class(config, self) - - def get_q(self, _set, sched_index, market_index, occupied): - q = self.model.predict(_set, sched_index, market_index, occupied) - return q +# class Light(object): +# DOL = "dol" +# OCC = "occ" +# +# def __init__(self, config, **kwargs): +# model_type = config.get("model_type", "simple") +# module = importlib.import_module("volttron.pnnl.models.light") +# model_class = getattr(module, model_type) +# self.model = model_class(config, self) +# +# def get_q(self, _set, sched_index, market_index, occupied): +# q = self.model.predict(_set, sched_index, market_index, occupied) +# return q -class simple(object): +class simple_profile(object): + DOL = "dol" + OCC = "occ" def __init__(self, config, parent, **kwargs): self.parent = parent self.inputs = parent.inputs - if "rated_power" in config: - self.rated_power = config["rated_power"] - else: - self.rated_power = config["model_parameters"]["rated_power"] + self.rated_power = config["rated_power"] + try: + self.lighting_schedule = config["default_lighting_schedule"] + except KeyError: + _log.warning("No no default lighting schedule!") + self.lighting_schedule = [1.0]*24 def update_data(self): pass def predict(self, _set, sched_index, market_index, occupied): - return _set*self.rated_power + if not occupied: + power = self.lighting_schedule[sched_index]*self.rated_power + else: + power = _set*self.rated_power + return power diff --git a/pnnl/models/meter.py b/pnnl/models/meter.py index 16e6304..7ca2cd9 100644 --- a/pnnl/models/meter.py +++ b/pnnl/models/meter.py @@ -10,21 +10,23 @@ utils.setup_logging() -class Meter(object): - WBP = "wbp" - - def __init__(self, config, **kwargs): - model_type = config.get("model_type") - module = importlib.import_module("volttron.pnnl.models.meter") - model_class = getattr(module, model_type) - self.model = model_class(config, self) - - def get_q(self, _set, sched_index, market_index, occupied): - q = self.model.predict(_set, sched_index, market_index, occupied) - return q +# class Meter(object): +# WBP = "wbp" +# +# def __init__(self, config, **kwargs): +# model_type = config.get("model_type") +# module = importlib.import_module("volttron.pnnl.models.meter") +# model_class = getattr(module, model_type) +# self.model = model_class(config, self) +# +# def get_q(self, _set, sched_index, market_index, occupied): +# q = self.model.predict(_set, sched_index, market_index, occupied) +# return q class simple(object): + WBP = "wbp" + def __init__(self, config, parent, **kwargs): self.parent = parent self.inputs = parent.inputs @@ -33,6 +35,6 @@ def update_data(self): pass def predict(self, _set, sched_index, market_index, occupied): - pass + return None diff --git a/pnnl/models/rtu.py b/pnnl/models/rtu.py index 99975f9..d858cb5 100644 --- a/pnnl/models/rtu.py +++ b/pnnl/models/rtu.py @@ -8,7 +8,33 @@ utils.setup_logging() -class RTU(object): +# class RTU(object): +# OAT = "oat" +# ZT = "zt" +# ZDAT = "zdat" +# MC = "mclg" +# MH = "mhtg" +# CSP = "csp" +# HSP = "hsp" +# ZTSP = "ztsp" +# OPS = { +# "csp": [(operator.gt, operator.add), (operator.lt, operator.sub)], +# "hsp": [(operator.lt, operator.sub), (operator.gt, operator.add)] +# } +# +# def __init__(self, config, **kwargs): +# model_type = config.get("model_type", "firstorderzone") +# module = importlib.import_module("volttron.pnnl.models.rtu") +# model_class = getattr(module, model_type) +# self.prediction_array = [] +# self.model = model_class(config, self) +# +# def get_q(self, _set, sched_index, market_index, occupied): +# q = self.model.predict(_set, sched_index, market_index, occupied) +# return q + + +class firstorderzone(object): OAT = "oat" SFS = "sfs" ZT = "zt" @@ -19,21 +45,8 @@ class RTU(object): OPS = { "csp": [(operator.gt, operator.add), (operator.lt, operator.sub)], "hsp": [(operator.lt, operator.sub), (operator.gt, operator.add)] -} + } - def __init__(self, config, **kwargs): - model_type = config.get("model_type", "firstorderzone") - module = importlib.import_module("volttron.pnnl.models.rtu") - model_class = getattr(module, model_type) - self.prediction_array = [] - self.model = model_class(config, self) - - def get_q(self, _set, sched_index, market_index, occupied): - q = self.model.predict(_set, sched_index, market_index, occupied) - return q - - -class firstorderzone(object): def __init__(self, config, parent, **kwargs): self.parent = parent self.c1 = config["c1"] @@ -92,7 +105,7 @@ def update(self, _set, sched_index, market_index, occupied): self.zt_predictions[market_index] = _set def predict(self, _set, sched_index, market_index, occupied): - if self.smc_interval is not None or market_index == -1: + if self.parent.market_number == 1: oat = self.oat zt = self.zt occupied = self.sfs if self.sfs is not None else occupied @@ -174,7 +187,7 @@ def update(self, _set, sched_index, market_index, occupied): q = self.get_t(_set, sched_index, market_index, occupied, dc=False) def init_predictions(self, output_info): - if self.single_market_contol_interval is not None: + if self.parent.market_number == 1: return occupied = self.check_future_schedule(self.current_datetime) if occupied: @@ -184,7 +197,7 @@ def init_predictions(self, output_info): q = self.model.predict(_set, -1, -1, occupied, False) def get_t(self, temp_stpt, sched_index, market_index, occupied, dc=True): - if self.smc_interval is not None: + if self.parent.market_number == 1: oat = self.oat zt = self.zt runtime = self.parent.single_market_contol_interval @@ -220,7 +233,7 @@ def get_t(self, temp_stpt, sched_index, market_index, occupied, dc=True): off_condition = ops[1][0] off_operator = ops[1][1] prediction_array = [zt] - for i in xrange(runtime): + for i in range(runtime): _log.debug("{} - temperature: {} - setpoint: {} - on: {} - current_time: {} - index: {} - loop: {}".format( self.parent.agent_name, zt, diff --git a/pnnl/models/tess.py b/pnnl/models/tess.py new file mode 100644 index 0000000..605f49a --- /dev/null +++ b/pnnl/models/tess.py @@ -0,0 +1,612 @@ +import itertools +import numpy as np +import pulp +import logging +import importlib +from variable_group import VariableGroup, RANGE +from volttron.platform.agent import utils + +_log = logging.getLogger(__name__) +utils.setup_logging() + +class Tess(object): + def __init__(self, config, parent, **kwargs): + self.a_coef = config.get("a_coef", [0.257986, 0.0389016, -0.00021708, 0.0468684, -0.00094284, -0.00034344]) + self.b_coef = config.get("b_coef", [0.933884, -0.058212, 0.00450036, 0.00243, 0.000486, -0.001215]) + self.p_coef = np.array(config.get("p_coef", [4.0033, -3.5162, -5.4302, 5.6807, -1.1989, -0.1963, 0.8593])) + self.r_coef = np.array(config.get("r_coef", [0.9804, -2.6207, 3.6708, -2.6975, 0.0446, 1.2533, 0.2494])) + self.c_coef = np.array(config.get("c_coef", [0.222903, 0.313387, 0.46371])) + self.Q_norm = config.get("Q_norm", 95) # kW + self.Q_stor = config.get("Q_stor", 500) # kWh + self.COP = config.get("COP", 3.5) + self.Cf = config.get("Cf", 3.915) # kJ/kg-K + self.m_ice = config.get("m_ice", 5.24) # kg/s + self.T_cw_ch = config.get("T_cw_ch", -5) # degrees C + self.T_cw_norm = config.get("T_cw_norm", 4.4) # degrees C + self.T_fr = config.get("T_fr", 0) # degrees C + self.init_soc = config.get("init_soc", 0.8) + self.final_soc = config.get("final_soc", -1.0) + self.charging_rate = None + self.chiller_only = 0 + self.ice_discharge_chiller_both = 1 + self.discharge_only = 2 + self.ice_discharge = 3 + self.charge_ice = 4 + self.charge_ice_plus_chiller = 5 + self.TSS = None + self.get_input_value = parent.get_input_value + self.soc = None + + def update_data(self): + self.TSS = self.get_input_value('tss') + _log.debug("Tess model update_data: {}".format(self.TSS)) + + @staticmethod + def poly(c, x): + """ + + :param c: Array of Float64 + :param x: + :return: + """ + result = None + if isinstance(c, np.ndarray): + arr = np.array(range(1, len(c))) + result = c[0] + np.sum(c[1:] * (x ** arr)) + return result + + def sigma_12(self, T_cw, t_out, coef): + """ + + :param T_cw: + :param t_out: + :param coef: Array of Float64 + :return: + """ + result = coef[0] + coef[1] * T_cw + coef[2] * T_cw ** 2 + coef[3] * t_out + coef[4] * t_out ** 2 + coef[ + 5] * T_cw * t_out + return result + + def sigma_3(self, PLR): + return self.poly(self.c_coef, PLR) + + def Q_avail(self, T_cw, t_out): + return self.Q_norm * self.sigma_12(T_cw, t_out, self.a_coef) + + def P_chiller(self, q_chiller, T_cw, t_out): + """ + + :param q_chiller: Cooling load + :param T_cw: Chilled water temperature + :param t_out: Outdoor temperature + :return: + """ + return self.Q_avail(T_cw, t_out) * self.sigma_12(T_cw, t_out, self.b_coef) * self.sigma_3(q_chiller / self.Q_avail(T_cw, t_out)) + + def u_UB(self, l): + return self.poly(self.p_coef, l) * self.m_ice * self.Cf * (self.T_fr - self.T_cw_ch) + + def u_LB(self, l): + return self.poly(self.r_coef, l) * self.m_ice * self.Cf * (self.T_cw_norm - self.T_fr) + + def constant(self, x): + def _constant(*args, **kwargs): + return x + + return _constant + + def build_tess_constraints(self, energy_price, reserve_price, t_out, q_cool): + """ + Build constraints and objective function for TESS + :param energy_price: Energy price from city market + :param reserve_price: Reserve price from city market + :param t_out: Outdoor temperature -- degree celcius + :param q_cool: Cooling load --- kW + :return: + """ + numHours = len(energy_price) + energy_price = np.array(energy_price) + reserve_price = np.array(reserve_price) + _log.debug("{}, ep: {}, rp: {}, oat: {}, qcool: {}".format(numHours, len(energy_price), len(reserve_price), len(t_out), len(q_cool))) + if numHours != len(reserve_price) or numHours != len(t_out) or numHours != len(q_cool): + raise (TypeError("The lengths of energy_price, reserve_price, and q_cool should match.")) + + print("EnergyPrice: {}".format(energy_price)) + print("ReservePrice: {}".format(reserve_price)) + print("T_out: {}".format(t_out)) + print("Q_cool: {}".format(q_cool)) + + numHours_range = (range(numHours),) + n2_range = (range(numHours + 1), ) + tess_p = VariableGroup("tess_p", indexes=numHours_range, lower_bound_func=self.constant(0.0)) # Power withdrawal from the grid (non-negative), unit: kW + tess_l = VariableGroup("tess_l", indexes=n2_range, lower_bound_func=self.constant(0.1), upper_bound_func=self.constant(0.9)) # State of charge (SoC) in [0.1, 0.9] + tess_r = VariableGroup("tess_r", indexes=numHours_range, lower_bound_func=self.constant(0.0)) # Reserve power (non-negative) + tess_u = VariableGroup("tess_u", indexes=numHours_range) # Ice-storage charging rate + tess_u_p = VariableGroup("tess_u_p", indexes=numHours_range, lower_bound_func=self.constant(0.0), base_name="u^+") # Positive parts of ice-storage charging rate + tess_u_n = VariableGroup("tess_u_n", indexes=numHours_range, upper_bound_func=self.constant(0.0), base_name="u^-") # Negative parts of ce-storage charging rate + tess_b = VariableGroup("tess_b", indexes=numHours_range, is_binary_var=True, ) # Ice-storage charging/discharging indicator: 1 == Charging, 0 == Discharging + tess_u_r = VariableGroup("tess_u_r", indexes=numHours_range, base_name="\\tilde{u}") # Ice-storage charging rate + tess_u_p_r = VariableGroup("tess_u_p_r", indexes=numHours_range, lower_bound_func=self.constant(0.0), base_name="\\tilde{u}^+") # Positive parts of ice-storage charging rate + tess_u_n_r = VariableGroup("tess_u_n_r", indexes=numHours_range, upper_bound_func=self.constant(0.0), base_name="\\tilde{u}^-") # Negative parts of ce-storage charging rate + tess_b_r = VariableGroup("tess_b_r", indexes=numHours_range, is_binary_var=True, base_name="\\tilde{b}") # Ice-storage charging/discharging indicator: 1 == Charging, 0 == Discharging + + t_UB = np.array([0.1, 0.6, 0.78, 0.9]) + t_UB_range = range(len(t_UB)-1) + u_UB_res = np.array([self.u_UB(x) for x in t_UB[0:]]) + #print("u_UB_res: {}".format(u_UB_res)) + #print("t_UB: {}".format(t_UB[0:-1] - t_UB[1:])) + a_UB = (u_UB_res[0:-1] - u_UB_res[1:]) / (t_UB[0:-1] - t_UB[1:]) + b_UB = u_UB_res[0:-1] - a_UB * t_UB[0:-1] + + print("a_UB: {}".format(a_UB)) + print("b_UB: {}".format(b_UB)) + + tess_l_UB = VariableGroup("tess_l_UB", (t_UB_range, range(numHours),), base_name="\\overline{l}") # Piecewise linearization weights for u_UB + tess_S_UB = VariableGroup("tess_S_UB", (t_UB_range, range(numHours),), is_binary_var=True, base_name="\\overline{S}") # Piecewise linearization weights for u_UB + + t_LB = np.array([0.1, 0.3, 0.5, 0.65, 0.78, 0.9]) + t_LB_range = range(len(t_LB)-1) + + u_LB_res = np.array([self.u_LB(x) for x in t_LB[0:]]) + #print("Len of u_LB_res: {}, len of t_LB: {}".format(len(u_LB_res), len(t_LB))) + #print("u_LB_res: {}, t_LB: {}".format(u_LB_res[0:-1], u_LB_res[1:])) + #print("u_LB_res: {}, t_LB: {}".format(t_LB[0:-1], t_LB[1:])) + + a_LB = (u_LB_res[0:-1] - u_LB_res[1:]) / (t_LB[0:-1] - t_LB[1:]) + b_LB = u_LB_res[0:-1] - a_LB * t_LB[0:-1] + print("a_LB: {}".format(a_LB)) + print("b_LB: {}".format(b_LB)) + + tess_l_LB = VariableGroup("tess_l_LB", (t_LB_range, range(numHours),), base_name="\\underline{\\tilde{l}}") # Piecewise linearization weights for u_LB + tess_S_LB = VariableGroup("tess_S_LB", (t_LB_range, range(numHours),), is_binary_var=True, base_name="\\underline{\\tilde{S}}")# Piecewise linearization weights for u_LB + + t_PLR = np.array([0, 0.5, 1]) + a_Pch_p = np.zeros((len(t_PLR)-1, numHours)) + for k in range(numHours): + a = np.array([self.P_chiller(x * self.Q_avail(self.T_cw_ch, t_out[k]), self.T_cw_ch, t_out[k]) for x in t_PLR[0:-1]]) + b = np.array([self.P_chiller(x * self.Q_avail(self.T_cw_ch, t_out[k]), self.T_cw_ch, t_out[k]) for x in t_PLR[1:]]) + a_Pch_p[:, k] = (a - b)/(t_PLR[0:-1] - t_PLR[1:])/self.Q_avail(self.T_cw_ch, t_out[k]) + + b_Pch_p = np.zeros((len(t_PLR)-1, numHours)) + for k in range(numHours): + a = np.array([self.P_chiller(x * self.Q_avail(self.T_cw_ch, t_out[k]), self.T_cw_ch, t_out[k]) for x in t_PLR[0:-1]]) + s = [x * self.Q_avail(self.T_cw_ch, t_out[k]) for x in t_PLR[0:-1]] + b = a_Pch_p[:, k] * s + #b_Pch_p[:, k] = self.P_chiller.(t_PLR[1:end - 1] * self.Q_avail(self.T_cw_ch, T_out[k]), self.T_cw_ch, T_out[k]) - a_Pch_p[:,k]. * t_PLR[1:end - 1] * self.Q_avail(self.T_cw_ch, T_out[k]); + b_Pch_p[:, k] = a - b + + a_Pch_n = np.zeros((len(t_PLR)-1, numHours)) + for k in range(numHours): + a = np.array([self.P_chiller(x * self.Q_avail(self.T_cw_norm, t_out[k]), self.T_cw_norm, t_out[k]) for x in t_PLR[0:-1]]) + b = np.array([self.P_chiller(x * self.Q_avail(self.T_cw_norm, t_out[k]), self.T_cw_norm, t_out[k]) for x in t_PLR[1:]]) + a_Pch_n[:, k] = (a - b) / (t_PLR[0:-1] - t_PLR[1:]) / self.Q_avail(self.T_cw_norm, t_out[k]) + + b_Pch_n = np.zeros((len(t_PLR)-1, numHours)) + for k in range(numHours): + a = np.array([self.P_chiller(x * self.Q_avail(self.T_cw_norm, t_out[k]), self.T_cw_norm, t_out[k]) for x in t_PLR[0:-1]]) + s = [x * self.Q_avail(self.T_cw_norm, t_out[k]) for x in t_PLR[0:-1]] + b = a_Pch_n[:, k] * s + b_Pch_n[:, k] = a - b + + #print("a_Pch_p: {}".format(a_Pch_p)) + #print("b_Pch_p: {}".format(b_Pch_p)) + #print("a_Pch_n: {}".format(a_Pch_n)) + #print("b_Pch_n: {}".format(b_Pch_n)) + + t_PLR_range = range(len(t_PLR)-1) + tess_q_p = VariableGroup("tess_q_p", (t_PLR_range, range(numHours),), base_name="q^+") + tess_S_p = VariableGroup("tess_S_p", (t_PLR_range, range(numHours),), is_binary_var=True, base_name="S^+") # Piecewise binary variable for q_p + tess_q_n = VariableGroup("tess_q_n", (t_PLR_range, range(numHours),), base_name="q^-") + tess_S_n = VariableGroup("tess_S_n", (t_PLR_range, range(numHours),), is_binary_var=True, base_name="S^-") # Piecewise binary variable for q_n + tess_q_p_r = VariableGroup("tess_q_p_r", (t_PLR_range, range(numHours),), base_name="\\tilde{q}^+") + tess_S_p_r = VariableGroup("tess_S_p_r", (t_PLR_range, range(numHours),), is_binary_var=True, base_name="\\tilde{S}^+") # Piecewise binary variable for q_p_r + tess_q_n_r = VariableGroup("tess_q_n_r", (t_PLR_range, range(numHours),), base_name="\\tilde{q}^-") + tess_S_n_r = VariableGroup("tess_S_n_r", (t_PLR_range, range(numHours),), is_binary_var=True, base_name="\\tilde{S}^-") # Piecewise binary variable for q_n_r + + # CONSTRAINTS + constraints = [] + + def add_constraint(name, indexes, constraint_func): + name_base = name + for _ in range(len(indexes)): + name_base += "_{}" + + for index in itertools.product(*indexes): + name = name_base.format(*index) + c = constraint_func(index) + + constraints.append((c, name)) + + name = 'tess_conInitSoc' + + def checkInitSoC(index): + return tess_l[index] == self.init_soc + + c = checkInitSoC(0) + constraints.append((c, name)) + + if self.final_soc >= 0: + name = 'tess_conFinalSoc_0' + checkFinalSoC = tess_l[numHours] == self.final_soc + constraints.append((checkFinalSoC, name)) + + numHours_index = (range(numHours),) + def tess_conSoDynamics_func(index): + k = index[0] + return tess_l[k+1] - tess_l[k] == tess_u[k] / self.Q_stor + add_constraint("tess_conSoDynamics", numHours_index, tess_conSoDynamics_func) + + def tess_conThermalCharging_func(index): + k = index[0] + return tess_u[k] == tess_u_p[k] + tess_u_n[k] + add_constraint("tess_conThermalCharging", numHours_index, tess_conThermalCharging_func) + + def tess_ab_func(index): + k = index[0] + return tess_l[k] == pulp.lpSum(tess_l_UB[RANGE,k]) + add_constraint("tess_ab", numHours_index, tess_ab_func) + + def tess_ab1_func(index): + k = index[0] + return pulp.lpSum(tess_S_UB[RANGE, k]) == 1 + + add_constraint("tess_ab1", numHours_index, tess_ab1_func) + + t_UB_index = (range(len(t_UB)-1), range(numHours),) + + def tess_ab2_func(index): + i, k = index + return tess_S_UB[i, k] * t_UB[i] <= tess_l_UB[i, k] + add_constraint("tess_ab2", t_UB_index,tess_ab2_func) + + def tess_ab3_func(index): + i, k = index + return tess_l_UB[i, k] <= tess_S_UB[i, k] * t_UB[i+1] + add_constraint("tess_ab3", t_UB_index, tess_ab3_func) + + def tess_conThermalChargingUB(index): + k = index[0] + return tess_u_p[k] <= pulp.lpSum(a_UB * tess_l_UB[RANGE, k] + b_UB * tess_S_UB[RANGE, k]) + + add_constraint("tess_conThermalChargingUB", numHours_index, tess_conThermalChargingUB) # u_UB(l[k]) + + def tess_ab6_func(index): + k = index[0] + return tess_u_p[k] <= tess_b[k] * 1e9 + + add_constraint("tess_ab6", numHours_index, tess_ab6_func) # u_UB(l[k]) + + def tess_ab5_func(index): + k = index[0] + return tess_l[k] == pulp.lpSum(tess_l_LB[RANGE, k]) + + add_constraint("tess_ab5", numHours_index, tess_ab5_func) # u_UB(l[k]) + + def tess_ab7_func(index): + k = index[0] + return pulp.lpSum(tess_S_LB[RANGE, k]) == 1 + + add_constraint("tess_ab7", numHours_index, tess_ab7_func) + + t_LB_index = (range(len(t_LB)-1), range(numHours),) + def tess_ab8_func(index): + i, k = index + return tess_S_LB[i, k] * t_LB[i] <= tess_l_LB[i, k] + add_constraint("tess_ab8", t_LB_index, tess_ab8_func) + + def tess_ab9_func(index): + i, k = index + return tess_l_LB[i, k] <= tess_S_LB[i, k] * t_LB[i + 1] + + add_constraint("tess_ab9", t_LB_index, tess_ab9_func) + + def tess_conThermalChargingLB_func(index): + k = index[0] + return tess_u_n[k] >= -pulp.lpSum(a_LB * tess_l_LB[RANGE, k] + b_LB * tess_S_LB[RANGE, k]) + + add_constraint("tess_conThermalChargingLB", numHours_index, tess_conThermalChargingLB_func) # -u_LB(l[k]) + + def tess_bc1_func(index): + k = index[0] + return tess_u_n[k] >= -(1 - tess_b[k]) * 1e9 + + add_constraint("tess_bc1", numHours_index, tess_bc1_func) + + def tess_bc2_func(index): + k = index[0] + return tess_u[k] + q_cool[k] >= 0 + + add_constraint("tess_bc2", numHours_index, tess_bc2_func) + + def tess_bc3_func(index): + k = index[0] + return q_cool[k] + tess_u_p[k] == pulp.lpSum(tess_q_p[RANGE, k]) + + add_constraint("tess_bc3", numHours_index, tess_bc3_func) + + def tess_bc4_func(index): + k = index[0] + return pulp.lpSum(tess_S_p[RANGE, k]) == 1 + + add_constraint("tess_bc4", numHours_index, tess_bc4_func) + + t_PLR_index = (range(len(t_PLR)-1), range(numHours)) + + def tess_S_p_func(index): + i, k = index + return tess_S_p[i, k] * t_PLR[i] * self.Q_avail(self.T_cw_ch, t_out[k]) <= tess_q_p[i, k] + + add_constraint("tess_S_p_constr", t_PLR_index, tess_S_p_func) + + def tess_D_p_func(index): + i, k = index + return tess_q_p[i, k] <= tess_S_p[i, k] * t_PLR[i + 1] * self.Q_avail(self.T_cw_ch, t_out[k]) + + add_constraint("tess_D_p", t_PLR_index, tess_D_p_func) + + def tess_q_cool_func(index): + k = index[0] + return q_cool[k] + tess_u_n[k] == pulp.lpSum(tess_q_n[RANGE, k]) + + add_constraint("tess_q_cool", numHours_index, tess_q_cool_func) + + def tess_S_n_func(index): + k = index[0] + return pulp.lpSum(tess_S_n[RANGE, k]) == 1 + + add_constraint("tess_S_n_constr", numHours_index, tess_S_n_func) + + def tess_X_n_func(index): + i, k = index + return tess_S_n[i, k] * t_PLR[i] * self.Q_avail(self.T_cw_norm, t_out[k]) <= tess_q_n[i, k] + + add_constraint("tess_X_n", t_PLR_index, tess_X_n_func) + + def tess_q_n_func(index): + i, k = index + return tess_q_n[i, k] <= tess_S_n[i, k] * t_PLR[i + 1] * self.Q_avail(self.T_cw_norm, t_out[k]) + + add_constraint("tess_S_n_constr", t_PLR_index, tess_q_n_func) + + def tess_conPowConsumption_func(index): + k = index[0] + + xx = (1 - tess_b[k]) * self.P_chiller(q_cool[k], self.T_cw_ch, t_out[k]) + yy = tess_b[k] * self.P_chiller(q_cool[k], self.T_cw_norm, t_out[k]) + a = a_Pch_p[:, k] * tess_q_p[RANGE, k] + b = b_Pch_p[:, k] * tess_S_p[RANGE, k] + c = a_Pch_n[:, k] * tess_q_n[RANGE,k] + d = b_Pch_n[:, k] * tess_S_n[RANGE,k] + return tess_p[k] == pulp.lpSum(a+b) + pulp.lpSum(c+d) - xx - yy + + add_constraint("tess_conPowConsumption", numHours_index, tess_conPowConsumption_func) + + def tess_conReserveThermalCharging_func(index): + k = index[0] + return tess_u_r[k] == tess_u_p_r[k] + tess_u_n_r[k] + + add_constraint("tess_conReserveThermalCharging", numHours_index, tess_conReserveThermalCharging_func) + + def tess_conReserveThermalChargingUB(index): + k = index[0] + return tess_u_p_r[k] <= tess_u_p[k] + + add_constraint("conReserveThermalChargingUB", numHours_index, tess_conReserveThermalChargingUB) + + def tess_conReserveThermalCharginge9_func(index): + k = index[0] + return tess_u_p_r[k] <= tess_b_r[k] * 1e9 + + add_constraint("tess_conReserveThermalCharginge9", numHours_index, tess_conReserveThermalCharginge9_func) + + def tess_conReserveThermalChargingLB(index): + k = index[0] + return tess_u_n_r[k] >= -pulp.lpSum(a_LB * tess_l_LB[RANGE, k] + b_LB * tess_S_LB[RANGE, k]) + add_constraint("tess_conReserveThermalChargingLB", numHours_index, tess_conReserveThermalChargingLB) # u_LB(l[k]) + + def tess_conReserveThermalChargingLBe9_func(index): + k = index[0] + return tess_u_n_r[k] >= (1 - tess_b_r[k]) * -1e9 + + add_constraint("tess_conReserveThermalChargingLBe9", numHours_index, tess_conReserveThermalChargingLBe9_func) + + def tess_ab10_func(index): + k = index[0] + return tess_u_r[k] + q_cool[k] >= 0 + + add_constraint("tess_ab10", numHours_index, tess_ab10_func) + + def tess_ab11_func(index): + k = index[0] + return q_cool[k] + tess_u_p_r[k] == pulp.lpSum(tess_q_p_r[RANGE, k]) + + add_constraint("tess_ab11", numHours_index, tess_ab11_func) + + def tess_ab12_func(index): + k = index[0] + return pulp.lpSum(tess_S_p_r[RANGE, k]) == 1 + + add_constraint("tess_ab12", numHours_index, tess_ab12_func) + + def tess_ab13_func(index): + i, k = index + return tess_S_p_r[i, k] * t_PLR[i] * self.Q_avail(self.T_cw_ch, t_out[k]) <= tess_q_p_r[i, k] + + add_constraint("tess_ab13", t_PLR_index, tess_ab13_func) + + def tess_ab14_func(index): + i, k = index + return tess_q_p_r[i, k] <= tess_S_p_r[i, k] * t_PLR[i + 1] * self.Q_avail(self.T_cw_ch, t_out[k]) + + add_constraint("tess_ab14", t_PLR_index, tess_ab14_func) + + def tess_ab15_func(index): + k = index[0] + return q_cool[k] + tess_u_n_r[k] == pulp.lpSum(tess_q_n_r[RANGE, k]) + + add_constraint("tess_ab15", numHours_index, tess_ab15_func) + + def tess_ab16_func(index): + k = index[0] + return pulp.lpSum(tess_S_n_r[RANGE, k]) == 1 + + add_constraint("tess_ab16", numHours_index, tess_ab16_func) + + def tess_snr_func(index): + i, k = index + return tess_S_n_r[i, k] * t_PLR[i] * self.Q_avail(self.T_cw_norm, t_out[k]) <= tess_q_n_r[i, k] + + add_constraint("tess_snr", t_PLR_index, tess_snr_func) + + def tess_qnr_func(index): + i, k = index + return tess_q_n_r[i, k] <= tess_S_n_r[i, k] * t_PLR[i + 1] * self.Q_avail(self.T_cw_norm, t_out[k]) + + add_constraint("tess_qnr", t_PLR_index, tess_qnr_func) + + def tess_conReserveCapacity(index): + k = index[0] + + xx = (1 - tess_b_r[k]) * self.P_chiller(q_cool[k], self.T_cw_ch, t_out[k]) + yy = tess_b_r[k] * self.P_chiller(q_cool[k], self.T_cw_norm, t_out[k]) + a = a_Pch_p[:, k] * tess_q_p_r[RANGE, k] + b = b_Pch_p[:, k] * tess_S_p_r[RANGE, k] + c = a_Pch_n[:, k] * tess_q_n_r[RANGE, k] + d = b_Pch_n[:, k] * tess_S_n_r[RANGE, k] + return tess_r[k] == tess_p[k] - pulp.lpSum(a+b)\ + - pulp.lpSum(c+d)\ + + xx\ + + yy + + add_constraint("tess_conReserveCapacity", numHours_index, tess_conReserveCapacity) + + def tess_conReserveEnergyLimit_func(index): + k = index[0] + return tess_l[k] + tess_u_r[k] / self.Q_stor >= 0 + + add_constraint("tess_conReserveEnergyLimit", numHours_index, tess_conReserveEnergyLimit_func) + objective_components = list() + + objective_components.append((-energy_price * tess_p[RANGE])/1000 + (reserve_price * tess_r[RANGE])/ 1000) + return constraints, objective_components + + def run_tess_optimization(self, energy_price, reserve_price, t_out, q_cool): + """ + Run optimization for TESS + :param energy_price: + :param reserve_price: + :param t_out: + :param q_cool: + :return: + """ + _log.debug("TESS model: energy_price:{price}, reserve_price:{reserve}, oat: {oat}, q_cool: {q_cool}".format( + price=energy_price, + reserve=reserve_price, + oat=t_out, + q_cool=q_cool + )) + # build constraints and objective function for TESS + tess_constraints, tess_objective_components = self.build_tess_constraints(energy_price, + reserve_price, + t_out, + q_cool) + + prob = pulp.LpProblem("TESS Optimization", pulp.LpMaximize) + prob += pulp.lpSum(tess_objective_components), "Objective Function" + prob.writeLP("pyversion.lp") + for c in tess_constraints: + prob += c + prob.writeLP("tess_output.txt") + time_limit = 30 + # TBD: time_limit + #try: + prob.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=time_limit)) + #except Exception as e: + #print("PULP failed!") + + print(pulp.LpStatus[prob.status]) + print(pulp.value(prob.objective)) + + result = {} + for var in prob.variables(): + result[var.name] = var.varValue + + N=len(energy_price) + p_result = [result['tess_p_{}'.format(x)] for x in range(N) ] + r_result = [result['tess_r_{}'.format(x)] for x in range(N)] + self.charging_rate = [result['tess_u_{}'.format(x)] for x in range(N) ] + self.soc = [result['tess_l_{}'.format(x)] for x in range(N+1)] + print("TESS: charging_rate(u): {}".format(self.charging_rate)) + tess_powerInject = - np.array(p_result) / 1000 # MW + tess_powerReserve = np.array(r_result) / 1000 # MW + + return tess_powerInject, tess_powerReserve + + + def calculate_control(self, current_date_time, q_cool): + hour = current_date_time.time().hour + minute = current_date_time.time().minute + mode = self.chiller_only + elapsed_time_in_1hour = float(minute)/60 + _log.debug("TESS: calculate_control: datetime: {}, self.soc: {}, q_cool: {}".format(current_date_time, self.soc, q_cool)) + _log.debug("TESS: hour: {}, minute: {}, elapsed_time_in_1hour: {}".format(hour, minute, elapsed_time_in_1hour)) + try: + if self.charging_rate is None: + _log.debug("TESS: self.charging_rate is None ") + return self.chiller_only + else: + if self.charging_rate[hour] == 0: + _log.debug("TESS: self.charging_rate[hour] == 0") + mode = self.chiller_only + _log.debug("TESS: charging_rate: {}".format(self.charging_rate[hour])) + elif self.charging_rate[hour] < 0 and self.charging_rate[hour] + q_cool[hour] == 0: + _log.debug("TESS: self.charging_rate[hour] < 0 and self.charging_rate[hour] + q_cool[hour] == 0") + _log.debug("TESS: u[hour]: {}, self.u_LB(self.soc[hour]: {}, self.u_UB(self.soc[hour]): {}, self.soc[hour]: {}".format(self.charging_rate[hour], + self.u_LB(self.soc[hour]), + self.u_UB(self.soc[hour]), + self.soc[hour])) + first_max = -self.charging_rate[hour]/self.u_LB(self.soc[hour]) + second_max = 1 + self.charging_rate[hour]/self.u_LB(self.soc[hour]) + _log.debug("TESS: first_max: {}, second_max: {}".format(first_max, second_max)) + if elapsed_time_in_1hour <= first_max: + mode = self.ice_discharge + elif elapsed_time_in_1hour <= second_max: + mode = self.chiller_only + elif self.charging_rate[hour] < 0 and (self.charging_rate[hour] + q_cool[hour] > 0): + _log.debug("TESS: self.charging_rate[hour] < 0 and (self.charging_rate[hour] + q_cool[hour] > 0)") + _log.debug("TESS: u[hour]: {}, self.u_LB(self.soc[hour]: {}, self.u_UB(self.soc[hour]): {}, self.soc[hour]: {}".format(self.charging_rate[hour], + self.u_LB(self.soc[hour]), + self.u_UB(self.soc[hour]), + self.soc[hour])) + first_max = -self.charging_rate[hour]/self.u_UB(self.soc[hour]) + second_max = 1 + self.charging_rate[hour]/self.u_UB(self.soc[hour]) + _log.debug("TESS: first_max: {}, second_max: {}".format(first_max, second_max)) + if elapsed_time_in_1hour <= first_max: + mode = self.ice_discharge_chiller_both + elif elapsed_time_in_1hour <= second_max: + mode = self.chiller_only + elif self.charging_rate[hour] > 0 and q_cool[hour] == 0: + _log.debug("TESS: self.charging_rate[hour] > 0 and q_cool[hour] == 0") + _log.debug("TESS: u[hour]: {}, self.u_LB(self.soc[hour]: {}, self.u_UB(self.soc[hour]): {}, self.soc[hour]: {}".format(self.charging_rate[hour], + self.u_LB(self.soc[hour]), + self.u_UB(self.soc[hour]), + self.soc[hour])) + first_max = self.charging_rate[hour] / self.u_UB(self.soc[hour]) + second_max = 1 - self.charging_rate[hour] / self.u_UB(self.soc[hour]) + _log.debug("TESS: first_max: {}, second_max: {}".format(first_max, second_max)) + if elapsed_time_in_1hour <= first_max: + mode = self.ice_discharge_chiller_both + elif elapsed_time_in_1hour <= second_max: + mode = self.chiller_only + elif self.charging_rate[hour] > 0: + _log.debug("TESS: self.charging_rate[hour] > 0 ") + first_max = self.charging_rate[hour] / self.u_UB(self.soc[hour]) + second_max = 1 - self.charging_rate[hour] / self.u_UB(self.soc[hour]) + if elapsed_time_in_1hour <= first_max: + mode = self.charge_ice_plus_chiller + elif elapsed_time_in_1hour <= second_max: + mode = self.chiller_only + except ValueError as e: + _log.exception("TESS: ValueError : {}".format(e)) + _log.debug("TESS: calculate_control Mode: {}".format(mode)) + return mode \ No newline at end of file diff --git a/pnnl/models/variable_group.py b/pnnl/models/variable_group.py new file mode 100644 index 0000000..6e4e933 --- /dev/null +++ b/pnnl/models/variable_group.py @@ -0,0 +1,111 @@ +import itertools +import numpy as np +import pandas +import pulp + +__all__ = ['VariableGroup', 'RANGE'] + +# variable to indicate that we want all variables that match a pattern +# one item in the tuple key can be RANGE +RANGE = -1 + + + +def binary_var(name): + return pulp.LpVariable(name, 0, 1, pulp.LpInteger) + + +def constant(x): + def _constant(*args, **kwargs): + return x + + return _constant + + +class VariableGroup(object): + def __init__(self, name, indexes=(), is_binary_var=False, lower_bound_func=None, upper_bound_func=None, + base_name=None): + self.variables = {} + + name_base = name + for _ in range(len(indexes)): + name_base += "_{}" + + for index in itertools.product(*indexes): + var_name = name_base.format(*index) + + if is_binary_var: + var = binary_var(var_name) + else: + + # find upper and lower bounds for the variable, if available + if lower_bound_func is not None: + lower_bound = lower_bound_func(index) + else: + lower_bound = None + + if upper_bound_func is not None: + upper_bound = upper_bound_func(index) + else: + upper_bound = None + + # # the lower bound should always be set if the upper bound is set + # if lower_bound is None and upper_bound is not None: + # raise RuntimeError("Lower bound should not be unset while upper bound is set") + + # create the lp variable + if lower_bound is not None and upper_bound is not None: + #print("name: {}, lower_bound: {}, upper_bound: {}".format(var_name, lower_bound, upper_bound)) + var = pulp.LpVariable(var_name, lowBound=lower_bound, upBound=upper_bound) + elif lower_bound is not None and upper_bound is None: + #print("name: {}, lower_bound: {}, ".format(var_name, lower_bound)) + var = pulp.LpVariable(var_name, lowBound=lower_bound) + elif lower_bound is None and upper_bound is not None: + #print("name: {}, upper_bound: {}, ".format(var_name, upper_bound)) + var = pulp.LpVariable(var_name, lowBound=None, upBound=upper_bound) + else: + #print("name: {} ".format(var_name)) + var = pulp.LpVariable(var_name) + + # if upper_bound is not None: + # print("name: {}, lower_bound: {}, upper_bound: {}".format(var_name, lower_bound, upper_bound)) + # var = pulp.LpVariable(var_name, lower_bound, upper_bound) + # elif lower_bound is not None: + # print("name: {}, lower_bound: {}, ".format(var_name, lower_bound)) + # var = pulp.LpVariable(var_name, lower_bound) + # else: + # print("name: {} ".format(var_name)) + # var = pulp.LpVariable(var_name) + + self.variables[index] = var + + def match(self, key): + def predicate(xs, ys): + z = 0 + for x, y in zip(xs, ys): + if x - y == 0: + z += 1 + return z == len(key) - 1 + + position = key.index(RANGE) + keys = list(self.variables.keys()) + keys = [k for k in keys if predicate(k, key)] + keys.sort(key=lambda k: k[position]) + + return [self.variables[k] for k in keys] + + def __getitem__(self, key): + if type(key) != tuple: + key = (key,) + + n_ones = 0 + for i, x in enumerate(key): + if x == RANGE: + n_ones += 1 + + if n_ones == 0: + return self.variables[key] + elif n_ones == 1: + return self.match(key) + else: + raise ValueError("Can only get RANGE for one index.") diff --git a/pnnl/models/vav.py b/pnnl/models/vav.py index 1d1eb49..29f1914 100644 --- a/pnnl/models/vav.py +++ b/pnnl/models/vav.py @@ -1,13 +1,24 @@ import logging -import importlib from volttron.platform.agent import utils from volttron.pnnl.models.utils import clamp +import volttron.pnnl.models.input_names as data_names _log = logging.getLogger(__name__) utils.setup_logging() +# +# class VAV(object): +# def __init__(self, config, **kwargs): +# model_type = config.get("model_type", "firstorderzone") +# module = importlib.import_module("volttron.pnnl.models.vav") +# model_class = getattr(module, model_type) +# self.model = model_class(config, self) +# +# def get_q(self, _set, sched_index, market_index, occupied): +# q = self.model.predict(_set, sched_index, market_index, occupied) +# return q -class VAV(object): +class firstorderzone(object): OAT = "oat" SFS = "sfs" ZT = "zt" @@ -15,41 +26,26 @@ class VAV(object): ZAF = "zaf" CSP = "csp" HSP = "hsp" - - def __init__(self, config, **kwargs): - model_type = config.get("model_type", "firstorderzone") - module = importlib.import_module("volttron.pnnl.models.vav") - model_class = getattr(module, model_type) - self.model = model_class(config, self) - - def get_q(self, _set, sched_index, market_index, occupied): - q = self.model.predict(_set, sched_index, market_index, occupied) - return q - - -class firstorderzone(object): def __init__(self, config, parent, **kwargs): self.parent = parent self.a1 = config.get("a1", 0) self.a2 = config.get("a2", 0) self.a3 = config.get("a3", 0) self.a4 = config.get("a4", 0) + self.coefficients = {"a1", "a2", "a3", "a4"} type = config.get("terminal_box_type", "VAV") self.get_input_value = parent.get_input_value - self.smc_interval = parent.single_market_contol_interval # parent mapping # data inputs - self.oat_name = parent.OAT - self.sfs_name = parent.SFS - self.zt_name = parent.ZT - self.zdat_name = parent.ZDAT - self.zaf_name = parent.ZAF - + self.oat_name = data_names.OAT + self.sfs_name = data_names.SFS + self.zt_name = data_names.ZT + self.zdat_name = data_names.ZDAT + self.zaf_name = data_names.ZAF + print("MODEL: {}".format(self.a1)) self.oat = self.get_input_value(self.oat_name) self.sfs = self.get_input_value(self.sfs_name) self.zt = self.get_input_value(self.zt_name) - # self.zdat = self.get_input_value(self.zdat_name) - # self.zaf = self.get_input_value(self.zaf_name) self.zt_predictions = [self.zt]*parent.market_number if type.lower() == "vav": @@ -59,29 +55,53 @@ def __init__(self, config, parent, **kwargs): self.parent.commodity = "DischargeAirTemperature" self.predict_quantity = self.getT + def update_coefficients(self, coefficients): + if set(coefficients.keys()) != self.coefficients: + _log.warning("Missing required coefficient to update model") + _log.warning("Provided coefficients %s -- required %s", + list(coefficients.keys()), self.coefficients) + return + self.a1 = coefficients["a1"] + self.a2 = coefficients["a2"] + self.a3 = coefficients["a3"] + self.a4 = coefficients["a4"] + message = { + "a1": self.a1, + "a2": self.a2, + "a3": self.a3, + "a4": self.a4 + } + topic_suffix = "MODEL_COEFFICIENTS" + self.parent.publish_record(topic_suffix, message) + def update_data(self): - self.oat = self.get_input_value(self.oat_name) - self.sfs = self.get_input_value(self.sfs_name) - self.zt = self.get_input_value(self.zt_name) - # self.zdat = self.get_input_value(self.zdat_name) - # self.zaf = self.get_input_value(self.zaf_name) - _log.debug( - "Update model data: oat: {} - zt: {} - sfs: {}".format(self.oat, self.zt, self.sfs)) + pass def update(self, _set, sched_index, market_index): self.zt_predictions[market_index] = _set def predict(self, _set, sched_index, market_index, occupied): - if self.smc_interval is not None or market_index == -1: - oat = self.oat - zt = self.zt - occupied = self.sfs if self.sfs is not None else occupied + if self.parent.market_number == 1: + oat = self.get_input_value(self.oat_name) + sfs = self.get_input_value(self.sfs_name) + zt = self.get_input_value(self.zt_name) + occupied = sfs if sfs is not None else occupied sched_index = self.parent.current_datetime.hour else: zt = self.zt_predictions[market_index] - oat = self.parent.oat_predictions[market_index] if self.parent.oat_predictions else self.oat + if zt is None: + zt = self.get_input_value(self.zt_name) + + if self.parent.oat_predictions: + oat = self.parent.oat_predictions[market_index] + else: + oat = self.get_input_value(self.oat_name) + q = self.predict_quantity(oat, zt, _set, sched_index) - _log.debug("{}: VAV_MODEL predicted {} - zt: {} - set: {} - sched: {}".format(self.parent.agent_name, q, zt, _set, sched_index)) + _log.debug( + "%s: vav.firstorderzone q: %s - zt: %s- set: %s - sched: %s", + self.parent.agent_name, q, zt, _set, sched_index + ) # might need to revisit this when doing both heating and cooling if occupied: q = clamp(q, min(self.parent.flexibility), max(self.parent.flexibility)) diff --git a/pnnl/transactive_base/transactive/aggregator_base.py b/pnnl/transactive_base/transactive/aggregator_base.py index b9a97ee..89f7eae 100644 --- a/pnnl/transactive_base/transactive/aggregator_base.py +++ b/pnnl/transactive_base/transactive/aggregator_base.py @@ -33,6 +33,7 @@ def __init__(self, config, **kwargs): for market_name in self.consumer_market: self.consumer_market[market_name] = ['_'.join([market_name, str(i)]) for i in range(self.market_number)] self.consumer_demand_curve[market_name] = [None]*self.market_number + self.consumer_reserve_demand_curve[market_name] = [None]*self.market_number self.supplier_curve = [] def init_markets(self): @@ -63,6 +64,8 @@ def aggregate_callback(self, timestamp, market_name, buyer_seller, agg_demand): "Curve": self.consumer_demand_curve[market_base][market_index].tuppleize(), "Commodity": market_base } + if self.consumer_reserve_demand_curve[market_base][market_index]: + message['Reserve_Curve'] = self.consumer_reserve_demand_curve[market_base][market_index].tuppleize() _log.debug("{} debug demand_curve - curve: {}".format(self.agent_name, self.consumer_demand_curve[market_base][market_index].points)) self.publish_record(topic_suffix, message) diff --git a/pnnl/transactive_base/transactive/device_control_base.py b/pnnl/transactive_base/transactive/device_control_base.py new file mode 100644 index 0000000..4b9a774 --- /dev/null +++ b/pnnl/transactive_base/transactive/device_control_base.py @@ -0,0 +1,292 @@ +import logging +import gevent +import dateutil.tz +import numpy as np +from volttron.platform.agent.utils import setup_logging +from dateutil.parser import parse +from volttron.platform.jsonrpc import RemoteError +from volttron.platform.vip.agent import errors +from volttron.platform.agent.math_utils import mean, stdev +from volttron.platform.messaging import topics, headers as headers_mod +from volttron.platform.agent.utils import setup_logging, format_timestamp, get_aware_utc_now + +_log = logging.getLogger(__name__) +setup_logging() +__version__ = '0.1' + +class DeviceControlBase(object): + + def __init__(self, config, vip_object, topic_prefix): + self.current_datetime = None + self.current_schedule = None + self.actuation_enabled = False + self.actuation_disabled = False + self.inputs = {} + self.outputs = {} + self.schedule = {} + self.occupied = False + self.actuation_obj = None + self.current_datetime = None + self.input_topics = [] + self.vip_obj = vip_object + self.topic_prefix = topic_prefix + input_data_tz = config.get("input_data_timezone", "UTC") + self.input_data_tz = dateutil.tz.gettz(input_data_tz) + self.agent_name = '' + + campus = config.get("campus", "") + building = config.get("building", "") + + # set actuation parameters for device control + actuate_topic = config.get("actuation_enable_topic", "default") + if actuate_topic == "default": + self.actuate_topic = '/'.join([campus, building, 'actuate']) + else: + self.actuate_topic = actuate_topic + self.actuate_onstart = config.get("actuation_enabled_onstart", False) + self.actuation_method = config.get("actuation_method", "periodic") + self.actuation_rate = config.get("control_interval", 300) + inputs = config.get("inputs", []) + self._outputs = config.get("outputs", []) + self.init_inputs(inputs) + schedule = config.get("schedule", {}) + self.init_schedule(schedule) + + def input_subscriptions(self): + for topic in self.input_topics: + _log.debug('Subscribing to: ' + topic) + self.vip_obj.pubsub.subscribe(peer='pubsub', + prefix=topic, + callback=self.update_input_data) + + def init_actuation_state(self, actuate_topic, actuate_onstart): + if self._outputs: + self.vip_obj.pubsub.subscribe(peer='pubsub', + prefix=actuate_topic, + callback=self.update_actuation_state) + if actuate_onstart: + self.update_actuation_state(None, None, None, None, None, True) + self.actuation_disabled = False + else: + _log.info("{} - cannot initialize actuation state, no configured outputs.".format(self.agent_name)) + + def init_schedule(self, schedule): + if schedule: + for day_str, schedule_info in schedule.items(): + _day = parse(day_str).weekday() + if schedule_info not in ["always_on", "always_off"]: + start = parse(schedule_info["start"]).time() + end = parse(schedule_info["end"]).time() + self.schedule[_day] = {"start": start, "end": end} + else: + self.schedule[_day] = schedule_info + + def check_schedule(self, dt): + current_schedule = self.schedule[dt.weekday()] + if "always_on" in current_schedule: + self.occupied = True + if not self.actuation_enabled: + self.update_actuation_state(None, None, None, None, None, True) + return + if "always_off" in current_schedule: + self.occupied = False + if self.actuation_enabled: + self.update_actuation_state(None, None, None, None, None, False) + return + _start = current_schedule["start"] + _end = current_schedule["end"] + if _start < self.current_datetime.time() < _end: + self.occupied = True + if not self.actuation_enabled: + self.update_actuation_state(None, None, None, None, None, True) + else: + self.occupied = False + if self.actuation_enabled: + self.update_actuation_state(None, None, None, None, None, False) + + def check_future_schedule(self, dt): + current_schedule = self.schedule[dt.weekday()] + if "always_on" in current_schedule: + return True + if "always_off" in current_schedule: + return False + _start = current_schedule["start"] + _end = current_schedule["end"] + if _start < dt.time() < _end: + return True + else: + return False + + def update_actuation_state(self, peer, sender, bus, topic, headers, message): + state = message + if self.actuation_disabled: + if sender is None: + _log.debug("{} is disabled not change in actuation state".format(self.agent_name)) + return + elif bool(state): + _log.debug("{} is re-enabled for actuation.".format(self.agent_name)) + self.actuation_disabled = False + if not self.actuation_disabled: + if sender is not None and not bool(state): + _log.debug("{} is disabled for actuation.".format(self.agent_name)) + self.actuation_disabled = True + + _log.debug("update actuation {}".format(state)) + if self.actuation_enabled and not bool(state): + for output_info in self.outputs.values(): + topic = output_info["topic"] + release = output_info["release"] + actuator = output_info["actuator"] + if self.actuation_obj is not None: + self.actuation_obj.kill() + self.actuation_obj = None + self.actuate(topic, release, actuator) + elif not self.actuation_enabled and bool(state): + for name, output_info in self.outputs.items(): + offset = output_info.get("offset", 0.0) + actuator = output_info.get("actuator", "platform.actuator") + topic = output_info["topic"] + release = output_info.get("release", None) + if isinstance(release, str) and release.lower() == "default": + try: + release_value = self.vip_obj.rpc.call(actuator, + 'get_point', + topic).get(timeout=10) + except (RemoteError, gevent.Timeout, errors.VIPError) as ex: + _log.warning("Failed to get {} - ex: {}".format(topic, str(ex))) + else: + release_value = None + self.outputs[name]["release"] = release_value + + self.actuation_enabled = state + + def update_input_data(self, peer, sender, bus, topic, headers, message): + data = message[0] + current_datetime = parse(headers.get("Date")) + self.current_datetime = current_datetime.astimezone(self.input_data_tz) + self.update_data(data) + if current_datetime is not None and self.schedule: + self.check_schedule(current_datetime) + + def update_data(self, data): + to_publish = {} + for name, input_data in self.inputs.items(): + for point, value in input_data.items(): + if point in data: + self.inputs[name][point] = data[point] + to_publish[point] = data[point] + topic_suffix = "/".join([self.agent_name, "InputData"]) + message = to_publish + self.publish_record(topic_suffix, message) + self.model.update_data() + + def actuate(self, point_topic, value, actuator): + try: + self.vip_obj.rpc.call(actuator, + 'set_point', + "", + point_topic, + value).get(timeout=10) + except (RemoteError, gevent.Timeout, errors.VIPError) as ex: + _log.warning("Failed to set {} - ex: {}".format(point_topic, str(ex))) + + def actuate(self, output_dict): + point_topic = output_dict["topic"] + point = output_dict["point"] + actuator = output_dict["actuator"] + value = output_dict.get("value") + offset = output_dict["offset"] + if value is not None: + value = value + offset + try: + self.vip_obj.rpc.call(actuator, + 'set_point', + "", + point_topic, + value).get(timeout=10) + except (RemoteError, gevent.Timeout, errors.VIPError) as ex: + _log.warning("Failed to set {} - ex: {}".format(point_topic, str(ex))) + + def determine_sched_index(self, index): + if self.current_datetime is None: + return index + elif index + self.current_datetime.hour + 1 < 24: + return self.current_datetime.hour + index + 1 + else: + return self.current_datetime.hour + index + 1 - 24 + + def get_input_value(self, mapped): + try: + return self.inputs[mapped].values()[0] + except KeyError: + return None + + def init_inputs(self, inputs): + for input_info in inputs: + point = input_info.pop("point") + mapped = input_info.pop("mapped") + topic = input_info.pop("topic") + value = input_info.pop("inital_value") + self.inputs[mapped] = {point: value} + if topic not in self.input_topics: + self.input_topics.append(topic) + + def init_outputs(self, outputs): + for output_info in outputs: + # Topic to subscribe to for data (currently data format must be + # consistent with a MasterDriverAgent all publish) + topic = output_info["topic"] + # Point name from as published by MasterDriverAgent + point = output_info.pop("point") + mapped = output_info.pop("mapped") + # Options for release are None or default + # None assumes BACnet release via priority array + # default will safe original value, at start of agent run for control restoration + # TODO: Update release value anytime agent has state tranistion for actuation_enable + release = output_info.get("release", None) + # Constant offset to apply to apply to determined actuation value + offset = output_info.get("offset", 0.0) + # VIP identity of Actuator to call via RPC to perform control of device + actuator = output_info.get("actuator", "platform.actuator") + # This is the flexibility range for the market commodity the + # transactive agent will utilize + flex = output_info["flexibility_range"] + # This is the flexibility of the control point, by default the same as the + # market commodity but not necessarily + ct_flex = output_info.get("control_flexibility", flex) + ct_flex = np.linspace(ct_flex[0], ct_flex[1], 11) + flex = np.linspace(flex[0], flex[1], 11) + fallback = output_info.get("fallback", mean(ct_flex)) + # TODO: Use condition to determine multiple output scenario + condition = output_info.get("condition", True) + + try: + value = self.vip_obj.rpc.call(actuator, + 'get_point', + topic).get(timeout=10) + except (RemoteError, gevent.Timeout, errors.VIPError) as ex: + _log.warning("Failed to get {} - ex: {}".format(topic, str(ex))) + value = fallback + if isinstance(release, str) and release.lower() == "default" and value is not None: + release_value = value + else: + release_value = None + off_setpoint = output_info.get("off_setpoint", value) + self.outputs[mapped] = { + "point": point, + "topic": topic, + "actuator": actuator, + "release": release_value, + "value": value, + "off_setpoint": off_setpoint, + "offset": offset, + "flex": flex, + "ct_flex": ct_flex, + "condition": condition + } + + def publish_record(self, topic_suffix, message): + headers = {headers_mod.DATE: format_timestamp(get_aware_utc_now())} + message["TimeStamp"] = format_timestamp(self.current_datetime) + topic = "/".join([self.topic_prefix, topic_suffix]) + self.vip_obj.pubsub.publish("pubsub", topic, headers, message).get() diff --git a/pnnl/transactive_base/transactive/transactive.py b/pnnl/transactive_base/transactive/transactive.py index 28f341b..4d34398 100644 --- a/pnnl/transactive_base/transactive/transactive.py +++ b/pnnl/transactive_base/transactive/transactive.py @@ -39,6 +39,7 @@ def __init__(self, config, **kwargs): self.mapped = None self.oat_predictions = [] self.market_prices = [] + self.reserve_market_prices = [] self.input_topics = [] self.inputs = {} self.outputs = {} @@ -87,6 +88,10 @@ def __init__(self, config, **kwargs): self.update_flag = [] self.demand_curve = [] self.prices = [] + self.max_iterations = 5 + self.iteration_counter = 0 + self.market_type = config.get("market_type", "tns") + self.market_converged = False @Core.receiver('onstart') def setup(self, sender, **kwargs): @@ -139,13 +144,16 @@ def init_outputs(self, outputs): actuator = output_info.get("actuator", "platform.actuator") # This is the flexibility range for the market commodity the # transactive agent will utilize - flex = output_info["flexibility_range"] - # This is the flexibility of the control point, by default the same as the - # market commodity but not necessarily - ct_flex = output_info.get("control_flexibility", flex) - ct_flex = np.linspace(ct_flex[0], ct_flex[1], 11) - flex = np.linspace(flex[0], flex[1], 11) - fallback = output_info.get("fallback", mean(ct_flex)) + ct_flex = None + flex = output_info.get("flexibility_range", None) + fallback = 0 + if flex is not None: + # This is the flexibility of the control point, by default the same as the + # market commodity but not necessarily + ct_flex = output_info.get("control_flexibility", flex) + ct_flex = np.linspace(ct_flex[0], ct_flex[1], 11) + flex = np.linspace(flex[0], flex[1], 11) + fallback = output_info.get("fallback", mean(ct_flex)) # TODO: Use condition to determine multiple output scenario condition = output_info.get("condition", True) @@ -284,8 +292,16 @@ def update_actuation_state(self, peer, sender, bus, topic, headers, message): self.actuation_enabled = state def update_outputs(self, name, price): + _log.debug("Current time {}".format(self.current_datetime)) + if self.market_converged: + if self.current_datetime is not None: + _hour = self.current_datetime.hour + self.current_price = self.market_prices[_hour] + _log.debug("update_outputs: market converged, current price is: {}".format(self.current_price)) + # If input data stops, current datetime will not change and current price remains same if price is None: if self.current_price is None: + _log.debug("Return coz self.current_price is None") return price = self.current_price sets = self.outputs[name]["ct_flex"] @@ -348,6 +364,10 @@ def offer_callback(self, timestamp, market_name, buyer_seller): demand_curve = self.create_demand_curve(market_index, sched_index, occupied) self.demand_curve[market_index] = demand_curve result, message = self.make_offer(market_name, buyer_seller, demand_curve) + _log.debug("MARKET prices: {}".format(self.market_prices)) + if self.market_type == 'city': + price = self.market_prices[market_index] + self.update_state(market_index, sched_index, price) def create_demand_curve(self, market_index, sched_index, occupied): _log.debug("{} debug demand_curve - index: {} - sched: {}".format(self.agent_name, @@ -361,8 +381,8 @@ def create_demand_curve(self, market_index, sched_index, occupied): _set = self.ct_flexibility[i] else: _set = self.off_setpoint - ct_flx.append(_set) q = self.get_q(_set, sched_index, market_index, occupied) + _log.debug("Prices: {}, Quantity: {}".format(prices, q)) demand_curve.add(Point(price=prices[i], quantity=q)) ct_flx = [min(ct_flx), max(ct_flx)] if ct_flx else [] @@ -385,6 +405,7 @@ def price_callback(self, timestamp, market_name, buyer_seller, price, quantity): else: _log.warn("{} - market {} did not clear, and no market_prices, using default fallback price!".format(self.agent_name, market_name)) price = self.default_price + cleared_quantity = None if self.demand_curve[market_index] is not None and self.demand_curve[market_index].points: cleared_quantity = self.demand_curve[market_index].x(price) @@ -392,11 +413,11 @@ def price_callback(self, timestamp, market_name, buyer_seller, price, quantity): _log.debug("{} price callback market: {}, price: {}, quantity: {}".format(self.agent_name, market_name, price, quantity)) topic_suffix = "/".join([self.agent_name, "MarketClear"]) message = {"MarketIndex": market_index, "Price": price, "Quantity": [quantity, cleared_quantity], "Commodity": self.commodity} - self.publish_record(topic_suffix, message) + #self.publish_record(topic_suffix, message) # this line of logic was different in VAV agent # if price is not None: #if price is not None and market_index < self.market_number-1: - if price is not None: + if price is not None and self.market_type != "city": self.update_state(market_index, sched_index, price) if price is not None and self.actuation_method == "market_clear" and market_index == 0: self.do_actuation(price) @@ -408,21 +429,42 @@ def error_callback(self, timestamp, market_name, buyer_seller, error_code, error aux)) def update_prices(self, peer, sender, bus, topic, headers, message): - _log.debug("Get prices prior to market start.") + _log.debug("Get prices prior to market start. {}".format(message)) current_hour = message['hour'] + #new_day_flag = message.get('new_day', True) + # if new_day_flag: + # self.iteration_counter = 0 #reset iteration counter + # else: + # self.iteration_counter += 1 + converged = message.get('converged', False) + start_of_cycle = message.get('start_of_cycle', False) + _log.debug("update_prices: {}, {}".format(converged, start_of_cycle)) + if start_of_cycle: + self.market_converged = False + + if converged: + self.market_converged = True + # MARKET CONVERGED!! + _log.debug("MARKET CONVERGED: {}".format(self.agent_name)) # Store received prices so we can use it later when doing clearing process - if self.market_prices: - if current_hour != self.current_hour: - self.current_price = self.market_prices[0] - self.current_hour = current_hour + # if self.market_prices: + # _log.debug("current prices not none {}, {}".format(current_hour, self.current_hour)) + # if current_hour != self.current_hour: + # _log.debug("current hour != current hour") + # self.current_price = self.market_prices[0] + + #self.current_hour = current_hour self.oat_predictions = [] oat_predictions = message.get("temp", []) self.oat_predictions = oat_predictions self.market_prices = message['prices'] # Array of prices + _log.debug("Received new market prices: {}".format(self.market_prices)) + self.reserve_market_prices = message.get('reserved_prices', []) # Array of reserve prices def determine_control(self, sets, prices, price): + _log.debug("transactive: determine_control") control = np.interp(price, prices, sets) control = self.clamp(control, min(self.ct_flexibility), max(self.ct_flexibility)) return control @@ -436,14 +478,19 @@ def determine_prices(self): if self.market_prices: avg_price = mean(self.market_prices) std_price = stdev(self.market_prices) - price_min = avg_price - self.price_multiplier * std_price - price_max = avg_price + self.price_multiplier * std_price + if std_price != 0: + price_min = avg_price - self.price_multiplier * std_price + price_max = avg_price + self.price_multiplier * std_price + else: + price_min = avg_price*0.8 + price_max = avg_price*1.2 else: avg_price = None std_price = None price_min = self.default_min_price price_max = self.default_max_price _log.debug("Prices: {} - avg: {} - std: {}".format(self.market_prices, avg_price, std_price)) + price_array = np.linspace(price_min, price_max, 11) return price_array @@ -465,7 +512,8 @@ def update_data(self, data): topic_suffix = "/".join([self.agent_name, "InputData"]) message = to_publish self.publish_record(topic_suffix, message) - self.model.update_data() + if self.model is not None: + self.model.update_data() def determine_sched_index(self, index): if self.current_datetime is None: @@ -491,4 +539,4 @@ def publish_record(self, topic_suffix, message): headers = {headers_mod.DATE: format_timestamp(get_aware_utc_now())} message["TimeStamp"] = format_timestamp(self.current_datetime) topic = "/".join([self.record_topic, topic_suffix]) - self.vip.pubsub.publish("pubsub", topic, headers, message).get() + self.vip.pubsub.publish("pubsub", topic, headers, message).get() \ No newline at end of file diff --git a/price_test.py b/price_test.py new file mode 100644 index 0000000..e69de29