def init_objects(self): # Add meter # meter = MeterPoint() # meter.name = 'BuildingElectricMeter' # meter.measurementType = MeasurementType.PowerReal # meter.measurementUnit = MeasurementUnit.kWh # self.meterPoints.append(meter) # Add weather forecast service weather_service = TemperatureForecastModel(self.config_path, self) self.informationServiceModels.append(weather_service) # # Add inelastive asset # inelastive_load = LocalAsset() # inelastive_load.name = 'InelasticBldgLoad' # inelastive_load.maximumPower = 0 # Remember that a load is a negative power [kW] # inelastive_load.minimumPower = -200 # # # Add inelastive asset model # inelastive_load_model = LocalAssetModel() # inelastive_load_model.name = 'InelasticBuildingModel' # inelastive_load_model.defaultPower = -100 # [kW] # inelastive_load_model.defaultVertices = [Vertex(float("inf"), 0, -100, True)] # # # Cross-reference asset & asset model # inelastive_load_model.object = inelastive_load # inelastive_load.model = inelastive_load_model # Add elastive asset elastive_load = LocalAsset() elastive_load.name = 'TccLoad' elastive_load.maximumPower = 0 # Remember that a load is a negative power [kW] elastive_load.minimumPower = -self.max_deliver_capacity # Add inelastive asset model # self.elastive_load_model = LocalAssetModel() self.elastive_load_model = TccModel() self.elastive_load_model.name = 'TccModel' self.elastive_load_model.defaultPower = -0.5*self.max_deliver_capacity # [kW] self.elastive_load_model.defaultVertices = [Vertex(0.055, 0, -self.elastive_load_model.defaultPower, True), Vertex(0.06, 0, -self.elastive_load_model.defaultPower/2, True)] # Cross-reference asset & asset model self.elastive_load_model.object = elastive_load elastive_load.model = self.elastive_load_model # Add inelastive and elastive loads as building' assets self.localAssets.extend([elastive_load]) # Add Market market = Market() market.name = 'dayAhead' market.commitment = False market.converged = False market.defaultPrice = 0.0428 # [$/kWh] market.dualityGapThreshold = self.duality_gap_threshold # [0.02 = 2#] market.initialMarketState = MarketState.Inactive market.marketOrder = 1 # This is first and only market market.intervalsToClear = 1 # Only one interval at a time market.futureHorizon = timedelta(hours=24) # Projects 24 hourly future intervals market.intervalDuration = timedelta(hours=1) # [h] Intervals are 1 h long market.marketClearingInterval = timedelta(hours=1) # [h] market.marketClearingTime = Timer.get_cur_time().replace(hour=0, minute=0, second=0, microsecond=0) # Aligns with top of hour market.nextMarketClearingTime = market.marketClearingTime + timedelta(hours=1) self.markets.append(market) # Campus object campus = Neighbor() campus.name = 'PNNL_Campus' campus.description = 'PNNL_Campus' campus.maximumPower = self.max_deliver_capacity campus.minimumPower = 0. # [avg.kW] campus.lossFactor = self.campus_loss_factor # Campus model campus_model = NeighborModel() campus_model.name = 'PNNL_Campus_Model' campus_model.location = self.name campus_model.defaultVertices = [Vertex(0.045, 25, 0, True), Vertex(0.048, 0, self.max_deliver_capacity, True)] campus_model.demand_threshold_coef = self.demand_threshold_coef # campus_model.demandThreshold = self.demand_threshold_coef * self.monthly_peak_power campus_model.demandThreshold = self.monthly_peak_power campus_model.transactive = True campus_model.inject(self, system_loss_topic=self.system_loss_topic) # Avg building meter building_meter = MeterPoint() building_meter.name = self.name + ' ElectricMeter' building_meter.measurementType = MeasurementType.AverageDemandkW building_meter.measurementUnit = MeasurementUnit.kWh campus_model.meterPoints.append(building_meter) # Cross-reference object & model campus_model.object = campus campus.model = campus_model self.campus = campus # Add campus as building's neighbor self.neighbors.append(campus)
class BuildingAgent(MarketAgent, myTransactiveNode): def __init__(self, config_path, **kwargs): MarketAgent.__init__(self, **kwargs) myTransactiveNode.__init__(self) self.config_path = config_path self.config = utils.load_config(config_path) self.name = self.config.get('name') self.agent_name = self.config.get('agentid', 'building_agent') self.db_topic = self.config.get("db_topic", "tnc") self.power_topic = self.config.get("power_topic") self.market_cycle_in_min = int(self.config.get('market_cycle_in_min', 60)) self.duality_gap_threshold = float(self.config.get('duality_gap_threshold', 0.01)) self.campus_loss_factor = float(self.config.get('campus_loss_factor', 0.01)) self.neighbors = [] self.max_deliver_capacity = float(self.config.get('max_deliver_capacity')) self.demand_threshold_coef = float(self.config.get('demand_threshold_coef')) self.monthly_peak_power = float(self.config.get('monthly_peak_power')) self.building_demand_topic = "{}/{}/campus/demand".format(self.db_topic, self.name) self.campus_supply_topic = "{}/campus/{}/supply".format(self.db_topic, self.name) self.system_loss_topic = "{}/{}/system_loss".format(self.db_topic, self.name) self.mix_market_running = False verbose_logging = self.config.get('verbose_logging', True) self.prices = [None for i in range(25)] self.quantities = [None for i in range(25)] self.building_demand_curves = [None for i in range(25)] self.mix_market_duration = timedelta(minutes=20) self.reschedule_interval = timedelta(minutes=10, seconds=1) 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 # Create market names to join self.base_market_name = 'electric' # Need to agree on this with other market agents self.market_names = [] for i in range(24): self.market_names.append('_'.join([self.base_market_name, str(i)])) 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 if self.simulation: self.ep_lines = [] self.cur_ep_line = 0 # with open(ep_res_path, 'r') as fh: # for line in fh: # self.ep_lines.append(line) _log2.debug("Mixmarket for agent {}:".format(self.name)) @Core.receiver('onstart') def onstart(self, sender, **kwargs): # Add other objects: assets, services, neighbors self.init_objects() # Join electric mix-markets for market in self.market_names: self.join_market(market, SELLER, self.reservation_callback, self.offer_callback, self.aggregate_callback, self.price_callback, self.error_callback) # Subscriptions self.vip.pubsub.subscribe(peer='pubsub', prefix=self.campus_supply_topic, callback=self.new_supply_signal) self.vip.pubsub.subscribe(peer='pubsub', prefix=self.power_topic, callback=self.new_demand_signal) def new_supply_signal(self, peer, sender, bus, topic, headers, message): _log.debug("At {}, {} receives new supply records: {}".format(Timer.get_cur_time(), self.name, message)) supply_curves = message['curves'] start_of_cycle = message['start_of_cycle'] self.campus.model.receive_transactive_signal(self, supply_curves) _log.debug("At {}, mixmarket state is {}, start_of_cycle {}".format(Timer.get_cur_time(), self.mix_market_running, start_of_cycle)) db_topic = "/".join([self.db_topic, self.name, "CampusSupply"]) message = supply_curves headers = {headers_mod.DATE: format_timestamp(Timer.get_cur_time())} self.vip.pubsub.publish("pubsub", db_topic, headers, message).get() if start_of_cycle: _log.debug("At {}, start of cycle. " "Mixmarket state before overriding is {}".format(Timer.get_cur_time(), self.mix_market_running)) # if self.simulation: # self.run_ep_sim(start_of_cycle) # else: self.start_mixmarket(start_of_cycle) def new_demand_signal(self, peer, sender, bus, topic, headers, message): mtrs = self.campus.model.meterPoints if len(mtrs) > 0: bldg_meter = mtrs[0] power_unit = message[1] cur_power = float(message[0]["WholeBuildingPower"]) power_unit = power_unit.get("WholeBuildingPower", {}).get("units", "kW") if isinstance(power_unit, str) and power_unit.lower() == "watts": cur_power = cur_power / 1000.0 bldg_meter.set_meter_value(cur_power) if Timer.get_cur_time().minute >= 58: bldg_meter.update_avg() def near_end_of_hour(self, now): near_end_of_hour = False if (now + self.mix_market_duration).hour != now.hour: near_end_of_hour = True _log.debug("{} did not start mixmarket because it's too late.".format(self.name)) return near_end_of_hour def start_mixmarket(self, start_of_cycle): # Reset price array self.prices = [None for i in range(25)] # Save the 1st quantity as prior 2nd quantity cur_quantity = self.quantities[1] cur_curve = self.building_demand_curves[1] # Reset quantities and curves self.quantities = [None for i in range(25)] self.building_demand_curves = [None for i in range(25)] # If new cycle, set the 1st quantity to the corresponding quantity of previous hour if start_of_cycle: self.quantities[0] = cur_quantity self.building_demand_curves[0] = cur_curve # Balance market with previous known demands market = self.markets[0] # Assume only 1 TNS market per node market.signal_new_data = True market.balance(self) # Check if now is near the end of the hour, applicable only if not in simulation mode now = Timer.get_cur_time() near_end_of_hour = False if not self.simulation: near_end_of_hour = self.near_end_of_hour(now) if market.converged: # Get new prices (expected 25 values: current hour + next 24) prices = market.marginalPrices # There is a case where the balancing happens at the end of the hour and continues to the next hour, which # creates 26 values. Get the last 25 values. prices = prices[-25:] self.prices = [p.value for p in prices] # Signal to start mix market only if the previous market is done if not self.mix_market_running and not near_end_of_hour: self.mix_market_running = True # Update weather information weather_service = None if len(self.informationServiceModels)>0: weather_service = self.informationServiceModels[0] weather_service.update_information(market) _log.debug("prices are {}".format(self.prices)) if weather_service is None: self.vip.pubsub.publish(peer='pubsub', topic='mixmarket/start_new_cycle', message={"prices": self.prices[-24:], "hour": now.hour}) else: temps = [x.value for x in weather_service.predictedValues] temps = temps[-24:] _log.debug("temps are {}".format(temps)) self.vip.pubsub.publish(peer='pubsub', topic='mixmarket/start_new_cycle', message={"prices": self.prices[-24:], "temp": temps, "hour": now.hour}) def balance_market(self, run_cnt): market = self.markets[0] # Assume only 1 TNS market per node market.new_data_signal = True market.balance(self) if market.converged: _log.debug("TNS market {} balanced successfully.".format(market.name)) balanced_curves = [(x.timeInterval.startTime, x.value.marginalPrice, x.value.power) for x in market.activeVertices] _log.debug("Balanced curves: {}".format(balanced_curves)) # Sum all the powers as will be needed by the net supply/demand curve. market.assign_system_vertices(self) # Check to see if it is ok to send signals self.campus.model.check_for_convergence(market) if not self.campus.model.converged: _log.debug("Campus model not converged. Sending signal back to campus.") self.campus.model.prep_transactive_signal(market, self) self.campus.model.send_transactive_signal(self, self.building_demand_topic) 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+1] #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 init_objects(self): # Add meter # meter = MeterPoint() # meter.name = 'BuildingElectricMeter' # meter.measurementType = MeasurementType.PowerReal # meter.measurementUnit = MeasurementUnit.kWh # self.meterPoints.append(meter) # Add weather forecast service weather_service = TemperatureForecastModel(self.config_path, self) self.informationServiceModels.append(weather_service) # # Add inelastive asset # inelastive_load = LocalAsset() # inelastive_load.name = 'InelasticBldgLoad' # inelastive_load.maximumPower = 0 # Remember that a load is a negative power [kW] # inelastive_load.minimumPower = -200 # # # Add inelastive asset model # inelastive_load_model = LocalAssetModel() # inelastive_load_model.name = 'InelasticBuildingModel' # inelastive_load_model.defaultPower = -100 # [kW] # inelastive_load_model.defaultVertices = [Vertex(float("inf"), 0, -100, True)] # # # Cross-reference asset & asset model # inelastive_load_model.object = inelastive_load # inelastive_load.model = inelastive_load_model # Add elastive asset elastive_load = LocalAsset() elastive_load.name = 'TccLoad' elastive_load.maximumPower = 0 # Remember that a load is a negative power [kW] elastive_load.minimumPower = -self.max_deliver_capacity # Add inelastive asset model # self.elastive_load_model = LocalAssetModel() self.elastive_load_model = TccModel() self.elastive_load_model.name = 'TccModel' self.elastive_load_model.defaultPower = -0.5*self.max_deliver_capacity # [kW] self.elastive_load_model.defaultVertices = [Vertex(0.055, 0, -self.elastive_load_model.defaultPower, True), Vertex(0.06, 0, -self.elastive_load_model.defaultPower/2, True)] # Cross-reference asset & asset model self.elastive_load_model.object = elastive_load elastive_load.model = self.elastive_load_model # Add inelastive and elastive loads as building' assets self.localAssets.extend([elastive_load]) # Add Market market = Market() market.name = 'dayAhead' market.commitment = False market.converged = False market.defaultPrice = 0.0428 # [$/kWh] market.dualityGapThreshold = self.duality_gap_threshold # [0.02 = 2#] market.initialMarketState = MarketState.Inactive market.marketOrder = 1 # This is first and only market market.intervalsToClear = 1 # Only one interval at a time market.futureHorizon = timedelta(hours=24) # Projects 24 hourly future intervals market.intervalDuration = timedelta(hours=1) # [h] Intervals are 1 h long market.marketClearingInterval = timedelta(hours=1) # [h] market.marketClearingTime = Timer.get_cur_time().replace(hour=0, minute=0, second=0, microsecond=0) # Aligns with top of hour market.nextMarketClearingTime = market.marketClearingTime + timedelta(hours=1) self.markets.append(market) # Campus object campus = Neighbor() campus.name = 'PNNL_Campus' campus.description = 'PNNL_Campus' campus.maximumPower = self.max_deliver_capacity campus.minimumPower = 0. # [avg.kW] campus.lossFactor = self.campus_loss_factor # Campus model campus_model = NeighborModel() campus_model.name = 'PNNL_Campus_Model' campus_model.location = self.name campus_model.defaultVertices = [Vertex(0.045, 25, 0, True), Vertex(0.048, 0, self.max_deliver_capacity, True)] campus_model.demand_threshold_coef = self.demand_threshold_coef # campus_model.demandThreshold = self.demand_threshold_coef * self.monthly_peak_power campus_model.demandThreshold = self.monthly_peak_power campus_model.transactive = True campus_model.inject(self, system_loss_topic=self.system_loss_topic) # Avg building meter building_meter = MeterPoint() building_meter.name = self.name + ' ElectricMeter' building_meter.measurementType = MeasurementType.AverageDemandkW building_meter.measurementUnit = MeasurementUnit.kWh campus_model.meterPoints.append(building_meter) # Cross-reference object & model campus_model.object = campus campus.model = campus_model self.campus = campus # Add campus as building's neighbor self.neighbors.append(campus) # Dummy callbacks def aggregate_power(self, peer, sender, bus, topic, headers, message): _log.debug("{}: received topic for power aggregation: {}".format(self.agent_name, topic)) data = message[0] _log.debug("{}: updating power aggregation: {}".format(self.agent_name, data)) 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)) idx = int(market_name.split('_')[-1]) idx += 1 # quantity has 25 values while there are 24 future markets self.building_demand_curves[idx] = (aggregate_demand.points[0], aggregate_demand.points[-1]) def price_callback(self, timestamp, market_name, buyer_seller, price, quantity): _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]) self.prices[idx+1] = price # price has 24 values, current hour price is excluded if price is None: raise "Market {} did not clear. Price is none.".format(market_name) idx += 1 # quantity has 25 values while there are 24 future markets 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 {}, Quantity is {} and quantities 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 mix_market_done = all([False if q is None else True for q in self.quantities[1:]]) if mix_market_done: # Check if any quantity is greater than physical limit of the supply wire _log.debug("Quantity: {}".format(self.quantities)) if not all([False if q > self.max_deliver_capacity else True for q in self.quantities[1:]]): _log.error("One of quantity is greater than " "physical limit {}".format(self.max_deliver_capacity)) # Check demand curves exist all_curves_exist = all([False if q is None else True for q in self.building_demand_curves[1:]]) if not all_curves_exist: _log.error("Demand curves: {}".format(self.building_demand_curves)) raise "Mix market has all quantities but not all demand curves" # Update demand and balance market self.mix_market_running = False #self.elastive_load_model.default_powers = [-q if q is not None else None for q in self.quantities] curves_arr = [(c[0].tuppleize(), c[1].tuppleize()) if c is not None else None for c in self.building_demand_curves] _log2.debug("Data at time {}:".format(Timer.get_cur_time())) _log2.debug("Market intervals: {}".format([x.name for x in self.markets[0].timeIntervals])) _log2.debug("Quantities: {}".format(self.quantities)) _log2.debug("Prices: {}".format(self.prices)) _log2.debug("Curves: {}".format(curves_arr)) db_topic = "/".join([self.db_topic, self.name, "AggregateDemand"]) message = {"Timestamp": format_timestamp(timestamp), "Curves": self.building_demand_curves} headers = {headers_mod.DATE: format_timestamp(Timer.get_cur_time())} self.vip.pubsub.publish("pubsub", db_topic, headers, message).get() db_topic = "/".join([self.db_topic, self.name, "Price"]) price_message = [] for i in range(len(self.markets[0].timeIntervals)): ts = self.markets[0].timeIntervals[i].name price = self.prices[i] quantity = self.quantities[i] price_message.append({'timeInterval': ts, 'price': price, 'quantity': quantity}) message = {"Timestamp": format_timestamp(timestamp), "Price": price_message} headers = {headers_mod.DATE: format_timestamp(Timer.get_cur_time())} self.vip.pubsub.publish("pubsub", db_topic, headers, message).get() self.elastive_load_model.set_tcc_curves(self.quantities, self.prices, self.building_demand_curves) self.balance_market(1) def run_ep_sim(self, start_of_cycle): # Reset price array self.prices = [None for i in range(25)] # Save the 1st quantity as prior 2nd quantity cur_quantity = self.quantities[1] cur_curve = self.building_demand_curves[1] # Reset quantities and curves self.quantities = [None for i in range(25)] self.building_demand_curves = [None for i in range(25)] # If new cycle, set the 1st quantity to the corresponding quantity of previous hour if start_of_cycle: self.quantities[0] = cur_quantity self.building_demand_curves[0] = cur_curve # Balance market with previous known demands market = self.markets[0] # Assume only 1 TNS market per node market.signal_new_data = True market.balance(self) # Check if now is near the end of the hour, applicable only if not in simulation mode now = Timer.get_cur_time() near_end_of_hour = False if not self.simulation: near_end_of_hour = self.near_end_of_hour(now) if market.converged: # Get new prices (expected 25 values: current hour + next 24) prices = market.marginalPrices # There is a case where the balancing happens at the end of the hour and continues to the next hour, which # creates 26 values. Get the last 25 values. prices = prices[-25:] self.prices = [p.value for p in prices] # Signal to start mix market only if the previous market is done if not self.mix_market_running and not near_end_of_hour: self.mix_market_running = True # Read data from e+ output file self.quantities = [] self.prices = [] self.building_demand_curves = [] if self.cur_ep_line < len(self.ep_lines): for i in range(self.cur_ep_line, len(self.ep_lines)): line = self.ep_lines[i] if "mixmarket DEBUG: Quantities: " in line: self.quantities = eval(line[line.find('['):]) if "mixmarket DEBUG: Prices: " in line: self.prices = eval(line[line.find('['):]) if "mixmarket DEBUG: Curves: " in line: tmp = eval(line[line.find('['):]) for item in tmp: if item is None: self.building_demand_curves.append(item) else: p1 = Point(item[0][0], item[0][1]) p2 = Point(item[1][0], item[1][1]) self.building_demand_curves.append((p1, p2)) # Stop when have enough information (ie. all data responded by a single E+ simulation) if len(self.quantities) > 0 and len(self.prices) > 0 and len(self.building_demand_curves) > 0: self.cur_ep_line = i + 1 break self.elastive_load_model.set_tcc_curves(self.quantities, self.prices, self.building_demand_curves) self.balance_market(1) # End E+ output reading 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))