def test_one_region_energy_market_with_elastic_unit_generic_constraints(): # Volume of each bid, number of bid bands must equal number of bands in price_bids. volume_bids = pd.DataFrame({'unit': ['A', 'B'], '1': [100.0, 100.0]}) # Price of each bid, bids must be monotonically increasing. price_bids = pd.DataFrame({'unit': ['A', 'B'], '1': [50.0, 20.0]}) # Factors limiting unit output unit_limits = pd.DataFrame({ 'unit': ['A', 'B'], 'capacity': [100.0, 120.0], # MW }) # Other unit properties unit_info = pd.DataFrame({'unit': ['A', 'B'], 'region': ['NSW', 'NSW']}) demand = pd.DataFrame({ 'region': ['NSW'], 'demand': [80.0] # MW }) # Generic constraints generic_cons = pd.DataFrame({ 'set': ['X'], 'type': ['>='], 'rhs': [65.0], }) violation_costs = pd.DataFrame({'set': ['X'], 'cost': [1000.0]}) unit_coefficients = pd.DataFrame({ 'set': ['X'], 'unit': ['A'], 'service': ['energy'], 'coefficient': [1.0] }) market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW']) market.set_unit_volume_bids(volume_bids) market.set_unit_bid_capacity_constraints(unit_limits) market.set_unit_price_bids(price_bids) market.set_demand_constraints(demand) market.set_generic_constraints(generic_cons) market.make_constraints_elastic('generic', violation_costs) market.link_units_to_generic_constraints(unit_coefficients) market.dispatch() expected_prices = pd.DataFrame({'region': ['NSW'], 'price': [20.0]}) expected_dispatch = pd.DataFrame({ 'unit': ['A', 'B'], 'service': ['energy', 'energy'], 'dispatch': [65.0, 15.0] }) assert_frame_equal(market.get_energy_prices(), expected_prices) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch)
def test_two_region_energy_market(): # Volume of each bid, number of bid bands must equal number of bands in price_bids. volume_bids = pd.DataFrame({ 'unit': ['A', 'B'], '1': [20.0, 20.0], # MW '2': [50.0, 100.0], # MW }) # Price of each bid, bids must be monotonically increasing. price_bids = pd.DataFrame({ 'unit': ['A', 'B'], '1': [50.0, 52.0], # $/MW '2': [53.0, 60.0], # $/MW }) # Factors limiting unit output unit_limits = pd.DataFrame({ 'unit': ['A', 'B'], 'capacity': [70.0, 120.0], # MW }) # Other unit properties unit_info = pd.DataFrame({ 'unit': ['A', 'B'], 'region': ['NSW', 'VIC'], # MW 'loss_factor': [0.9, 0.95] # MW/h }) demand = pd.DataFrame({ 'region': ['NSW', 'VIC'], 'demand': [60.0, 80.0] # MW }) market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW', 'VIC']) market.set_unit_volume_bids(volume_bids) market.set_unit_bid_capacity_constraints(unit_limits) market.set_unit_price_bids(price_bids) market.set_demand_constraints(demand) market.dispatch() expected_prices = pd.DataFrame({ 'region': ['NSW', 'VIC'], 'price': [53 / 0.9, 60 / 0.95] }) expected_dispatch = pd.DataFrame({ 'unit': ['A', 'B'], 'service': ['energy', 'energy'], 'dispatch': [60.0, 80.0] }) assert_frame_equal(market.get_energy_prices(), expected_prices) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch)
def __init__(self, unit_inputs, interconnector_inputs, constraint_inputs, demand_inputs): self.unit_inputs = unit_inputs self.interconnector_inputs = interconnector_inputs self.constraint_inputs = constraint_inputs self.regional_demand_inputs = demand_inputs unit_info = self.unit_inputs.get_unit_info() self.market = markets.SpotMarket( market_regions=['QLD1', 'NSW1', 'VIC1', 'SA1', 'TAS1'], unit_info=unit_info) self.market.solver_name = 'GUROBI'
# List for saving outputs to. outputs = [] # Create and dispatch the spot market for each dispatch interval. for interval in dispatch_intervals: raw_inputs_loader.set_interval(interval) unit_inputs = units.UnitData(raw_inputs_loader) interconnector_inputs = interconnectors.InterconnectorData( raw_inputs_loader) constraint_inputs = constraints.ConstraintData(raw_inputs_loader) demand_inputs = demand.DemandData(raw_inputs_loader) unit_info = unit_inputs.get_unit_info() market = markets.SpotMarket( market_regions=['QLD1', 'NSW1', 'VIC1', 'SA1', 'TAS1'], unit_info=unit_info) # Set bids volume_bids, price_bids = unit_inputs.get_processed_bids() market.set_unit_volume_bids(volume_bids) market.set_unit_price_bids(price_bids) # Set bid in capacity limits unit_bid_limit = unit_inputs.get_unit_bid_availability() market.set_unit_bid_capacity_constraints(unit_bid_limit) cost = constraint_inputs.get_constraint_violation_prices()['unit_capacity'] market.make_constraints_elastic('unit_bid_capacity', violation_cost=cost) # Set limits provided by the unconstrained intermittent generation # forecasts. Primarily for wind and solar.
# FCAS requirement in the region\s being dispatched. fcas_requirements = pd.DataFrame({ 'set': ['nsw_regulation_requirement', 'nsw_raise_6s_requirement'], 'region': ['NSW', 'NSW'], 'service': ['raise_reg', 'raise_6s'], 'volume': [10.0, 10.0] # MW }) print(fcas_requirements) # set region service volume # 0 nsw_regulation_requirement NSW raise_reg 10.0 # 1 nsw_raise_6s_requirement NSW raise_6s 10.0 # Create the market model with unit service bids. market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW']) market.set_unit_volume_bids(volume_bids) market.set_unit_price_bids(price_bids) # Create constraints that enforce the top of the FCAS trapezium. fcas_availability = fcas_trapeziums.loc[:, [ 'unit', 'service', 'max_availability' ]] market.set_fcas_max_availability(fcas_availability) # Create constraints that enforce the lower and upper slope of the FCAS regulation # service trapeziums. regulation_trapeziums = fcas_trapeziums[fcas_trapeziums['service'] == 'raise_reg'] market.set_energy_and_regulation_capacity_constraints(regulation_trapeziums)
def test_use_unit_generic_constraints_to_exclude_unit_from_providing_raise_reg( ): # Volume of each bid. volume_bids = pd.DataFrame({ 'unit': ['A', 'A', 'B', 'B', 'B'], 'service': ['energy', 'raise_6s', 'energy', 'raise_6s', 'raise_reg'], '1': [100.0, 11.0, 110.0, 15.0, 15.0], # MW }) # Price of each bid. price_bids = pd.DataFrame({ 'unit': ['A', 'A', 'B', 'B', 'B'], 'service': ['energy', 'raise_6s', 'energy', 'raise_6s', 'raise_reg'], '1': [50.0, 35.0, 60.0, 20.0, 30.0], # $/MW }) # Unit locations. unit_info = pd.DataFrame({'unit': ['A', 'B'], 'region': ['NSW', 'NSW']}) # The demand in the region\s being dispatched. demand = pd.DataFrame({ 'region': ['NSW'], 'demand': [195.0] # MW }) # FCAS requirement in the region\s being dispatched. fcas_requirements = pd.DataFrame({ 'set': ['nsw_regulation_requirement', 'nsw_raise_6s_requirement'], 'region': ['NSW', 'NSW'], 'service': ['raise_reg', 'raise_6s'], 'volume': [10.0, 10.0] # MW }) # Generic constraints interconnector_coefficients = pd.DataFrame({ 'set': ['nsw_raise_6s_requirement'], 'unit': ['B'], 'service': ['raise_6s'], 'coefficient': [-1.0], }) # Create the market model with unit service bids. market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW', 'VIC']) market.set_unit_volume_bids(volume_bids) market.set_unit_price_bids(price_bids) # Set the demand for energy. market.set_demand_constraints(demand) # Set the required volume of FCAS services. market.set_fcas_requirements_constraints(fcas_requirements) # Create generic constraints market.link_units_to_generic_constraints(interconnector_coefficients) # Calculate dispatch and pricing market.dispatch() expected_dispatch = pd.DataFrame({ 'unit': ['A', 'A', 'B', 'B', 'B'], 'service': ['energy', 'raise_6s', 'energy', 'raise_6s', 'raise_reg'], 'dispatch': [100.0, 10.0, 95.0, 0.0, 10.0] }) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch)
def test_two_region_energy_market_with_interconnector_generic_constraints(): # Volume of each bid, number of bid bands must equal number of bands in price_bids. volume_bids = pd.DataFrame({'unit': ['A', 'B'], '1': [100.0, 100.0]}) # Price of each bid, bids must be monotonically increasing. price_bids = pd.DataFrame({'unit': ['A', 'B'], '1': [50.0, 20.0]}) # Factors limiting unit output unit_limits = pd.DataFrame({ 'unit': ['A', 'B'], 'capacity': [100.0, 120.0], # MW }) # Other unit properties unit_info = pd.DataFrame({'unit': ['A', 'B'], 'region': ['NSW', 'VIC']}) demand = pd.DataFrame({ 'region': ['NSW', 'VIC'], 'demand': [60.0, 80.0] # MW }) # Generic constraints generic_cons = pd.DataFrame({ 'set': ['X'], 'type': ['>='], 'rhs': [10.0], }) interconnector_coefficients = pd.DataFrame({ 'set': ['X'], 'interconnector': ['little_link'], 'coefficient': [1.0] }) # There is one interconnector between NSW and VIC. Its nominal direction is towards VIC. interconnectors = pd.DataFrame({ 'interconnector': ['little_link'], 'to_region': ['VIC'], 'from_region': ['NSW'], 'max': [100.0], 'min': [-120.0] }) market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW', 'VIC']) market.set_interconnectors(interconnectors) market.set_unit_volume_bids(volume_bids) market.set_unit_bid_capacity_constraints(unit_limits) market.set_unit_price_bids(price_bids) market.set_demand_constraints(demand) market.set_generic_constraints(generic_cons) market.link_interconnectors_to_generic_constraints( interconnector_coefficients) market.dispatch() expected_prices = pd.DataFrame({ 'region': ['NSW', 'VIC'], 'price': [50.0, 20.0] }) expected_dispatch = pd.DataFrame({ 'unit': ['A', 'B'], 'service': ['energy', 'energy'], 'dispatch': [70.0, 70.0] }) assert_frame_equal(market.get_energy_prices(), expected_prices) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch)
def test_raise_6s_and_raise_reg(): # Volume of each bid. volume_bids = pd.DataFrame({ 'unit': ['A', 'A', 'B', 'B', 'B'], 'service': ['energy', 'raise_6s', 'energy', 'raise_6s', 'raise_reg'], '1': [100.0, 10.0, 110.0, 15.0, 15.0], # MW }) # Price of each bid. price_bids = pd.DataFrame({ 'unit': ['A', 'A', 'B', 'B', 'B'], 'service': ['energy', 'raise_6s', 'energy', 'raise_6s', 'raise_reg'], '1': [50.0, 35.0, 60.0, 20.0, 30.0], # $/MW }) # Participant defined operational constraints on FCAS enablement. fcas_trapeziums = pd.DataFrame({ 'unit': ['B', 'B', 'A'], 'service': ['raise_reg', 'raise_6s', 'raise_6s'], 'max_availability': [15.0, 15.0, 10.0], 'enablement_min': [50.0, 50.0, 70.0], 'low_break_point': [65.0, 65.0, 80.0], 'high_break_point': [95.0, 95.0, 100.0], 'enablement_max': [110.0, 110.0, 110.0] }) # Unit locations. unit_info = pd.DataFrame({'unit': ['A', 'B'], 'region': ['NSW', 'NSW']}) # The demand in the region\s being dispatched. demand = pd.DataFrame({ 'region': ['NSW'], 'demand': [195.0] # MW }) # FCAS requirement in the region\s being dispatched. fcas_requirements = pd.DataFrame({ 'set': ['nsw_regulation_requirement', 'nsw_raise_6s_requirement'], 'region': ['NSW', 'NSW'], 'service': ['raise_reg', 'raise_6s'], 'volume': [10.0, 10.0] # MW }) # Create the market model with unit service bids. market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW']) market.set_unit_volume_bids(volume_bids) market.set_unit_price_bids(price_bids) # Create constraints that enforce the top of the FCAS trapezium. fcas_availability = fcas_trapeziums.loc[:, [ 'unit', 'service', 'max_availability' ]] market.set_fcas_max_availability(fcas_availability) # Create constraints the enforce the lower and upper slope of the FCAS regulation # service trapeziums. regulation_trapeziums = fcas_trapeziums[fcas_trapeziums['service'] == 'raise_reg'] market.set_energy_and_regulation_capacity_constraints( regulation_trapeziums) # Create constraints that enforce the lower and upper slope of the FCAS contingency # trapezium. These constrains also scale slopes of the trapezium to ensure the # co-dispatch of contingency and regulation services is technically feasible. contingency_trapeziums = fcas_trapeziums[fcas_trapeziums['service'] == 'raise_6s'] market.set_joint_capacity_constraints(contingency_trapeziums) # Set the demand for energy. market.set_demand_constraints(demand) # Set the required volume of FCAS services. market.set_fcas_requirements_constraints(fcas_requirements) # Calculate dispatch and pricing market.dispatch() expected_dispatch = pd.DataFrame({ 'unit': ['A', 'A', 'B', 'B', 'B'], 'service': ['energy', 'raise_6s', 'energy', 'raise_6s', 'raise_reg'], 'dispatch': [100.0, 5.0, 95.0, 5.0, 10.0] }) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch) expected_energy_prices = pd.DataFrame({'region': ['NSW'], 'price': [75.0]}) assert_frame_equal(market.get_energy_prices(), expected_energy_prices) expected_fcas_prices = pd.DataFrame({ 'region': ['NSW', 'NSW'], 'service': ['raise_6s', 'raise_reg'], 'price': [35.0, 45.0] }) assert_frame_equal(market.get_fcas_prices(), expected_fcas_prices)
def test_one_region_energy_and_raise_regulation_markets(): # Volume of each bid, number of bands must equal number of bands in price_bids. volume_bids = pd.DataFrame({ 'unit': ['A', 'B', 'B'], 'service': ['energy', 'energy', 'raise_reg'], '1': [100.0, 110.0, 15.0], # MW }) # Price of each bid, bids must be monotonically increasing. price_bids = pd.DataFrame({ 'unit': ['A', 'B', 'B'], 'service': ['energy', 'energy', 'raise_reg'], '1': [50.0, 60.0, 20.0], # $/MW }) # Participant defined operational constraints on FCAS enablement. fcas_trapeziums = pd.DataFrame({ 'unit': ['B'], 'service': ['raise_reg'], 'max_availability': [15.0], 'enablement_min': [50.0], 'low_break_point': [65.0], 'high_break_point': [95.0], 'enablement_max': [110.0] }) # Unit locations. unit_info = pd.DataFrame({'unit': ['A', 'B'], 'region': ['NSW', 'NSW']}) # The demand in the region\s being dispatched. demand = pd.DataFrame({ 'region': ['NSW'], 'demand': [100.0] # MW }) # FCAS requirement in the region\s being dispatched. fcas_requirement = pd.DataFrame({ 'set': ['nsw_regulation_requirement'], 'region': ['NSW'], 'service': ['raise_reg'], 'volume': [10.0] # MW }) # Create the market model market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW']) market.set_unit_volume_bids(volume_bids) market.set_unit_price_bids(price_bids) market.set_fcas_max_availability( fcas_trapeziums.loc[:, ['unit', 'service', 'max_availability']]) market.set_energy_and_regulation_capacity_constraints(fcas_trapeziums) market.set_demand_constraints(demand) market.set_fcas_requirements_constraints(fcas_requirement) # Calculate dispatch and pricing market.dispatch() # Return the total dispatch of each unit in MW. Note that despite the energy bid of A being cheaper and capable # of meeting total demand, the more expensive B is dispatch up to 60 MW so that it can deliver the raise_reg # service. print(market.get_unit_dispatch()) # unit service dispatch # 0 A energy 40.0 # 1 B energy 60.0 # 2 B raise_reg 10.0 # Return the price of energy. print(market.get_energy_prices()) # region price # 0 NSW 60.0 # Return the price of regulation FCAS. Note to meet marginal FCAS demand unit B has to turn up and unit A needs to # turn down, at a net marginal cost of 10 $/MW, it would also cost 20 $/MW to increase unit B FCAS dispatch, hence # the total marginal cost of raise reg is 10 + 20 = 30. print(market.get_fcas_prices()) # set price # 0 nsw_regulation_requirement 30.0 expected_dispatch = pd.DataFrame({ 'unit': ['A', 'B', 'B'], 'service': ['energy', 'energy', 'raise_reg'], 'dispatch': [40.0, 60.0, 10.0] }) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch) expected_energy_prices = pd.DataFrame({'region': ['NSW'], 'price': [50.0]}) assert_frame_equal(market.get_energy_prices(), expected_energy_prices) expected_fcas_prices = pd.DataFrame({ 'region': ['NSW'], 'service': ['raise_reg'], 'price': [30.0] }) assert_frame_equal(market.get_fcas_prices(), expected_fcas_prices)
def test_one_interconnector(): # The only generator is located in NSW. unit_info = pd.DataFrame({ 'unit': ['A'], 'region': ['NSW'] # MW }) # Create a market instance. market = markets.SpotMarket(unit_info=unit_info, market_regions=['NSW', 'VIC']) # Volume of each bids. volume_bids = pd.DataFrame({ 'unit': ['A'], '1': [100.0] # MW }) market.set_unit_volume_bids(volume_bids) # Price of each bid. price_bids = pd.DataFrame({ 'unit': ['A'], '1': [50.0] # $/MW }) market.set_unit_price_bids(price_bids) # NSW has no demand but VIC has 90 MW. demand = pd.DataFrame({ 'region': ['NSW', 'VIC'], 'demand': [0.0, 90.0] # MW }) market.set_demand_constraints(demand) # There is one interconnector between NSW and VIC. Its nominal direction is towards VIC. interconnectors = pd.DataFrame({ 'interconnector': ['little_link'], 'to_region': ['VIC'], 'from_region': ['NSW'], 'max': [100.0], 'min': [-120.0] }) market.set_interconnectors(interconnectors) # The interconnector loss function. In this case losses are always 5 % of line flow. def constant_losses(flow): return abs(flow) * 0.05 # The loss function on a per interconnector basis. Also details how the losses should be proportioned to the # connected regions. loss_functions = pd.DataFrame({ 'interconnector': ['little_link'], 'from_region_loss_share': [0.5], # losses are shared equally. 'loss_function': [constant_losses] }) # The points to linearly interpolate the loss function bewtween. In this example the loss function is linear so only # three points are needed, but if a non linear loss function was used then more points would be better. interpolation_break_points = pd.DataFrame({ 'interconnector': ['little_link', 'little_link', 'little_link'], 'loss_segment': [1, 2, 3], 'break_point': [-120.0, 0.0, 100] }) market.set_interconnector_losses(loss_functions, interpolation_break_points) # Calculate dispatch. market.dispatch() expected_prices = pd.DataFrame({ 'region': ['NSW', 'VIC'], 'price': [50., 50. * (((90. / 0.975) + (90. / 0.975) * 0.025) / 90)] }) expected_dispatch = pd.DataFrame({ 'unit': ['A'], 'service': ['energy'], 'dispatch': [(90.0 / 0.975) + (90.0 / 0.975) * 0.025] }) expected_interconnector_flow = pd.DataFrame({ 'interconnector': ['little_link'], 'link': ['little_link'], 'flow': [90.0 / 0.975], 'losses': [(90.0 / 0.975) * 0.05] }) assert_frame_equal(market.get_energy_prices(), expected_prices) assert_frame_equal(market.get_unit_dispatch(), expected_dispatch) assert_frame_equal(market.get_interconnector_flows(), expected_interconnector_flow)