def determine_opportunity_costs_model_b( previous_commitments: List[List[Commitment]], start: datetime, end: datetime, resolution: timedelta, ) -> Series: # Todo: set up at least one more model to determine opportunity costs # Todo (not urgent): slowly decay opportunity costs (to model a discounted sum of future returns) return initialize_series(0, start, end, resolution)
def implement_your_own_flex_split_method_here( ems_agents: List[EMS], flex_request: FlexRequest) -> Series: """Todoc: write docstring.""" return initialize_series( data=flex_request.commitment.constants.values, start=flex_request.start, end=flex_request.end, resolution=flex_request.resolution, )
def determine_opportunity_costs_model_a( previous_commitments: List[List[Commitment]], start: datetime, end: datetime, resolution: timedelta, ) -> Series: """Always expect to make 1 buck. Just a silly implementation to get us started. Previous commitments is a list of commitments per EMS, i.e. a list of lists.""" return initialize_series(1, start, end, resolution)
def equal_flex_split_requested(ems_agents: List[EMS], flex_request: FlexRequest, environment) -> Series: """Split up the requested values in the FlexRequest equally amongst the EMS agents.""" # TODO: Use environment.market_agent.commitment_data["Remaining Imbalances"] # flex_absolute = environment.market_agent.balancing_opportunities[ # "Imbalance (in MW)" # ] - environment.market_agent.commitment_data["Commited flexibility"].fillna(0) flex_absolute = environment.market_agent.commitment_data.loc[:, "Requested flexibility"] flex_absolute = initialize_series( data=[ value / len(ems_agents) for value in flex_absolute[flex_request.start:flex_request.end - flex_request.resolution].values ], start=flex_request.start, end=flex_request.end, resolution=flex_request.resolution, ) flex_relative = initialize_series( data=[ value / len(ems_agents) for value in flex_request.commitment.constants.values ], start=flex_request.start, end=flex_request.end, resolution=flex_request.resolution, ) environment.logfile.write("\nEQUAL FLEX SPLIT: {} \n".format( flex_absolute.values)) return {"target_power": flex_relative, "target_flex": flex_absolute}
def create_prognosis(self, udi_events: List[UdiEvent]) -> Prognosis: """Todoc: write doc string.""" # Todo: create prognosed values based on udi_events prognosed_values = initialize_series( data=None, start=udi_events[0].start, end=udi_events[0].end, resolution=udi_events[0].resolution, ) for event in udi_events: prognosed_values = prognosed_values.add( event.commitment.constants[:], fill_value=0) return ( Prognosis( id=self.environment.plan_board.get_message_id(), start=self.environment.now, end=self.environment.now + self.flex_trade_horizon, resolution=self.environment.resolution, prognosed_values=prognosed_values, ), udi_events, )
def device_scheduler( device_constraints: List[DataFrame], ems_constraints: DataFrame, commitment_quantities: List[Series], commitment_downwards_deviation_price: Union[List[Series], List[float]], commitment_upwards_deviation_price: Union[List[Series], List[float]], ) -> Tuple[List[Series], List[float]]: """Schedule devices given constraints on a device and EMS level, and given a list of commitments by the EMS. The commitments are assumed to be with regards to the flow of energy to the device (positive for consumption, negative for production). The solver minimises the costs of deviating from the commitments, and returns the costs per commitment. Device constraints are on a device level. Handled constraints (listed by column name): max: maximum stock assuming an initial stock of zero (e.g. in MWh or boxes) min: minimum stock assuming an initial stock of zero derivative max: maximum flow (e.g. in MW or boxes/h) derivative min: minimum flow derivative equals: exact amount of flow EMS constraints are on an EMS level. Handled constraints (listed by column name): derivative max: maximum flow derivative min: minimum flow Commitments are on an EMS level. Parameter explanations: commitment_quantities: amounts of flow specified in commitments (both previously ordered and newly requested) - e.g. in MW or boxes/h commitment_downwards_deviation_price: penalty for downwards deviations of the flow - e.g. in EUR/MW or EUR/(boxes/h) - either a single value (same value for each flow value) or a Series (different value for each flow value) commitment_upwards_deviation_price: penalty for upwards deviations of the flow All Series and DataFrames should have the same resolution. For now we pass in the various constraints and prices as separate variables, from which we make a MultiIndex DataFrame. Later we could pass in a MultiIndex DataFrame directly. """ # If the EMS has no devices, don't bother if len(device_constraints) == 0: return [], [] * len(commitment_quantities) # Check if commitments have the same time window and resolution as the constraints start = device_constraints[0].index.values[0] resolution = to_timedelta(device_constraints[0].index.freq) end = device_constraints[0].index.values[-1] + resolution if len(commitment_quantities) != 0: start_c = commitment_quantities[0].index.values[0] resolution_c = to_timedelta(commitment_quantities[0].index.freq) end_c = commitment_quantities[0].index.values[-1] + resolution if not (start_c == start and end_c == end): raise Exception( "Not implemented for different time windows.\n(%s,%s)\n(%s,%s)" % (start, end, start_c, end_c)) if resolution_c != resolution: raise Exception( "Not implemented for different resolutions.\n%s\n%s" % (resolution, resolution_c)) # Turn prices per commitment into prices per commitment flow if len(commitment_downwards_deviation_price) != 0: if all(not isinstance(price, Series) for price in commitment_downwards_deviation_price): commitment_downwards_deviation_price = [ initialize_series(price, start, end, resolution) for price in commitment_downwards_deviation_price ] if len(commitment_upwards_deviation_price) != 0: if all(not isinstance(price, Series) for price in commitment_upwards_deviation_price): commitment_upwards_deviation_price = [ initialize_series(price, start, end, resolution) for price in commitment_upwards_deviation_price ] # Determine appropriate overall bounds for power and price min_down_price = min(min(p) for p in commitment_downwards_deviation_price) max_down_price = max(max(p) for p in commitment_downwards_deviation_price) min_up_price = min(min(p) for p in commitment_upwards_deviation_price) max_up_price = max(max(p) for p in commitment_upwards_deviation_price) overall_min_price = min(min_down_price, min_up_price) overall_max_price = max(max_down_price, max_up_price) overall_min_power = min(ems_constraints["derivative min"]) overall_max_power = max(ems_constraints["derivative max"]) model = ConcreteModel() # Add indices for devices (d), datetimes (j) and commitments (c) model.d = RangeSet(0, len(device_constraints) - 1, doc="Set of devices") model.j = RangeSet(0, len(device_constraints[0].index.values) - 1, doc="Set of datetimes") model.c = RangeSet(0, len(commitment_quantities) - 1, doc="Set of commitments") # Add parameters def commitment_quantity_select(m, c, j): v = commitment_quantities[c].iloc[j] if isnan(v): # Discount this nan commitment by setting the prices to 0 commitment_downwards_deviation_price[c].iloc[j] = 0 commitment_upwards_deviation_price[c].iloc[j] = 0 return 0 else: return v def price_down_select(m, c, j): return commitment_downwards_deviation_price[c].iloc[j] def price_up_select(m, c, j): return commitment_upwards_deviation_price[c].iloc[j] def device_max_select(m, d, j): v = device_constraints[d]["max"].iloc[j] if isnan(v): return infinity else: return v def device_min_select(m, d, j): v = device_constraints[d]["min"].iloc[j] if isnan(v): return -infinity else: return v def device_derivative_max_select(m, d, j): max_v = device_constraints[d]["derivative max"].iloc[j] equal_v = device_constraints[d]["derivative equals"].iloc[j] if isnan(max_v) and isnan(equal_v): return infinity else: return nanmin([max_v]) def device_derivative_min_select(m, d, j): min_v = device_constraints[d]["derivative min"].iloc[j] equal_v = device_constraints[d]["derivative equals"].iloc[j] if isnan(min_v) and isnan(equal_v): return -infinity else: return nanmax([min_v]) def device_derivative_equal_select(m, d, j): min_v = device_constraints[d]["derivative min"].iloc[j] equal_v = device_constraints[d]["derivative equals"].iloc[j] if isnan(equal_v): return 0 else: return nanmax([equal_v]) def ems_derivative_max_select(m, j): v = ems_constraints["derivative max"].iloc[j] if isnan(v): return infinity else: return v def ems_derivative_min_select(m, j): v = ems_constraints["derivative min"].iloc[j] if isnan(v): return -infinity else: return v model.commitment_quantity = Param(model.c, model.j, initialize=commitment_quantity_select) model.up_price = Param(model.c, model.j, initialize=price_up_select) model.down_price = Param(model.c, model.j, initialize=price_down_select) model.device_max = Param(model.d, model.j, initialize=device_max_select) model.device_min = Param(model.d, model.j, initialize=device_min_select) model.device_derivative_max = Param( model.d, model.j, initialize=device_derivative_max_select) model.device_derivative_min = Param( model.d, model.j, initialize=device_derivative_min_select) model.device_derivative_equal = Param( model.d, model.j, initialize=device_derivative_equal_select) model.ems_derivative_max = Param(model.j, initialize=ems_derivative_max_select) model.ems_derivative_min = Param(model.j, initialize=ems_derivative_min_select) # Add variables model.power = Var( model.d, model.j, domain=Reals, initialize=0, bounds=(overall_min_power, overall_max_power), ) # Add constraints as a tuple of (lower bound, value, upper bound) def device_bounds(m, d, j): return ( m.device_min[d, j], sum(m.power[d, k] for k in range(0, j + 1)), m.device_max[d, j], ) def device_derivative_bounds(m, d, j): return ( m.device_derivative_min[d, j], m.power[d, j] - m.device_derivative_equal[d, j], m.device_derivative_max[d, j], ) def ems_derivative_bounds(m, j): return m.ems_derivative_min[j], sum( m.power[:, j]), m.ems_derivative_max[j] model.device_energy_bounds = Constraint(model.d, model.j, rule=device_bounds) model.device_power_bounds = Constraint(model.d, model.j, rule=device_derivative_bounds) model.ems_power_bounds = Constraint(model.j, rule=ems_derivative_bounds) # Add logical disjunction for deviations model.price = Var(model.c, model.j, initialize=0, bounds=(overall_min_price, overall_max_price)) def up_linker(b, c, d, j): # print("In up linker") m = b.model() ems_power_in_j = sum(m.power[d, j] for d in m.d) ems_power_deviation = ems_power_in_j - m.commitment_quantity[c, j] # try: # print(value(ems_power_deviation)) # except: # pass b.linker = Constraint(expr=m.price[c, j] == m.up_price[c, j]) b.constr = Constraint(expr=ems_power_deviation >= 0) b.BigM = Suffix(direction=Suffix.LOCAL) b.BigM[b.linker] = 10e5 return def down_linker(b, c, d, j): # print("In down linker") m = b.model() ems_power_in_j = sum(m.power[d, j] for d in m.d) ems_power_deviation = ems_power_in_j - m.commitment_quantity[c, j] # try: # print(value(ems_power_deviation)) # except: # pass b.linker = Constraint(expr=m.price[c, j] == m.down_price[c, j]) b.constr = Constraint(expr=ems_power_deviation <= 0) b.BigM = Suffix(direction=Suffix.LOCAL) b.BigM[b.linker] = 10e5 return # def zero_linker(b, c, d, j): # #print("In down linker") # m = b.model() # ems_power_in_j = sum(m.power[d, j] for d in m.d) # ems_power_deviation = ems_power_in_j - m.commitment_quantity[c, j] # #try: # #print(value(ems_power_deviation)) # #except: # #pass # b.linker = Constraint(expr=m.price[c, j] == 0) # b.constr = Constraint(expr=ems_power_deviation == 0) # #b.BigM = Suffix(direction=Suffix.LOCAL) # #b.BigM[b.linker] = 10e10 # return model.up_deviation = Disjunct(model.c, model.d, model.j, rule=up_linker) model.down_deviation = Disjunct(model.c, model.d, model.j, rule=down_linker) # model.zero_deviation = Disjunct(model.c, model.d, model.j, rule=zero_linker) def bind_prices(m, c, d, j): return [ model.up_deviation[c, d, j], model.down_deviation[c, d, j], # model.zero_deviation[c, d, j] ] model.up_or_down_deviation = Disjunction(model.c, model.d, model.j, rule=bind_prices, xor=True) # Add objective def cost_function(m): costs = 0 for j in m.j: for c in m.c: ems_power_in_j = sum(m.power[d, j] for d in m.d) ems_power_deviation = ems_power_in_j - m.commitment_quantity[c, j] costs += ems_power_deviation * m.price[c, j] return costs model.costs = Objective(rule=cost_function, sense=minimize) # def xfrm(m): # TransformationFactory('gdp.chull').apply_to(m) # model.xfrm = BuildAction(rule=xfrm) # Transform and solve xfrm = TransformationFactory("gdp.bigm") xfrm.apply_to(model) solver = SolverFactory( "cplex", executable="D:/CPLEX/Studio/cplex/bin/x64_win64/cplex") # solver.options['CPXchgprobtype'] = "CPXPROB_QP" # solver.options["solver"] = "CPXqpopt" solver.options["qpmethod"] = 1 solver.options["optimalitytarget"] = 3 # solver.options["acceptable_constr_viol_tol"] = 10 # solver.options['acceptable_tol'] = 1 # solver.options['acceptable_dual_inf_tol'] = 10 # solver.options['acceptable_compl_inf_tol'] = 10 results = solver.solve(model, tee=False) planned_costs = value(model.costs) planned_power_per_device = [] for d in model.d: planned_device_power = [ round(model.power[d, j].value, 3) for j in model.j ] planned_power_per_device.append( initialize_series(planned_device_power, start=start, end=end, resolution=resolution)) # model.display() # results.pprint() # model.down_deviation.pprint() # model.up_deviation.pprint() # model.power.pprint() # print(planned_power_per_device) # input() # Redo the cost calculation, because before the solver actually approximated the prices. def redo_cost_calculation(m): commitments_costs = [] for c in m.c: commitment_cost = 0 for j in m.j: ems_power_in_j = sum(m.power[d, j] for d in m.d) ems_power_deviation = ems_power_in_j - m.commitment_quantity[c, j] if value(ems_power_deviation) >= 0: commitment_cost += round( value(ems_power_deviation * m.up_price[c, j]), 3) else: commitment_cost += round( value(ems_power_deviation * m.down_price[c, j]), 3) commitments_costs.append(commitment_cost) return commitments_costs planned_costs_per_commitment = redo_cost_calculation(model) # print(planned_costs_per_commitment) return planned_power_per_device, planned_costs_per_commitment
def create_adverse_and_plain_offers(self, flex_request, best_udi_event, opportunity_costs, plan_board): ''' Adverse_flexoffer: Offered power and offered flexibility values comply with the requested ones, regardless of the actual values the EMS has planned.Hence, the commitment costs include deviation costs for each datetime where the EM-Systems plan to deviate. Plain_flexoffer: Offered values and offered flexibility comply with the planned values of the EM-Systems. Commitment costs include only the cost difference of the contract costs.''' start = flex_request.start end = flex_request.end - flex_request.resolution offered_flexibility = best_udi_event.offered_flexibility requested_flexibility = flex_request.requested_flexibility deviation_costs_best_udi_event = initialize_series( data=None, start=flex_request.start, end=flex_request.end, resolution=flex_request.resolution) # for index, row in offered_flexibility.loc[start:end].iteritems(): # # if offered_flexibility.loc[index] is not nan_to_num(requested_flexibility.loc[index]): # # if offered_flexibility.loc[index] > requested_flexibility.loc[index]: # # diff = (offered_flexibility.loc[index] - requested_flexibility[index]) # # if diff > 0: # # deviation_costs_best_udi_event[index] = diff * best_udi_event.deviation_cost_curve.gradient_up # else: # deviation_costs_best_udi_event[index] = diff * best_udi_event.deviation_cost_curve.gradient_down # # elif offered_flexibility.loc[index] < requested_flexibility.loc[index]: # diff = (requested_flexibility[index] - offered_flexibility.loc[index]) # # if diff > 0: # # deviation_costs_best_udi_event[index] = diff * best_udi_event.deviation_cost_curve.gradient_up # else: # deviation_costs_best_udi_event[index] = diff * best_udi_event.deviation_cost_curve.gradient_down # # else: # deviation_costs_best_udi_event[index] = 0 # If deviation costs in best_udi_event are not defined, overwrite the nan with 0 (otherwise the following calculations fail due to nan) # for index, row in best_udi_event.deviation_costs.loc[start:end].iteritems(): # # if isnull(best_udi_event.deviation_costs.loc[index]): # # deviation_costs_best_udi_event[index] = 0 # # else: # deviation_costs_best_udi_event[index] = best_udi_event.deviation_cost.loc[index] # adverse_values = flex_request.requested_values.loc[start : end].where(isnull(requested_flexibility) == False) # # adverse_flexibility = flex_request.requested_flexibility.loc[start : end].where(isnull(requested_flexibility) == False) # # commitment_costs_best_udi_event = best_udi_event.costs \ # + opportunity_costs.loc[start : end].sum(axis="index") # # adverse_flex_offer = FlexOffer( # id=plan_board.get_message_id(), # description="Adverse flex offer", # offered_values=adverse_values, # offered_flexibility=adverse_flexibility, # deviation_cost_curve=flex_request.commitment.deviation_cost_curve, # costs=commitment_costs_best_udi_event, # flex_request=flex_request, # ) plain_values = best_udi_event.offered_values.loc[start:end] plain_flexibility = offered_flexibility.loc[start:end] commitment_costs_best_udi_event = best_udi_event.costs \ + opportunity_costs.loc[start : end].sum(axis="index") plain_flex_offer = FlexOffer( id=plan_board.get_message_id(), description="Plain flex offer", offered_values=plain_values, offered_flexibility=plain_flexibility, deviation_cost_curve=flex_request.commitment.deviation_cost_curve, costs=commitment_costs_best_udi_event, flex_request=flex_request, ) flex_offers = [] flex_offers.append(plain_flex_offer) # Only take both offers if values are different # if adverse_flex_offer.costs.sum(axis=0) is not plain_flex_offer.costs.sum(axis=0): # flex_offers.append(plain_flex_offer) # # else: # flex_offers.append(adverse_flex_offer) # flex_offers.append(plain_flex_offer) return flex_offers
def step(self): print( "+++++++++++++++++++++ NEW STEP +++++++++++++++++++++ TIME: {}\n" .format(self.environment.now)) # Pull market agent request for prognosis prognosis_request = self.market_agent.post_prognosis_request() # Add prognosis request to plan board self.environment.plan_board.store_message( timeperiod=self.environment.now, message=prognosis_request, keys=["TA", "MA"], ) # Decision Gate 1: TA and MA bargain over prognosis price # Update parameters related to q-learning # self.prognosis_q_parameter["Step now"] = self.environment.step_now print( "---------------------PROGNOSIS NEGOTIATION--------------------------" ) update_adaptive_strategy_data(description="Prognosis", ta_parameter=self.prognosis_parameter, plan_board=self.environment.plan_board, timeperiod_now=self.environment.now, step_now=self.environment.step_now, snapshot=True) prognosis_decision = start_negotiation( description="Prognosis", environment_now=self.environment.now, ta_parameter=self.prognosis_parameter, ma_parameter=self.environment.market_agent.prognosis_parameter, plan_board=self.environment.plan_board, negotiation_log=self.environment.plan_board. prognosis_negotiations_log, ) # If the negotiation got cleared let the model continue, otherwise proceed to next step of simulation horizon if "Not Cleared" in prognosis_decision["Status"]: self.commitment_data.loc[ self.environment.now, "Clearing price prognosis negotiations 1"] = nan return else: print("TA: Prognosis negotiation status: AGREEMENT\n") self.commitment_data.loc[ self.environment.now, "Clearing price prognosis negotiations 1"] = prognosis_decision[ "Clearing price"] pass # Pull UdiEvents while pushing empty DeviceMessages to each EMS print( "---------------------PROGNOSIS UDI EVENTS--------------------------" ) udi_events = [] for ems in self.ems_agents: # Create empty device message device_message = self.create_device_message( ems, description="Prognosis data", targeted_power=initialize_series( None, self.environment.now, self.environment.now + self.flex_trade_horizon, self.environment.resolution, ), targeted_flexibility=initialize_series( 0, self.environment.now, self.environment.now + self.flex_trade_horizon, self.environment.resolution, ), deviation_cost_curve=DeviationCostCurve( gradient=0, flow_unit_multiplier=self.environment.flow_unit_multiplier, ), ) # Add DeviceMessage to plan board self.environment.plan_board.store_message( timeperiod=self.environment.now, message=device_message, keys=[ems.name]) # Get UDI event udi_events.append(ems.post_udi_event(device_message)) # Add UDI events to plan board for event in udi_events: self.environment.plan_board.store_message( timeperiod=self.environment.now, message=event, keys=[ems.name]) # Determine Prognosis prognosis, prognosis_udi_events = self.create_prognosis(udi_events) print("TA: Prognosis event flexibility: {}".format( prognosis_udi_events[0].offered_flexibility)) # Add Prognosis to planboard message log self.environment.plan_board.store_message( timeperiod=self.environment.now, message=prognosis, keys=["TA", "MA"]) # TODO: Decision Gate 2: If prognosis is very interesting, than another negotiation over the prognosis price starts # Pull FlexRequest while pushing Prognosis to MA flex_request = self.market_agent.post_flex_request( self.market_agent.get_prognosis(prognosis)) # Add message to plan board self.environment.plan_board.store_message( timeperiod=self.environment.now, message=flex_request, keys=["TA", "MA"]) # Determine FlexOffer (could be multiple offers) flex_offers, flex_offer_udi_events = self.create_flex_offer( flex_request) print("TA: Flexrequest udi event flexibility: {}\n".format( flex_offer_udi_events[0].offered_flexibility)) # Flex Decision Gate 1: TA and MA bargain over flex request price update_adaptive_strategy_data(description="Flexrequest", ta_parameter=self.flexrequest_parameter, plan_board=self.environment.plan_board, timeperiod_now=self.environment.now, step_now=self.environment.step_now, snapshot=True) for enum, offer in enumerate(flex_offers): self.flexrequest_parameter["Reservation price"] = offer.costs.sum( axis="index") # Submit flexoffer with adverse costs to MA self.market_agent.get_flex_offer(offer) self.environment.plan_board.store_message( timeperiod=self.environment.now, message=offer, keys=["TA", "MA"]) print("TA: Market agent reservation price {}\n".format( self.environment.market_agent. flexrequest_parameter["Reservation price"])) print("TA: Trading agent reservation price {}\n".format( self.flexrequest_parameter["Reservation price"])) flexrequest_decision = start_negotiation( description=offer.description, environment_now=self.environment.now, ta_parameter=self.flexrequest_parameter, ma_parameter=self.environment.market_agent. flexrequest_parameter, plan_board=self.environment.plan_board, negotiation_log=self.environment.plan_board. flexrequest_negotiations_log, ) print("TA: Flex negotiation status: {}\n".format( flexrequest_decision["Status"])) print("TA: Flex negotiation clearing price: {}\n".format( flexrequest_decision["Clearing price"])) print( "----------------------DATA VALUE UPDATE-------------------------- TIME: {} \n" .format(self.environment.now)) if "NOT CLEARED" in flexrequest_decision["Status"]: # Check if another offer is available and continue with the next negotiation if enum is not len(flex_offers) - 1: print("Next Round") print("TA: Next offer: {}".format(offer[enum + 1])) continue # If there is no offer left to bargain over, save agents data and return else: for ems, event in zip(self.ems_agents, prognosis_udi_events): print( "TA: Store prognosis flex as commited: {}".format( event.offered_flexibility. loc[offer.start:offer.end - offer.resolution])) # Assign the prognosed udi event values to a device message, and pass it to the EMS for storing data and commitment ems.store_data( commitments=event.commitment, device_message=self.create_device_message( ems, description="Failed Negotiation", targeted_power=event.offered_values. loc[offer.start:offer.end - offer.resolution], targeted_flexibility=event.offered_flexibility. loc[offer.start:offer.end - offer.resolution], deviation_cost_curve=(0, 0), costs=event.costs, order=True), ) return # "CLEARED" else: # Assign the offered udi event values to a device message, and pass it to the EMS for storing data and commitment for ems, event in zip(self.ems_agents, flex_offer_udi_events): print("TA: Cleared negotiaton over {}\n".format( offer.description)) commited_power = deepcopy(event.offered_values) commited_flexibility = deepcopy(event.offered_flexibility) # This loop cuts already commited values from actual offer (-> applicable commitments) for index, row in offer.offered_values.iteritems(): if isnull(offer.offered_flexibility.loc[index]): commited_flexibility.loc[index] = nan if isnull(offer.offered_values.loc[index]): commited_power.loc[index] = nan self.store_commitment_data(commited_power, commited_flexibility) ems.store_data( commitments=event.commitment, device_message=self.create_device_message( ems, description="Succeeded Negotiation", targeted_power=commited_power, targeted_flexibility=commited_flexibility, deviation_cost_curve=flex_request.commitment. deviation_cost_curve, costs=event.costs, order=True), ) # Pull FlexOrder by pushing FlexOffer to MA flex_order = self.market_agent.post_flex_order( self.market_agent.get_flex_offer(flex_offer=offer, order=True)) #Add Order to planboard message log self.environment.plan_board.store_message( timeperiod=self.environment.now, message=flex_order, keys=["TA", "MA"]) return
def create_flex_offer( self, flex_request: FlexRequest) -> Tuple[FlexOffer, List[UdiEvent]]: """Create a FlexOffer after freely exploring EMS flexibility.""" # Todo: set aspiration margin udi_events_local_memory = [] udi_event_cnt = 0 udi_event_costs = float("inf") # Either do a central optimisation just for the analysis, or do a flex split if self.central_optimization is True: # Todo: create a UdiEvent based on central optimization best_udi_event = None best_udi_events = [None] else: # Todo: write more flex_split_methods if needed, where each method results in one UdiEvent flex_split_methods = [ equal_flex_split_requested, # implement_your_own_flex_split_method_here, ] aggregated_udi_events = [] for flex_split_method in flex_split_methods: # TODO: Find other way to get absolute flex values and remove environment from arguments then output = flex_split_method(self.ems_agents, flex_request, self.environment) targeted_power = output["target_power"] targeted_flexibility = output["target_flex"] # Find out how well the EMS agents can fulfil the FlexRequest. udi_events = [] for ems in self.ems_agents: # TODO: Get targeted_flex into device message and store values at EMS for index, row in targeted_flexibility.iteritems(): ems.ems_data.loc[ index, "Requested flexibility"] = targeted_flexibility.loc[ index] # Determine DeviceMessage device_message = self.create_device_message( ems, description="Flex request", targeted_power=targeted_power, targeted_flexibility=targeted_flexibility, deviation_cost_curve=flex_request.commitment. deviation_cost_curve, ) # Pull UdiEvent while pushing DeviceMessage to EMS udi_events.append( ems.post_udi_event( ems.get_device_message(device_message))) udi_events_local_memory.append(udi_events) # Calculate aggregated power values and total costs (private and bid) offered_values_aggregated = initialize_series( data=[ sum(x) for x in zip(*[ udi_event.offered_power.values for udi_event in udi_events ]) ], start=udi_events[0].start, end=udi_events[0].end, resolution=udi_events[0].resolution, ) offered_flexibility_aggregated = initialize_series( data=[ sum(x) for x in zip(*[ udi_event.offered_flexibility.values for udi_event in udi_events ]) ], start=udi_events[0].start, end=udi_events[0].end, resolution=udi_events[0].resolution, ) offered_costs_aggregated = initialize_series( data=[ sum(x) for x in zip(*[ udi_event.costs.values for udi_event in udi_events ]) ], start=udi_events[0].start, end=udi_events[0].end, resolution=udi_events[0].resolution, ) # Check if already commited values are same as actual offer values. If true, don't offer them again. offered_values_aggregated, offered_flexibility_aggregated, offered_costs_aggregated = \ sort_out_already_commited_values(self, offered_values_aggregated, offered_flexibility_aggregated, offered_costs_aggregated) #TODO: Check if dev costs and contract costs are still used aggregated_udi_events.append( UdiEvent( id= -1, # Not part of the plan board, as this is just a convenient object for the Trading Agent offered_values=offered_values_aggregated, offered_flexibility=offered_flexibility_aggregated, contract_costs=sum([ udi_event.contract_costs for udi_event in udi_events ]), deviation_costs=sum([ udi_event.deviation_costs for udi_event in udi_events ]), deviation_cost_curve=flex_request.commitment. deviation_cost_curve, costs=offered_costs_aggregated, )) # Todo: choose the best aggregated UdiEvent (implement policies as separate module) best_udi_event = None for event in aggregated_udi_events: if event.costs.sum() < udi_event_costs: best_udi_event = event else: pass if len(aggregated_udi_events) > 1: udi_event_cnt += 1 # Unpack opportunity costs for actual horizon opportunity_costs = self.commitment_data["Opportunity costs"].loc[ flex_request.start:flex_request.end - flex_request.resolution] # NOTE: Use function call to create one or multiple specific offers after UDI-event aggregation # TODO: Move function create_adverse_and_plain_offers from comopt.utils to comopt.model.utils # -> There's some (recursive?) issue when loading the dependencies that i couldn't get solved (yet). flex_offers = create_adverse_and_plain_offers( self, flex_request, best_udi_event, opportunity_costs, self.environment.plan_board) # Todo: suggest DeviationCostCurve # deviation_cost_curve = DeviationCostCurve(gradient=1, flow_unit_multiplier=self.environment.flow_unit_multiplier) #------------- PRINTS ---------------# for offer in flex_offers: print("\nTA: Costs/Power/Flex {}: {} {} {}\n".format( offer.description, offer.costs, offer.offered_values, offer.offered_flexibility)) # print("TA: Power {}: {}\n".format(offer.description, ) # print("TA: Flexibility {}: {}\n".format(offer.description, )) # print("\nTA: UDI Event flex: {}\n".format(udi_events_local_memory[udi_event_cnt][0].offered_flexibility)) return flex_offers, udi_events_local_memory[udi_event_cnt]