def get_forecast_weatherservice(self, mkt): """ Uses VOLTTRON DarkSky weather agent running on local or remote platform to get 24 hour forecast for weather data. :param mkt: :return: """ weather_results = None weather_data = None try: result = self.parent.vip.rpc.call(self.weather_vip, "get_hourly_forecast", self.location, external_platform=self.remote_platform).get(timeout=15) weather_results = result[0]["weather_results"] except (gevent.Timeout, RemoteError) as ex: _log.warning("RPC call to {} failed for weather forecast: {}".format(self.weather_vip, ex)) if weather_results is not None: try: weather_data = [[parser.parse(oat[0]).astimezone(self.localtz), oat[1][self.oat_point_name]] for oat in weather_results] weather_data = [[oat[0].replace(tzinfo=None), oat[1]] for oat in weather_data] except KeyError: if not self.predictedValues: raise Exception("Measurement Point Name is not correct") # How do we deal with never getting weather information? Exit? except Exception as ex: if not self.predictedValues: raise Exception("Exception {} processing weather data.".format(ex)) # Copy weather data to predictedValues if weather_data is not None: self.predictedValues = [] for ti in mkt.timeIntervals: # Find item which has the same timestamp as ti.timeStamp start_time = ti.startTime.replace(minute=0) items = [x[1] for x in weather_data if x[0] == start_time] # Create interval value and add it to predicted values if items: temp = items[0] interval_value = IntervalValue(self, ti, mkt, MeasurementType.PredictedValue, temp) self.predictedValues.append(interval_value) elif self.predictedValues: hour_gap = mkt.timeIntervals[0].startTime - self.predictedValues[0].timeInterval.startTime max_hour_gap = timedelta(hours=4) if hour_gap > max_hour_gap: self.predictedValues = [] raise Exception('No weather data for time: {}'.format(utils.format_timestamp(mkt.timeIntervals[0].startTime))) else: predictedValues = [] for i in range(1, len(mkt.timeIntervals)): interval_value = IntervalValue(self, mkt.timeIntervals[i], mkt, MeasurementType.PredictedValue, self.predictedValues[i-1].value) predictedValues.append(interval_value) self.predictedValues = predictedValues else: raise Exception( 'No weather data for time: {}'.format(utils.format_timestamp(mkt.timeIntervals[0].startTime)))
def update_information(ism, mkt): # Gather active time intervals ti ti = mkt.timeIntervals # index through active time intervals ti for i in range(len(ti)): # for i = 1:length(ti) # Get the start time for the indexed time interval st = ti(i).startTime # Extract the starting time hour hr = st.hour # Look up the value in a table. NOTE: tables may be used during # development until active information services are developed. # Is the purpose of this one to read MODERATE weather temperature? YES T = readtable(ism.file) value = T(hr + 1, 1) # Check whether the information exists in the indexed time interval # Question: what is ism? InformationServiceModel doesn't have 'values' as well as 'iv' properties. # Suggestion: use long name as much as possible # Need an example on what this one does. Really need a unit test here? #iv = findobj(ism.values, 'timeInterval', ti(i)) iv = [x for x in ism.values if x.timeInterval.startTime == ti[i].startTime] # iv = iv[0] if len(iv)>0 else None if iv is None: # isempty(iv): # The value does not exist in the indexed time interval. Create it and store it. #iv = IntervalValue(ism, ti(i), mkt, 'Temperature', value) iv = IntervalValue(ism, ti[i], mkt, MeasurementType.Temperature, value) ism.values = [ism.values, iv] else: # The value exists in the indexed time interval. Simply reassign it iv.value = value
def update_dual_costs(self, mkt): # Update the dual cost for all active time intervals # (NOTE: Choosing not to separate this def from the base class because # cost might need to be handled differently and redefined in subclasses.) # Gather the active time intervals ti time_intervals = mkt.timeIntervals time_interval_values = [t.startTime for t in time_intervals] self.dualCosts = [ x for x in self.dualCosts if x.timeInterval.startTime in time_interval_values ] # Index through the time intervals ti for i in range(1, len(time_intervals)): # Find the marginal price mp for the indexed time interval ti(i) in # the given market mkt mp = find_obj_by_ti(mkt.marginalPrices, time_intervals[i]) mp = mp.value # a marginal price [$/kWh] # Find the scheduled power sp for the asset in the indexed time interval ti(i) sp = find_obj_by_ti(self.scheduledPowers, time_intervals[i]) sp = sp.value # schedule power [avg.kW] # Find the production cost in the indexed time interval pc = find_obj_by_ti(self.productionCosts, time_intervals[i]) pc = pc.value # production cost [$] # Dual cost in the time interval is calculated as production cost, # minus the product of marginal price, scheduled power, and the # duration of the time interval. dur = time_intervals[i].duration.seconds // 3600 dc = pc - (mp * sp * dur) # a dual cost [$] # Check whether a dual cost exists in the indexed time interval iv = find_obj_by_ti(self.dualCosts, time_intervals[i]) if iv is None: # No dual cost was found in the indexed time interval. Create an # interval value and assign it the calculated value. iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.DualCost, dc) # Append the new interval value to the list of active interval values self.dualCosts.append(iv) else: # The dual cost value was found to already exist in the indexed # time interval. Simply reassign it the new calculated value. iv.value = dc # a dual cost [$] # Ensure that only active time intervals are in the list of dual costs adc #self.dualCosts = [x for x in self.dualCosts if x.timeInterval in ti] # Sum the total dual cost and save the value self.totalDualCost = sum([x.value for x in self.dualCosts]) dc = [(x.timeInterval.name, x.value) for x in self.dualCosts] _log.debug("{} asset model dual costs are: {}".format(self.name, dc))
def update_vertices(self, mkt): # Create vertices to represent the asset's flexibility # # For the base local asset model, a single, inelastic power is needed. # There is no flexibility. The constant power may be represented by a # single (price, power) point (See struct Vertex). # Gather active time intervals ti = mkt.timeIntervals # active TimeIntervals time_interval_values = [t.startTime for t in ti] self.activeVertices = [ x for x in self.activeVertices if x.timeInterval.startTime in time_interval_values ] # Index through active time intervals ti for i in range(len(ti)): # Find the scheduled power for the indexed time interval # Extract the scheduled power value sp = find_obj_by_ti(self.scheduledPowers, ti[i]) sp = sp.value # avg. kW] # Create the vertex that can represent this (lack of) flexibility value = Vertex(float("inf"), 0.0, sp, True) # Check to see if the active vertex already exists for this indexed time interval. iv = find_obj_by_ti(self.activeVertices, ti[i]) # If the active vertex does not exist, a new interval value must be # created and stored. if iv is None: # Create the interval value and place the active vertex in it iv = IntervalValue(self, ti[i], mkt, MeasurementType.ActiveVertex, value) # Append the interval value to the list of active vertices self.activeVertices.append(iv) else: # Otherwise, simply reassign the active vertex value to the # existing listed interval value. (NOTE that this base local # asset model unnecessarily reassigns constant values, but the # reassignment is allowed because it teaches how a more dynamic # assignment may be maintained. iv.value = value av = [(x.timeInterval.name, x.value.marginalPrice, x.value.power) for x in self.activeVertices] _log.debug("{} asset model active vertices are: {}".format( self.name, av))
def test_view_marginal_prices(): print('Running Market.test_view_marginal_prices()') pf = 'pass' # Establish a test market test_mkt = Market # Create and store three TimeIntervals dt = datetime at = dt dur = timedelta(hours=1) mkt = test_mkt mct = dt ti = [] # TimeInterval.empty st = dt ti[0] = TimeInterval(at, dur, mkt, mct, st) st = st + dur ti[1] = TimeInterval(at, dur, mkt, mct, st) st = st + dur ti[2] = TimeInterval(at, dur, mkt, mct, st) test_mkt.timeIntervals = ti ## Test using a Market object print('- using a Market object') iv = [] # IntervalValue.empty # Create and store three marginal price values iv[0] = IntervalValue(test_mkt, ti[2], test_mkt, MeasurementType.MarginalPrice, 3) iv[1] = IntervalValue(test_mkt, ti[0], test_mkt, MeasurementType.MarginalPrice, 1) iv[2] = IntervalValue(test_mkt, ti[1], test_mkt, MeasurementType.MarginalPrice, 2) test_mkt.marginalPrices = iv try: test_mkt.view_marginal_prices() print(' - function ran without errors') except: raise (' - function encountered errors and stopped') # Success print('- the test ran to completion') print('Result: #s\n\n', pf)
def update_vertices(self, mkt): if self.tcc_curves is None: super(TccModel, self).update_vertices(mkt) else: time_intervals = mkt.timeIntervals _log.debug("At {}, Tcc market has {} intervals".format( Timer.get_cur_time(), len(time_intervals))) # 1st mix-market doesn't have tcc_curves info => keep previous active vertices if self.tcc_curves[0] is None: first_interval_vertices = [ iv for iv in self.activeVertices if iv.timeInterval.startTime == time_intervals[0].startTime ] self.activeVertices = first_interval_vertices # After 1st mix-market, we always have tcc_curves for 25 market intervals => clear all previous av else: self.activeVertices = [] for i in range(len(time_intervals)): if self.tcc_curves[i] is None: continue point1 = self.tcc_curves[i][0].tuppleize() q1 = -point1[0] p1 = point1[1] point2 = self.tcc_curves[i][1].tuppleize() q2 = -point2[0] p2 = point2[1] v1 = Vertex(p1, 0, q1) iv1 = IntervalValue(self, time_intervals[i], mkt, MeasurementType.ActiveVertex, v1) self.activeVertices.append(iv1) if q2 != q1: v2 = Vertex(p2, 0, q2) iv2 = IntervalValue(self, time_intervals[i], mkt, MeasurementType.ActiveVertex, v2) self.activeVertices.append(iv2) av = [(x.timeInterval.name, x.value.marginalPrice, x.value.power) for x in self.activeVertices] _log.debug("TCC active vertices are: {}".format(av))
def schedule_engagement(self, mkt): # To assign engagement, or commitment, which # is relevant to some local assets (supports future capabilities). # NOTE: The assignment of engagement schedule, if used, may be assigned # during the scheduling of power, not separately as demonstrated here. # Commitment and engagement are closely aligned with the optimal # production costs of schedulable generators and utility def of # engagements (e.g., demand responses). # NOTE: Because this is a future capability, Implementers might choose to # simply return from the call until LocalAsset behaviors are found to need # commitment or engagement. # Gather the active time intervals ti time_intervals = mkt.timeIntervals # active TimeIntervals time_interval_values = [t.startTime for t in time_intervals] self.engagementSchedule = [ x for x in self.engagementSchedule if x.timeInterval.startTime in time_interval_values ] # Index through the active time intervals ti for i in range(len(time_intervals)): # Check whether an engagement schedule exists in the indexed time interval iv = find_obj_by_ti(self.engagementSchedule, time_intervals[i]) # NOTE: this template currently assigns engagement value as true (i.e., engaged). val = True # Asset is committed or engaged if iv is None: # No engagement schedule was found in the indexed time interval. # Create an interval value and assign its value. iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.EngagementValue, val) # an IntervalValue # Append the interval value to the list of active interval values self.engagementSchedule.append(iv) else: # An engagement schedule was found in the indexed time interval. # Simpy reassign its value. iv.value = val # [$]
def test_view_net_curve(): print('Running Market.test_view_net_curve()') pf = 'pass' # Establish a test market test_mkt = Market() # Create and store one TimeInterval dt = datetime(2018, 1, 1, 12, 0, 0) at = dt dur = timedelta(hours=1) mkt = test_mkt mct = dt st = dt ti = [TimeInterval(at, dur, mkt, mct, st)] test_mkt.timeIntervals = ti ## Test using a Market object print('- using a Market object') # Create and store three active vertices v = [Vertex(0.01, 0, -1), Vertex(0.02, 0, 1), Vertex(0.03, 0, 1)] iv = [ IntervalValue(test_mkt, ti[0], test_mkt, MeasurementType.ActiveVertex, v[2]), IntervalValue(test_mkt, ti[0], test_mkt, MeasurementType.ActiveVertex, v[0]), IntervalValue(test_mkt, ti[0], test_mkt, MeasurementType.ActiveVertex, v[1]) ] test_mkt.activeVertices = [iv] test_mkt.view_net_curve(0) print(' - function ran without errors') # Success print('- the test ran to completion') print('Result: #s\n\n', pf)
def schedule_power(self, mkt): self.scheduledPowers = [] time_intervals = mkt.timeIntervals for i in range(len(time_intervals)): value = self.defaultPower if self.quantities is not None and len( self.quantities) > i and self.quantities[i] is not None: value = -self.quantities[i] iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.ScheduledPower, value) self.scheduledPowers.append(iv) sp = [(x.timeInterval.name, x.value) for x in self.scheduledPowers] _log.debug("TCC scheduledPowers are: {}".format(sp))
def assign_system_vertices(self, mtn): # Collect active vertices from neighbor and asset models # and reassign them with aggregate system information for all active time intervals. # # ASSUMPTIONS: # - Active time intervals exist and are up-to-date # - Local convergence has occurred, meaning that power balance, marginal # price, and production costs have been adequately resolved from the # local agent's perspective # - The active vertices of local asset models exist and are up-to-date. # The vertices represent available power flexibility. The vertices # include meaningful, accurate production-cost information. # - There is agreement locally and in the network concerning the format # and content of transactive records # - Calls method mkt.sum_vertices in each time interval. # # INPUTS: # mtn - myTransactiveNode object # # OUTPUTS: # - Updates mkt.activeVertices - vertices that define the net system # balance and flexibility. The meaning of the vertex properties are # - marginalPrice: marginal price [$/kWh] # - cost: total production cost at the vertex [$]. (A locally # meaningful blended electricity price is (total production cost / # total production)). # - power: system net power at the vertex (The system "clears" where # system net power is zero.) time_interval_values = [t.startTime for t in self.timeIntervals] # Delete any active vertices that are not in active time intervals. This # prevents time intervals from accumulating indefinitely. self.activeVertices = [x for x in self.activeVertices if x.timeInterval.startTime in time_interval_values] for ti in self.timeIntervals: # Find and delete existing aggregate active vertices in the indexed # time interval. These shall be recreated. self.activeVertices = [x for x in self.activeVertices if x.timeInterval.startTime != ti.startTime] # Call the utility method mkt.sum_vertices to recreate the # aggregate vertices in the indexed time interval. (This method is # separated out because it will be used by other methods.) s_vertices = self.sum_vertices(mtn, ti) # Create and store interval values for each new aggregate vertex v for sv in s_vertices: iv = IntervalValue(self, ti, self, MeasurementType.SystemVertex, sv) self.activeVertices.append(iv)
def test_update_vertices(): # TEST_UPDATE_VERTICES() - test method update_vertices(), which for this # base class of LocalAssetModel does practically nothing and must be # redefined by child classes that represent flesible assets. print('Running LocalAssetModel.test_update_vertices()') pf = 'pass' # Create a test Market object. test_market = Market # Create and store a TimeInterval object. dt = datetime.now() # datetime that may be used for most datetime arguments time_interval = TimeInterval(dt, timedelta(hours=1), test_market, dt, dt) test_market.timeIntervals = [time_interval] # Create a test LocalAssetModel object. test_model = LocalAssetModel() # Create and store a scheduled power IntervalValue in the active time interval. test_model.scheduledPowers = [ IntervalValue(test_model, time_interval, test_market, MeasurementType.ScheduledPower, 50)] # Create a LocalAsset object and its maximum and minimum powers. test_object = LocalAsset() test_object.maximumPower = 200 test_object.minimumPower = 0 # Have the LocalAsset model and object cross reference one another. test_object.model = test_model test_model.object = test_object ## TEST 1 print('- Test 1: Basic operation') test_model.update_vertices(test_market) print(' - the method ran without errors') if len(test_model.activeVertices) != 1: pf = 'fail' print(' - there is an unexpected number of active vertices') else: print(' - the expected number of active vertices was found') # Success. print('- the test ran to completion') print('\nResult: #s\n\n', pf)
def get_forecast_file(self, mkt): self.init_weather_data() # Copy weather data to predictedValues self.predictedValues = [] for ti in mkt.timeIntervals: # Find item which has the same timestamp as ti.timeStamp start_time = ti.startTime.replace(minute=0) 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(utils.format_timestamp(ti.startTime))) # Create interval value and add it to predicted values temp = items[0]['Value'] interval_value = IntervalValue(self, ti, mkt, MeasurementType.PredictedValue, temp) self.predictedValues.append(interval_value)
def schedule_power(self, mkt): # Determine powers of an asset in active time # intervals. NOTE that this method may be redefined by subclasses if more # features are needed. NOTE that this method name is common for all asset # and neighbor models to facilitate its redefinition. # # PRESUMPTIONS: # - Active time intervals exist and have been updated # - Marginal prices exist and have been updated. NOTE: Marginal prices # are not used for inelastic assets. # - Transition costs, if relevant, are applied during the scheduling # of assets. # - An engagement schedule, if used, is applied during an asset's power # scheduling. # - Scheduled power and engagement schedule must be self consistent at # the end of this method. That is, power should not be scheduled while # the asset is disengaged (uncommitted). # # INPUTS: # mkt - market object # # OUTPUTS: # - Updates self.scheduledPowers - the schedule of power consumed # - Updates self.engagementSchedule - an array that states whether the # asset is engaged (committed) (true) or not (false) in the time interval # Gather the active time intervals ti time_intervals = mkt.timeIntervals time_interval_values = [t.startTime for t in time_intervals] self.scheduledPowers = [ x for x in self.scheduledPowers if x.timeInterval.startTime in time_interval_values ] time_intervals.sort(key=lambda x: x.startTime) len_powers = len(self.default_powers) default_value = self.defaultPower # Index through the active time intervals ti for i in range(len(time_intervals)): # Check whether a scheduled power already exists for the indexed time interval iv = find_obj_by_ti(self.scheduledPowers, time_intervals[i]) # Reassign default value if there is power value for this time interval if len_powers > i: default_value = self.default_powers[i] if self.default_powers[ i] is not None else self.defaultPower if iv is None: # A scheduled power does not exist for the indexed time interval # Create an interval value and assign the default value iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.ScheduledPower, default_value) # Append the new scheduled power to the list of scheduled # powers for the active time intervals self.scheduledPowers.append(iv) else: # The scheduled power already exists for the indexed time # interval. Simply reassign its value iv.value = default_value # [avg.kW] sp = [(x.timeInterval.name, x.value) for x in self.scheduledPowers] _log.debug("{} asset model scheduledPowers are: {}".format( self.name, sp))
def test_calculate_reserve_margin(): # TEST_LAM_CALCULATE_RESERVE_MARGIN() - a LocalAssetModel ("LAM") class # method NOTE: Reserve margins are introduced but not fully integrated into # code in early template versions. # CASES: # 1. uses hard maximum if no active vertices exist # 2. vertices exist # 2.1 uses maximum vertex power if it is less than hard power constraint # 2.2 uses hard constraint if it is less than maximum vertex power # 2.3 upper flex power is greater than scheduled power assigns correct # positive reserve margin # 2.4 upperflex power less than scheduled power assigns zero value to # reserve margin. print('Running LocalAssetModel.test_calculate_reserve_margin()') pf = 'pass' # Establish test market test_mkt = Market() # Establish test market with an active time interval # Note: modified 1/29/18 due to new TimeInterval constructor dt = datetime.now() at = dt # NOTE: def Hours() corrects behavior of Matlab hours(). dur = timedelta(hours=1) mkt = test_mkt mct = dt # st = datetime(date) st = datetime.combine(date.today(), time()) ti = TimeInterval(at, dur, mkt, mct, st) # Store time interval test_mkt.timeIntervals = [ti] # Establish a test object that is a LocalAsset with assigned maximum power test_object = LocalAsset() test_object.maximumPower = 100 # Establish test object that is a LocalAssetModel test_model = LocalAssetModel() test_model.scheduledPowers = [ IntervalValue(test_model, ti, test_mkt, MeasurementType.ScheduledPower, 0.0)] # Allow object and model to cross-reference one another. test_object.model = test_model test_model.object = test_object # Run the first test case. test_model.calculate_reserve_margin(test_mkt) print('- method ran without errors') if len(test_model.reserveMargins) != 1: raise Exception('- an unexpected number of results were stored') else: print('- one reserve margin was stored, as expected') if test_model.reserveMargins[0].value != test_object.maximumPower: pf = 'fail' raise Exception('- the method did not use the available maximum power') else: print('- the method used maximum power value, as expected') # create some vertices and store them iv = [ IntervalValue(test_model, ti, test_mkt, MeasurementType.Vertex, Vertex(0, 0, -10)), IntervalValue(test_model, ti, test_mkt, MeasurementType.Vertex, Vertex(0, 0, 10)) ] test_model.activeVertices = iv # run test with maximum power greater than maximum vertex test_object.maximumPower = 100 test_model.calculate_reserve_margin(test_mkt) if test_model.reserveMargins[0].value != 10: pf = 'fail' raise Exception('- the method should have used vertex for comparison') else: print('- the method correctly chose to use the vertex power') # run test with maximum power less than maximum vertex test_object.maximumPower = 5 test_model.calculate_reserve_margin(test_mkt) if test_model.reserveMargins[0].value != 5: pf = 'fail' raise Exception('- method should have used maximum power for comparison') else: print('- the method properly chose to use the maximum power') # run test with scheduled power greater than maximum vertex test_model.scheduledPowers[0].value = 20 test_object.maximumPower = 500 test_model.calculate_reserve_margin(test_mkt) if test_model.reserveMargins[0].value != 0: pf = 'fail' raise Exception('- method should have assigned zero for a neg. result') else: print('- the method properly assigned 0 for a negative result') # Success. print('- the test ran to completion') print('\nResult: #s\n\n', pf)
def test_update_production_costs(): # TEST_UPDATE_PRODUCTION_COSTS() - test method update_production_costs() # that calculates production costs from active vertices and scheduled # powers. # NOTE: This test is virtually identical to the NeighborModel test of the # same name. print('Running LocalAssetModel.test_update_production_costs()') pf = 'pass' # Create a test Market object. test_market = Market # Create and store a TimeInterval object. dt = datetime.now() # datetime that may be used for most datetime arguments time_interval = TimeInterval(dt, timedelta(hours=1), test_market, dt, dt) test_market.timeIntervals = [time_interval] # Create a test LocalAssetModel object. test_model = LocalAssetModel() # Create and store a scheduled power IntervalValue in the active time # interval. test_model.scheduledPowers = [ IntervalValue(test_model, time_interval, test_market, MeasurementType.ScheduledPower, 50)] # Create and store some active vertices IntervalValue objects in the # active time interval. vertices = [ Vertex(0.1, 1000, 0), Vertex(0.2, 1015, 100) ] interval_values = [ IntervalValue(test_model, time_interval, test_market, MeasurementType.ActiveVertex, vertices[0]), IntervalValue(test_model, time_interval, test_market, MeasurementType.ActiveVertex, vertices[1]) ] test_model.activeVertices = interval_values # TEST 1 print('- Test 1: First calculation of a production cost') test_model.update_production_costs(test_market) print(' - the method ran without errors') if len(test_model.productionCosts) != 1: pf = 'fail' print(' - the wrong number of production costs was created') else: print(' - the right number of production cost values was created') production_cost = test_model.productionCosts[0].value if float(production_cost) != float(1007.5): pf = 'fail' print(' - an unexpected production cost value was found') else: print(' - the expected production cost value was found') # TEST 2 print('- Test 2: Reassignment of an existing production cost') # Configure the test by modifying the scheduled power value. test_model.scheduledPowers[0].value = 150 test_model.update_production_costs(test_market) print(' - the method ran without errors') if len(test_model.productionCosts) != 1: pf = 'fail' print(' - the wrong number of productions was created') else: print(' - the right number of production cost values was created') production_cost = test_model.productionCosts[0].value if float(production_cost) != float(1015): pf = 'fail' print(' - an unexpected dual cost value was found') else: print(' - the expected dual cost value was found') # Success. print('- the test ran to completion') print('\nResult: #s\n\n', pf)
def test_prod_cost_from_vertices(): from local_asset_model import LocalAssetModel # TEST_PROD_COST_FROM_VERTICES - tests function prod_cost_from_vertices() print('Running test_prod_cost_from_vertices()') pf = 'pass' # Create a test object test_object = LocalAssetModel # Create a test market test_market = Market # Create several active vertices av av = [Vertex(0.02, 5, 0), Vertex(0.02, 7, 100), Vertex(0.025, 9.25, 200)] # Create a time interval dt = datetime.now() at = dt # NOTE: Function Hours() corrects behavior of Matlab function hours(). dur = timedelta(hours=1) mkt = test_market mct = dt st = dt ti = TimeInterval(at, dur, mkt, mct, st) # Create and store the activeVertices, which are IntervalValues test_object.activeVertices = [ IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[0]), IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[1]), IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[2]) ] ## CASE: Various signed powers when there is more than one vertex test_powers = [-50, 0, 50, 150, 250] pc = [] for p in test_powers: pc.append(prod_cost_from_vertices(test_object, ti, p)) # pc(1) = 0: value is always 0 for power < 0 # pc(2) = 5.0: assign cost from first vertex # pc(3) = 6.0: interpolate between vertices # pc(4) = 8.125: interpolate between vertices # pc(5) = 9.25: use last vertex cost if power > last vertex power #if ~all(pc == [0, 5.0, 6.0, 8.125, 9.25]) expected = [0, 5.0, 6.0, 8.125, 9.25] if not all([pc[i] == expected[i] for i in range(len(pc))]): pf = 'fail' raise Exception('- the production cost was incorrectly calculated') else: print('- the production cost was correctly calculated') ## CASE: One vertex (inelastic case, a constant) test_object.activeVertices = [ IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[0]) ] #pc[i] = prod_cost_from_vertices(test_object, ti, test_powers[i]) pc = [] for p in test_powers: pc.append(prod_cost_from_vertices(test_object, ti, p)) expected = [0.0, 5.0, 5.0, 5.0, 5.0] #if ~all(pc == [0.0, 5.0, 5.0, 5.0, 5.0]) if not all([pc[i] == expected[i] for i in range(len(pc))]): pf = 'fail' raise Exception( '- made an incorrect assignment when there is one vertex') else: print('- made a correct assignment when there is one vertex') ## CASE: No active vertices (error case): test_object.activeVertices = [] #print('off', 'all') try: pc = prod_cost_from_vertices(test_object, ti, test_powers[4]) pf = 'fail' #print('on', 'all') raise Exception( '- the function should have warned and continued when there were no active vertices' ) except: print( '- the function returned gracefully when there were no active vertices' ) #print('on', 'all') # Success print('- the test function ran to completion') print('Result: #s\n\n', pf)
def update_production_costs(self, mkt): # Calculate the costs of generated energies. # (NOTE: Choosing not to separate this def from the base class because # cost might need to be handled differently and redefined in subclasses.) # Gather active time intervals ti time_intervals = mkt.timeIntervals time_interval_values = [t.startTime for t in time_intervals] self.productionCosts = [ x for x in self.productionCosts if x.timeInterval.startTime in time_interval_values ] # Index through the active time interval ti for i in range(1, len(time_intervals)): # Get the scheduled power sp in the indexed time interval sp = find_obj_by_ti(self.scheduledPowers, time_intervals[i]) sp = sp.value # schedule power [avg.kW] # Call on def that calculates production cost pc based on the # vertices of the supply or demand curve # NOTE that this def is now stand-alone because it might be # generally useful for a number of models. pc = prod_cost_from_vertices(self, time_intervals[i], sp) # interval production cost [$] # Check for a transition cost in the indexed time interval. # (NOTE: this differs from neighbor models, which do not posses the # concept of commitment and engagement. This is a good reason to keep # this method within its base class to allow for subtle differences.) tc = find_obj_by_ti(self.transitionCosts, time_intervals[i]) if tc is None: tc = 0.0 # [$] else: tc = tc.value # [$] # Add the transition cost to the production cost pc = pc + tc # Check to see if the production cost value has been defined for the # indexed time interval iv = find_obj_by_ti(self.productionCosts, time_intervals[i]) if iv is None: # The production cost value has not been defined in the indexed # time interval. Create it and assign its value pc. iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.ProductionCost, pc) # Append the production cost to the list of active production # cost values self.productionCosts.append(iv) # IntervalValues else: # The production cost value already exists in the indexed time # interval. Simply reassign its value. iv.value = pc # interval production cost [$] # Ensure that only active time intervals are in the list of active # production costs apc #self.productionCosts = [x for x in self.productionCosts if x.timeInterval in time_intervals] # Sum the total production cost self.totalProductionCost = sum([x.value for x in self.productionCosts ]) # total production cost [$] pc = [(x.timeInterval.name, x.value) for x in self.productionCosts] _log.debug("{} asset model production costs are: {}".format( self.name, pc))
def assign_transition_costs(self, mkt): # Assign the cost of changeing # engagement state from the prior to the current time interval # # PRESUMPTIONS: # - Time intervals exist and have been updated # - The engagement schedule exists and has been updated. Contents are # logical [true/false]. # - Engagement costs have been accurately assigned for [disengagement, # unchanged, engagement] # # INPUTS: # mkt - Market object # # USES: # - self.engagementCost - three costs that correspond to # [disengagement, unchanged, engagement) transitions # - self.engagement_cost() - assigns appropriate cost from # self.engagementCost property # - self.engagementSchedule - engagement states (true/false) for the asset # in active time intervals # # OUTPUTS: # Assigns values to self.transition_costs # Gather active time intervals time_intervals = mkt.timeIntervals time_interval_values = [t.startTime for t in time_intervals] self.transitionCosts = [ x for x in self.transitionCosts if x.timeInterval.startTime in time_interval_values ] # Ensure that ti is ordered by time interval start times time_intervals.sort(key=lambda x: x.startTime) # Index through all but the first time interval ti for i in range(len(time_intervals)): # Find the current engagement schedule ces in the current indexed # time interval ti(i) ces = [ x for x in self.engagementSchedule if x.timeInterval.startTime == time_intervals[i].startTime ] # Extract its engagement state ces = ces[0].value # logical (true/false) # Find the engagement schedule pes in the prior indexed time interval ti(i-1) pes = [ x for x in self.engagementSchedule if x.timeInterval.startTime == time_intervals[i - 1].startTime ] # And extract its value pes = pes[0].value # logical (true/false) # Calculate the state transition # - -1:Disengaging # - 0:Unchaged # - 1:Engaging dif = ces - pes # Assign the corresponding transition cost val = self.engagement_cost(dif) # Check whether a transition cost exists in the indexed time interval iv = find_obj_by_ti(self.transitionCosts, time_intervals[i]) if iv is None: # No transition cost was found in the indexed time interval. # Create an interval value and assign its value. iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.TransitionCost, val) # Append the interval value to the list of active interval values self.transitionCosts.append(iv) else: # A transition cost was found in the indexed time interval. # Simpy reassign its value. iv.value = val # [$]
def update_supply_demand(self, mtn): # For each time interval, sum the power that is generated, imported, # consumed, or exported for all modeled local resources, neighbors, and # local load. # Extract active time intervals time_intervals = self.timeIntervals # active TimeIntervals time_interval_values = [t.startTime for t in time_intervals] # Delete netPowers not in active time intervals self.netPowers = [x for x in self.netPowers if x.timeInterval.startTime in time_interval_values] # Index through the active time intervals ti for i in range(1, len(time_intervals)): # Initialize total generation tg tg = 0.0 # [avg.kW] # Initialize total demand td td = 0.0 # [avg.kW] # Index through local asset models m. m = mtn.localAssets for k in range(len(m)): mo = find_obj_by_ti(m[k].model.scheduledPowers, time_intervals[i]) # Extract and include the resource's scheduled power p = mo.value # [avg.kW] if p > 0: # Generation # Add positive powers to total generation tg tg = tg + p # [avg.kW] else: # Demand # Add negative powers to total demand td td = td + p # [avg.kW] # Index through neighbors m m = mtn.neighbors for k in range(len(m)): # Find scheduled power for this neighbor in the indexed time interval mo = find_obj_by_ti(m[k].model.scheduledPowers, time_intervals[i]) # Extract and include the neighbor's scheduled power p = mo.value # [avg.kW] if p > 0: # Generation # Add positive power to total generation tg tg = tg + p # [avg.kW] else: # Demand # Add negative power to total demand td td = td + p # [avg.kW] # At this point, total generation and importation tg, and total # demand and exportation td have been calculated for the indexed # time interval ti[i] # Save the total generation in the indexed time interval # Check whether total generation exists for the indexed time interval iv = find_obj_by_ti(self.totalGeneration, time_intervals[i]) if iv is None: # No total generation was found in the indexed time interval. # Create an interval value. iv = IntervalValue(self, time_intervals[i], self, MeasurementType.TotalGeneration, tg) # an IntervalValue # Append the total generation to the list of total generations self.totalGeneration.append(iv) else: # Total generation exists in the indexed time interval. Simply # reassign its value. iv.value = tg # Calculate and save total demand for this time interval. # NOTE that this formulation includes both consumption and # exportation among total load. # Check whether total demand exists for the indexed time interval iv = find_obj_by_ti(self.totalDemand, time_intervals[i]) if iv is None: # No total demand was found in the indexed time interval. Create # an interval value. iv = IntervalValue(self, time_intervals[i], self, MeasurementType.TotalDemand, td) # an IntervalValue # Append the total demand to the list of total demands self.totalDemand.append(iv) else: # Total demand was found in the indexed time interval. Simply reassign it. iv.value = td # Update net power for the interval # Net power is the sum of total generation and total load. # By convention generation power is positive and consumption # is negative. # Check whether net power exists for the indexed time interval iv = find_obj_by_ti(self.netPowers, time_intervals[i]) if iv is None: # Net power is not found in the indexed time interval. Create an interval value. iv = IntervalValue(self, time_intervals[i], self, MeasurementType.NetPower, tg + td) # Append the net power to the list of net powers self.netPowers.append(iv) else: # A net power was found in the indexed time interval. Simply reassign its value. iv.value = tg + td np = [(x.timeInterval.name, x.value) for x in self.netPowers] _log.debug("{} market netPowers are: {}".format(self.name, np))
def test_update_dc_threshold(): print('Running BulkSupplier_dc.test_update_dc_threshold()') pf = 'pass' ## Basic configuration for tests: # Create a test object and initialize demand-realted properties test_obj = BulkSupplier_dc() test_obj.demandMonth = datetime.now().month # month(datetime) test_obj.demandThreshold = 1000 # Create a test market test_mkt = Market() # Create and store two time intervals dt = datetime.now() at = dt dur = timedelta(hours=1) # Hours(1) mkt = test_mkt mct = dt st = dt ti = [TimeInterval(at, dur, mkt, mct, st)] st = st + dur ti.append(TimeInterval(at, dur, mkt, mct, st)) test_mkt.timeIntervals = ti ## Test case when there is no MeterPoint object test_obj.demandThreshold = 1000 test_obj.demandMonth = datetime.now().month # month(datetime) test_obj.meterPoints = [] # MeterPoint.empty # Create and store a couple scheduled powers # iv(1) = IntervalValue(test_obj, ti[0], test_mkt, MeasurementType.ScheduledPower, 900) # iv(2) = IntervalValue(test_obj, ti[1], test_mkt, MeasurementType.ScheduledPower, 900) iv = [ IntervalValue(test_obj, ti[0], test_mkt, MeasurementType.ScheduledPower, 900), IntervalValue(test_obj, ti[1], test_mkt, MeasurementType.ScheduledPower, 900) ] test_obj.scheduledPowers = iv try: test_obj.update_dc_threshold(test_mkt) print('- the method ran without errors') except: pf = 'fail' print('- the method encountered errors when called') if test_obj.demandThreshold != 1000: pf = 'fail' print('- the method inferred the wrong demand threshold value') else: print('- the method properly kept the old demand threshold value with no meter') iv = [ IntervalValue(test_obj, ti[0], test_mkt, MeasurementType.ScheduledPower, 1100), IntervalValue(test_obj, ti[1], test_mkt, MeasurementType.ScheduledPower, 900) ] test_obj.scheduledPowers = iv try: test_obj.update_dc_threshold(test_mkt) print('- the method ran without errors when there is no meter') except: pf = 'fail' print('- the method encountered errors when there is no meter') if test_obj.demandThreshold != 1100: pf = 'fail' print('- the method did not update the inferred demand threshold value') else: print('- the method properly updated the demand threshold value with no meter') ## Test with an appropriate MeterPoint meter # Create and store a MeterPoint test object test_mtr = MeterPoint() test_mtr.measurementType = MeasurementType.AverageDemandkW test_mtr.currentMeasurement = 900 test_obj.meterPoints = [test_mtr] # Reconfigure the test object for this test: iv = [ IntervalValue(test_obj, ti[0], test_mkt, MeasurementType.ScheduledPower, 900), IntervalValue(test_obj, ti[1], test_mkt, MeasurementType.ScheduledPower, 900) ] test_obj.scheduledPowers = iv test_obj.demandThreshold = 1000 test_obj.demandMonth = datetime.now().month # Run the test. Confirm it runs. try: test_obj.update_dc_threshold(test_mkt) print('- the method ran without errors when there is a meter') except: pf = 'fail' print('- the method encountered errors when there is a meter') # Check that the old threshold is correctly retained. if test_obj.demandThreshold != 1000: pf = 'fail' print('- the method failed to keep the correct demand threshold value when there is a meter') else: print('- the method properly kept the old demand threshold value when there is a meter') # Reconfigure the test object with a lower current threshold iv = [ IntervalValue(test_obj, ti[0], test_mkt, MeasurementType.ScheduledPower, 900), IntervalValue(test_obj, ti[1], test_mkt, MeasurementType.ScheduledPower, 900) ] test_obj.scheduledPowers = iv test_obj.demandThreshold = 800 # Run the test. test_obj.update_dc_threshold(test_mkt) # Check that a new, higher demand threshold was set. if test_obj.demandThreshold != 900: pf = 'fail' print(['- the method failed to update the demand threshold value when there is a meter']) else: print('- the method properly updated the demand threshold value when there is a meter') ## Test rollover to new month # Configure the test object test_obj.demandMonth = dt + relativedelta.relativedelta(months=-1) # month(datetime - days(31)) # prior month test_obj.demandThreshold = 1000 test_obj.scheduledPowers[0].value = 900 test_obj.scheduledPowers[1].value = 900 test_obj.meterPoints = [] # MeterPoint.empty # Run the test test_obj.update_dc_threshold(test_mkt) # See if the demand threshold was reset at the new month. if test_obj.demandThreshold != 0.8 * 1000: pf = 'fail' print('- the method did not reduce the threshold properly in a new month') else: print('- the method reduced the threshold properly in a new month') # Success print('- the test ran to completion') print('Result: #s\n\n', pf)
def check_marginal_prices(self): # Check that marginal prices exist for active time intervals. If they do # not exist for a time interval, choose from these alternatives that are # ordered from best to worst: # (1) initialize the marginal price from that of the preceding interval. # (2) use the default marginal price. # OUTPUTS: # populates list of active marginal prices (see class IntervalValue) ti = self.timeIntervals # Clean up the list of active marginal prices. Remove any active # marginal prices that are not in active time intervals. self.marginalPrices = [x for x in self.marginalPrices if x.timeInterval in ti] # Index through active time intervals ti for i in range(len(ti)): # Check to see if a marginal price exists in the active time interval iv = find_obj_by_ti(self.marginalPrices, ti[i]) if iv is None: # No marginal price was found in the indexed time interval. Is # a marginal price defined in the preceding time interval? # Extract the starting time st of the currently indexed time interval st = ti[i].startTime # Calculate the starting time st of the previous time interval st = st - ti[i].duration # Find the prior active time interval pti that has this # calculated starting time pti = find_obj_by_st(self.timeIntervals, st) # prior time interval # Initialize previous marginal price value pmp as an empty set pmp = None # prior marginal prices if pti is not None: # There is an active preceding time interval. Check whether # there is an active marginal price in the previous time interval. pmp = find_obj_by_ti(self.marginalPrices, pti) if pmp is None: # No marginal price was found in the previous time interval # either. Assign the marginal price from a default value. value = self.defaultPrice # [$/kWh] else: # A marginal price value was found in the previous time # interval. Use that marginal price. value = pmp.value # [$/kWh] # Create an interval value for the new marginal price in the # indexed time interval with either the default price or the # marginal price from the previous active time interval. iv = IntervalValue(self, ti[i], self, MeasurementType.MarginalPrice, value) # Append the marginal price value to the list of active marginal prices self.marginalPrices.append(iv)
def calculate_blended_prices(self): # Calculate the blended prices for active time intervals. # # The blended price is the averaged weighted price of all locally # generated and imported energies. A sum is made of all costs of # generated and imported energies, which are prices weighted by their # corresponding energy. This sum is divided by the total generated and # imported energy to get the average. # # The blended price does not include supply surplus and may therefore be # a preferred representation of price for local loads and friendly # neighbors, for which myTransactiveNode is not competitive and # profit-seeking. # Update and gather active time intervals ti. It's simpler to # recalculate the active time intervals than it is to check for # errors. self.check_intervals() ti = self.timeIntervals # Gather primal production costs of the time intervals. pc = self.productionCosts # Perform checks on interval primal production costs to ensure smooth # calculations. NOTE: This does not check the veracity of the # primal costs. # CASE 1: No primal production costs have been populated for the various # assets and neighbors. This results in termination of the # process. if pc is None or len(pc) == 0: # isempty(pc) _log.warning('Primal costs have not yet been calculated.') return # CASE 2: There is at least one active time interval for which primal # costs have not been populated. This results in termination of the # process. elif len(ti) > len(pc): _log.warning('Missing primal costs for active time intervals.') return # CASE 3: There is at least one extra primal production cost that does # not refer to an active time interval. It will be removed. elif len(ti) < len(pc): _log.warning('Removing primal costs that are not among active time intervals.') self.productionCosts = [x for x in self.productionCosts if x.timeInterval in self.timeIntervals] for i in range(len(ti)): pc = find_obj_by_ti(self.productionCosts, ti[i]) tg = find_obj_by_ti(self.totalGeneration, ti[i]) bp = pc / tg self.blendedPrices1 = [x for x in self.blendedPrices1 if x != ti[i]] val = bp iv = IntervalValue(self, ti[i], self, MeasurementType.BlendedPrice, val) # Append the blended price to the list of interval values self.blendedPrices1.append(iv)
def test_production(): from local_asset_model import LocalAssetModel from market import Market print('Running test_production()') pf = 'pass' # Create a test object test_object = LocalAssetModel() # Create a test market test_market = Market() # Create several active vertices av av = [ Vertex(0.0200, 5.00, 0.0), Vertex(0.0200, 7.00, 100.0), Vertex(0.0250, 9.25, 200.0) ] # Create a time interval ti dt = datetime.now() at = dt # NOTE: Function Hours() corrects the behavior of Matlab hours(). dur = timedelta(hours=1) mkt = test_market mct = dt st = dt ti = TimeInterval(at, dur, mkt, mct, st) # Assign activeVertices, which are IntervalValues test_object.activeVertices = [ IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[0]), IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[1]), IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[2]) ] ## CASE: Various marginal prices when there is more than one vertex test_prices = [-0.010, 0.000, 0.020, 0.0225, 0.030] p = [0] * len(test_prices) #zeros(1, length(test_prices)) for i in range(len(test_prices)): #for i = 1:length(test_prices) p[i] = production(test_object, test_prices[i], ti) print('- the function ran without errors') # p(1) = 0: below first vertex # p(2) = 0: below first vertex # p(3) = 100: at first vertex, which has identical marginal price as second # p(4) = 150: interpolate between vertices # p(5) = 200: exceeds last vertex #if ~all(abs(p - [0, 0, 100, 150, 200]) < 0.001): expected = [0, 0, 100, 150, 200] if not all([p[i] - expected[i] < 0.001 for i in range(len(p))]): pf = 'fail' raise Exception('- the production cost was incorrectly calculated') else: print('- the production cost was correctly calculated') ## CASE: One vertex (inelastic case, a constant) test_object.activeVertices = [ IntervalValue(test_object, ti, test_market, MeasurementType.ActiveVertex, av[2]) ] for i in range(5): p[i] = production(test_object, test_prices[i], ti) #if ~all(p == 200 * ones(1, length(p))): if not all(x == 200 for x in p): pf = 'fail' raise Exception( 'the vertex power should be assigned when there is one vertex') else: print('- the correct power was assigned when there is one vertex') ## CASE: No active vertices (error case): test_object.activeVertices = [] try: p = production(test_object, test_prices[4], ti) pf = 'fail' raise Exception( '- an error should have occurred with no active vertices') except: print('- with no vertices, system returned with warnings, as expected') # Success print('- the test function ran to completion') print('Result: #s\n\n', pf)
def test_schedule_power(cls): print('Running test_schedule_power()') pf = 'pass' # Create a test Market object. test_mkt = Market() # Create and store a couple TimeInterval objects at a known date and # time. dt = datetime(2017, 11, 1, 12, 0, 0) # Wednesday Nov. 1, 2017 at noon at = dt dur = timedelta(hours=1) mkt = test_mkt mct = dt st = dt test_intervals = [TimeInterval(at, dur, mkt, mct, st)] st = st + dur # 1p on the same day test_intervals.append(TimeInterval(at, dur, mkt, mct, st)) test_mkt.timeIntervals = test_intervals # Create a test TemperatureForecastModel object and give it some # temperature values in the test TimeIntervals. # test_forecast = TemperatureForecastModel # # The information type should be specified so the test object will # # correctly identivy it. # test_forecast.informationType = 'temperature' # # test_forecast.update_information(test_mkt) # test_forecast.predictedValues(1) = IntervalValue(test_forecast, test_intervals(1), test_mkt, 'Temperature', 20) # Heating regime # test_forecast.predictedValues(2) = IntervalValue(test_forecast, test_intervals(2), test_mkt, 'Temperature', 100) # Cooling regime # test_obj.informationServiceModels = {test_forecast} # Create a OpenLoopRichlandLoadPredictor test object. test_forecast = TemperatureForecastModel() test_forecast.informationType = 'temperature' test_forecast.predictedValues = [ IntervalValue(test_forecast, test_intervals[0], test_mkt, MeasurementType.Temperature, 20), # Heating regime IntervalValue(test_forecast, test_intervals[1], test_mkt, MeasurementType.Temperature, 100) # Cooling regime ] test_obj = OpenLoopRichlandLoadPredictor(test_forecast) # Manually evaluate from the lookup table and the above categorical inputs # DOW = Wed. ==> Intercept1 = 146119 Intercept2 = 18836 Intercept3 = -124095 Factor1 = -1375 Factor2 = 1048 Temperature1 = 20 Temperature2 = 100 LOAD = [ -(Intercept1 + Intercept2 + Factor1 * Temperature1), -(Intercept1 + Intercept3 + Factor2 * Temperature2) ] try: test_obj.schedule_power(test_mkt) print('- the method ran without errors') except: pf = 'fail' _log.warning('- the method had errors when called') #if any(abs([test_obj.scheduledPowers(1: 2).value] - [LOAD])) > 5 if any([ abs(test_obj.scheduledPowers[i].value - LOAD[i]) > 5 for i in range(len(test_obj.scheduledPowers)) ]): pf = 'fail' _log.warning('- the calculated powers were not as expected') else: print('- the calculated powers were as expected') # Success print('- the test ran to completion') print('Result: #s\n\n', pf)
def calculate_reserve_margin(self, mkt): # Estimate available (spinning) reserve margin for this asset. # # NOTES: # This method works with the simplest base classes that have constant # power and therefore provide no spinning reserve. This method may be # redefined by subclasses of the local asset model to add new features # or capabilities. # This calculation will be more meaningful and useful after resource # commitments and uncertainty estimates become implemented. Until then, # reserve margins may be tracked, even if they are not used. # # PRESUMPTIONS: # - Active time intervals exist and have been updated # - The asset's maximum power is a meaningful and accurate estimate of # the maximum power level that can be achieved on short notice, i.e., # spinning reserve. # # INPUTS: # mkt - market object # # OUTPUTS: # Modifies self.reserveMargins - an array of estimated (spinning) reserve # margins in active time intervals # Gather the active time intervals ti time_intervals = mkt.timeIntervals # active TimeIntervals time_interval_values = [t.startTime for t in time_intervals] self.reserveMargins = [ x for x in self.reserveMargins if x.timeInterval.startTime in time_interval_values ] # Index through active time intervals ti for i in range(len(time_intervals)): # Calculate the reserve margin for the indexed interval. This is the # non-negative difference between the maximum asset power and the # scheduled power. In principle, generation may be increased or # demand decreased by this quantity to act as spinning reserve. # Find the scheduled power in the indexed time interval iv = find_obj_by_ti(self.scheduledPowers, time_intervals[i]) # Calculate the reserve margin rm in the indexed time interval. The # reserve margin is the differnce between the maximum operational # power value in the interval and the scheduled power. The # operational maximum should be less than the object's hard physical # power constraint, so a check is in order. # start with the hard physical constraint. hard_const = self.object.maximumPower # [avg.kW] # Calculate the operational maximum constraint, which is the highest # point on the supply/demand curve (i.e., the vertex) that represents # the residual flexibility of the asset in the time interval. op_const = find_objs_by_ti(self.activeVertices, time_intervals[i]) if len(op_const) == 0: op_const = hard_const else: op_const = [x.value for x in op_const] # active vertices op_const = max([x.power for x in op_const ]) # operational max. power[avg.kW] # Check that the upper operational power constraint is less than or # equal to the object's hard physical constraint. soft_maximum = min(hard_const, op_const) # [avg.kW] # And finally calculate the reserve margin. rm = max(0, soft_maximum - iv.value) # reserve margin [avg. kW] # Check whether a reserve margin already exists for the indexed time interval iv = find_obj_by_ti(self.reserveMargins, time_intervals[i]) # an IntervalValue if iv is None: # A reserve margin does not exist for the indexed time interval. # create it. (See IntervalValue class.) iv = IntervalValue(self, time_intervals[i], mkt, MeasurementType.ReserveMargin, rm) # an IntervalValue # Append the new reserve margin interval value to the list of # reserve margins for the active time intervals self.reserveMargins.append(iv) else: # The reserve margin already exists for the indexed time # interval. Simply reassign its value. iv.value = rm # reserve margin [avg.kW]
def schedule_power(self, mkt): # Estimate stochastic generation from a solar # PV array as a function of time-of-day and a cloud-cover factor. # INPUTS: # obj - SolarPvResourceModel class object # tod - time of day # OUTPUTS: # p - calcalated maximum power production at this time of day # LOCAL: # h - hour (presumes 24-hour clock, local time) # ************************************************************************* # Gather active time intervals tis = mkt.timeIntervals # Index through the active time intervals ti for ti in tis: # Production will be estimated from the time-of-day at the center of # the time interval. tod = ti.startTime + ti.duration / 2 # a datetime # extract a fractional representation of the hour-of-day h = tod.hour m = tod.minute h = h + m / 60 # TOD stated as fractional hours # Estimate solar generation as a sinusoidal function of daylight hours. if h < 5.5 or h > 17.5: # The time is outside the time of solar production. Set power to zero. p = 0.0 # [avg.kW] else: # A sinusoidal function is used to forecast solar generation # during the normally sunny part of a day. p = 0.5 * (1 + math.cos((h - 12) * 2.0 * math.pi / 12)) p = self.object.maximumPower * p p = self.cloudFactor * p # [avg.kW] # Check whether a scheduled power exists in the indexed time interval. iv = find_obj_by_ti(self.scheduledPowers, ti) if iv is None: # No scheduled power value is found in the indexed time interval. # Create and store one. iv = IntervalValue(self, ti, mkt, MeasurementType.ScheduledPower, p) # Append the scheduled power to the list of scheduled powers. self.scheduledPowers.append(iv) else: # A scheduled power already exists in the indexed time interval. # Simply reassign its value. iv.value = p # [avg.kW] # Assign engagement schedule in the indexed time interval # NOTE: The assignment of engagement schedule, if used, will often be # assigned during the scheduling of power, not separately as # demonstrated here. # Check whether an engagement schedule exists in the indexed time interval iv = find_obj_by_ti(self.engagementSchedule, ti) # NOTE: this template assigns engagement value as true (i.e., engaged). val = True # Asset is committed or engaged if iv is None: # No engagement schedule was found in the indexed time interval. # Create an interval value and assign its value. iv = IntervalValue(self, ti, mkt, MeasurementType.EngagementSchedule, val) # an IntervalValue # Append the interval value to the list of active interval values self.engagementSchedule.append(iv) else: # An engagement schedule was found in the indexed time interval. # Simpy reassign its value. iv.value = val # [$] # Remove any extra scheduled powers self.scheduledPowers = [ x for x in self.scheduledPowers if x.timeInterval in tis ] # Remove any extra engagement schedule values self.engagementSchedule = [ x for x in self.engagementSchedule if x.timeInterval in tis ]
def test_update_costs(): print('Running AbstractModel.test_update_costs()') pf = 'pass' # Create a test market test_mkt test_mkt = Market() # Create a sample time interval ti dt = datetime.now() at = dt # NOTE: Function Hours() corrects behavior of Matlab hours(). dur = timedelta(hours=1) mkt = test_mkt mct = dt st = datetime.combine(date.today(), time()) + timedelta(hours=20) ti = TimeInterval(at, dur, mkt, mct, st) # Save the time interval test_mkt.timeIntervals = [ti] # Assign a marginal price in the time interval test_mkt.check_marginal_prices() # Create a Neighbor test object and give it a default maximum power value test_obj = Neighbor() # test_obj.maximumPower = 100 # Create a corresponding NeighborModel test_mdl = NeighborModel() # Make sure that the model and object cross-reference one another test_obj.model = test_mdl test_mdl.object = test_obj test_mdl.scheduledPowers = [ IntervalValue(test_mdl, ti, test_mkt, MeasurementType.ScheduledPower, 100) ] test_mdl.activeVertices = [ IntervalValue(test_mdl, ti, test_mkt, MeasurementType.ActiveVertex, Vertex(0.05, 0, 100)) ] # Run a test with a NeighborModel object print('- running test with a NeighborModel:') try: test_mdl.update_costs(test_mkt) print(' - the method encountered no errors') except: pf = 'fail' raise ' - the method did not run without errors' if len(test_mdl.productionCosts) != 1: pf = 'fail' raise ' - the method did not store a production cost' else: print(' - the method calculated and stored a production cost') if len(test_mdl.dualCosts) != 1: pf = 'fail' raise ' - the method did not store a dual cost' else: print(' - the method stored a dual cost') if test_mdl.totalProductionCost != sum( [x.value for x in test_mdl.productionCosts]): pf = 'fail' raise ' - the method did not store a total production cost' else: print(' - the method stored an total production cost') if test_mdl.totalDualCost != sum([x.value for x in test_mdl.dualCosts]): pf = 'fail' raise ' - the method did not store a total dual cost' else: print(' - the method stored an total dual cost') # Run a test again with a LocalAssetModel object test_obj = LocalAsset() # test_obj.maximumPower = 100 test_mdl = LocalAssetModel() test_obj.model = test_mdl test_mdl.object = test_obj test_mdl.scheduledPowers = [ IntervalValue(test_mdl, ti, test_mkt, MeasurementType.ScheduledPower, 100) ] test_mdl.activeVertices = [ IntervalValue(test_mdl, ti, test_mkt, MeasurementType.ActiveVertex, Vertex(0.05, 0, 100)) ] print('- running test with a LocalAssetModel:') try: test_mdl.update_costs(test_mkt) print(' - the method encountered no errors') except: pf = 'fail' raise ' - the method did not run without errors' if len(test_mdl.productionCosts) != 1: pf = 'fail' raise ' - the method did not store a production cost' else: print(' - the method calculated and stored a production cost') if len(test_mdl.dualCosts) != 1: pf = 'fail' raise ' - the method did not store a dual cost' else: print(' - the method stored a dual cost') if test_mdl.totalProductionCost != sum( [x.value for x in test_mdl.productionCosts]): pf = 'fail' raise ' - the method did not store a total production cost' else: print(' - the method stored an total production cost') if test_mdl.totalDualCost != sum([x.value for x in test_mdl.dualCosts]): pf = 'fail' raise ' - the method did not store a total dual cost' else: print(' - the method stored an total dual cost') # Success print('- the test ran to completion') print('Result: %s', pf)
def test_update_dual_costs(): # TEST_UPDATE_DUAL_COSTS() - test method update_dual_costs() that creates # or revises the dual costs in active time intervals using active vertices, # scheduled powers, and marginal prices. # NOTE: This test is virtually identical to the NeighborModel test of the # same name. print('Running LocalAssetModel.test_update_dual_costs()') pf = 'pass' # Create a test Market object. test_market = Market() # Create and store a TimeInterval object. dt = datetime.now() # datetime that may be used for most datetime arguments time_interval = TimeInterval(dt, timedelta(hours=1), test_market, dt, dt) test_market.timeIntervals = [time_interval] # Create and store a marginal price IntervalValue object. test_market.marginalPrices = [ IntervalValue(test_market, time_interval, test_market, MeasurementType.MarginalPrice, 0.1)] # Create a test LocalAssetModel object. test_model = LocalAssetModel() # Create and store a scheduled power IntervalValue in the active time # interval. test_model.scheduledPowers = [ IntervalValue(test_model, time_interval, test_market, MeasurementType.ScheduledPower, 100)] # Create and store a production cost IntervalValue object in the active # time interval. test_model.productionCosts = [ IntervalValue(test_model, time_interval, test_market, MeasurementType.ProductionCost, 1000)] # TEST 1 print('- Test 1: First calculation of a dual cost') test_model.update_dual_costs(test_market) print(' - the method ran without errors') if len(test_model.dualCosts) != 1: pf = 'fail' print(' - the wrong number of dual cost values was created') else: print(' - the right number of dual cost values was created') dual_cost = test_model.dualCosts[0].value if dual_cost != (1000 - 100 * 0.1): pf = 'fail' print(' - an unexpected dual cost value was found') else: print(' - the expected dual cost value was found') # TEST 2 print('- Test 2: Reassignment of an existing dual cost') # Configure the test by modifying the marginal price value. test_market.marginalPrices[0].value = 0.2 test_model.update_dual_costs(test_market) print(' - the method ran without errors') if len(test_model.dualCosts) != 1: pf = 'fail' print(' - the wrong number of dual cost values was created') else: print(' - the right number of dual cost values was created') dual_cost = test_model.dualCosts[0].value if dual_cost != (1000 - 100 * 0.2): pf = 'fail' print(' - an unexpected dual cost value was found') else: print(' - the expected dual cost value was found') # Success. print('- the test ran to completion') print('\nResult: #s\n\n', pf)
def update_costs(self, mtn): # Sum the production and dual costs from all modeled local resources, local # loads, and neighbors, and then sum them for the entire duration of the # time horizon being calculated. # # PRESUMPTIONS: # - Dual costs have been created and updated for all active time # intervals for all neighbor objects # - Production costs have been created and updated for all active time # intervals for all asset objects # # INTPUTS: # mtn - my Transactive Node object # # OUTPUTS: # - Updates Market.productionCosts - an array of total production cost in # each active time interval # - Updates Market.totalProductionCost - the sum of production costs for # the entire future time horizon of active time intervals # - Updates Market.dualCosts - an array of dual cost for each active time # interval # - Updates Market.totalDualCost - the sum of all the dual costs for the # entire future time horizon of active time intervals # Call each LocalAssetModel to update its costs for la in mtn.localAssets: la.model.update_costs(self) # Call each NeighborModel to update its costs for n in mtn.neighbors: n.model.update_costs(self) for i in range (1, len(self.timeIntervals)): ti = self.timeIntervals[i] # Initialize the sum dual cost sdc in this time interval sdc = 0.0 # [$] # Initialize the sum production cost spc in this time interval spc = 0.0 # [$] for la in mtn.localAssets: iv = find_obj_by_ti(la.model.dualCosts, ti) sdc = sdc + iv.value # sum dual cost [$] iv = find_obj_by_ti(la.model.productionCosts, ti) spc = spc + iv.value # sum production cost [$] for n in mtn.neighbors: iv = find_obj_by_ti(n.model.dualCosts, ti) sdc = sdc + iv.value # sum dual cost [$] iv = find_obj_by_ti(n.model.productionCosts, ti) spc = spc + iv.value # sum production cost [$] # Check to see if a sum dual cost exists in the indexed time interval iv = find_obj_by_ti(self.dualCosts, ti) if iv is None: # No dual cost was found for the indexed time interval. Create # an IntervalValue and assign it the sum dual cost for the # indexed time interval iv = IntervalValue(self, ti, self, MeasurementType.DualCost, sdc) # an IntervalValue # Append the dual cost to the list of interval dual costs self.dualCosts.append(iv) # = [mkt.dualCosts, iv] # IntervalValues else: # A sum dual cost value exists in the indexed time interval. # Simply reassign its value iv.value = sdc # sum dual cost [$] # Check to see if a sum production cost exists in the indexed time interval iv = find_obj_by_ti(self.productionCosts, ti) if iv is None: # No sum production cost was found for the indexed time # interval. Create an IntervalValue and assign it the sum # prodution cost for the indexed time interval iv = IntervalValue(self, ti, self, MeasurementType.ProductionCost, spc) # Append the production cost to the list of interval production costs self.productionCosts.append(iv) else: # A sum production cost value exists in the indexed time # interval. Simply reassign its value iv.value = spc # sum production cost [$] # Sum total dual cost for the entire time horizon self.totalDualCost = sum([x.value for x in self.dualCosts]) # [$] # Sum total primal cost for the entire time horizon self.totalProductionCost = sum([x.value for x in self.productionCosts]) # [$]
def test_prod_cost_from_formula(): from local_asset_model import LocalAssetModel from market import Market print('Running test_prod_cost_from_formula()') pf = 'pass' # Create a test object test_object = LocalAssetModel() # Create a test market test_market = Market() # Create and store the object's cost parameters test_object.costParameters = [4, 3, 2] # Create and store three hourly TimeIntervals # Modified to use the TimeInterval constructor. dt = datetime.now() at = dt dur = timedelta(hours=1) mkt = test_market mct = dt st = dt ti = [TimeInterval(at, dur, mkt, mct, st)] st = st + dur ti.append(TimeInterval(at, dur, mkt, mct, st)) st = st + dur ti.append(TimeInterval(at, dur, mkt, mct, st)) test_market.timeIntervals = ti # Create and store three corresponding scheduled powers iv = [ IntervalValue(test_object, ti[0], test_market, MeasurementType.ScheduledPower, 100), IntervalValue(test_object, ti[1], test_market, MeasurementType.ScheduledPower, 200), IntervalValue(test_object, ti[2], test_market, MeasurementType.ScheduledPower, 300) ] test_object.scheduledPowers = iv # Run the test pc = [0] * 3 for i in range(3): pc[i] = prod_cost_from_formula(test_object, ti[i]) # pc(1) = 4 + 3 * 100 + 0.5 * 2 * 100^2 = 10304 # pc(2) = 4 + 3 * 200 + 0.5 * 2 * 200^2 = 40604 # pc(3) = 4 + 3 * 300 + 0.5 * 2 * 300^2 = 90904 #if all(pc ~=[10304, 40604, 90904]) expected = [10304, 40604, 90904] if all([pc[i] != expected[i] for i in range(len(pc))]): pf = 'fail' raise Exception('- production cost was incorrectly calculated') else: print('- production cost was correctly calculated') # Success print('- the test ran to completion') print('Result: #s\n\n', pf)