parent
c638aaf4ec
commit
afcf8cfabb
@ -0,0 +1,57 @@
|
||||
# # Locational Marginal Prices
|
||||
|
||||
# Locational Marginal Prices (LMPs) refer to the cost of supplying electricity at specific locations of the network. LMPs are crucial for the operation of electricity markets and have many other applications, such as indicating what areas of the network may require additional generation or transmission capacity. UnitCommitment.jl implements two methods for calculating LMPS: Conventional LMPs and Approximated Extended LMPs (AELMPs). In this tutorial, we introduce each method and illustrate their usage.
|
||||
|
||||
# ### Conventional LMPs
|
||||
|
||||
# Conventional LMPs work by (1) solving the original SCUC problem, (2) fixing all binary variables to their optimal values, and (3) re-solving the resulting linear programming model. In this approach, the LMPs are defined as the values of the dual variables associated with the net injection constraints.
|
||||
|
||||
# The first step to use this method is to load and optimize an instance, as explained in previous tutorials:
|
||||
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
instance = UnitCommitment.read_benchmark("matpower/case14/2017-01-01")
|
||||
model =
|
||||
UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer)
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Next, we call `UnitCommitment.compute_lmp`, as shown below. The function accepts three arguments -- a solved SCUC model, the LMP method, and a linear optimizer -- and it returns a dictionary mapping `(scenario_name, bus_name, time)` to the marginal price.
|
||||
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
UnitCommitment.ConventionalLMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# For example, the following code queries the LMP of bus `b1` in scenario `s1` at time 1:
|
||||
|
||||
@show lmp["s1", "b1", 1]
|
||||
|
||||
# ### Approximate Extended LMPs
|
||||
|
||||
# Approximate Extended LMPs (AELMPs) are an alternative method to calculate locational marginal prices which attemps to minimize uplift payments. The method internally works by modifying the instance data in three ways: (1) it sets the minimum power output of each generator to zero, (2) it averages the start-up cost over the offer blocks for each generator, and (3) it relaxes all integrality constraints. To compute AELMPs, as shown in the example below, we call `compute_lmp` and provide `UnitCommitment.AELMP()` as the second argument.
|
||||
|
||||
# This method has two configurable parameters: `allow_offline_participation` and `consider_startup_costs`. If `allow_offline_participation = true`, then offline generators are allowed to participate in the pricing. If instead `allow_offline_participation = false`, offline generators are not allowed and therefore are excluded from the system. A solved UC model is optional if offline participation is allowed, but is required if not allowed. The method forces offline participation to be allowed if the UC model supplied by the user is not solved. For the second field, If `consider_startup_costs = true`, then start-up costs are integrated and averaged over each unit production; otherwise the production costs stay the same. By default, both fields are set to `true`.
|
||||
|
||||
# !!! warning
|
||||
|
||||
# This method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1, the initial power of all generators must be 0, and the initial status of all generators must be negative. The method does not support time-varying start-up costs, and only currently works for deterministic instances. If offline participation is not allowed, AELMPs treats an asset to be offline if it is never on throughout all time periods.
|
||||
|
||||
instance = UnitCommitment.read_benchmark("test/aelmp_simple")
|
||||
|
||||
model =
|
||||
UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer)
|
||||
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
UnitCommitment.AELMP(
|
||||
allow_offline_participation = false,
|
||||
consider_startup_costs = true,
|
||||
),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
@show lmp["s1", "B1", 1]
|
@ -0,0 +1,183 @@
|
||||
# # Market Clearing
|
||||
|
||||
# In North America, electricity markets are structured around two primary types of markets: the day-ahead (DA) market and the real-time (RT) market. The DA market schedules electricity generation and consumption for the next day, based on forecasts and bids from electricity suppliers and consumers. The RT market, on the other hand, operates continuously throughout the day, addressing the discrepancies between the DA schedule and actual demand, typically every five minutes. UnitCommitment.jl is able to simulate the DA and RT market clearing process. Specifically, the package provides the function `UnitCommitment.solve_market` which performs the following steps:
|
||||
|
||||
# 1. Solve the DA market problem.
|
||||
# 2. Extract commitment status of all generators.
|
||||
# 3. Solve a sequence of RT market problems, fixing the commitment status of each generator to the corresponding optimal solution of the DA problem.
|
||||
|
||||
# To use this function, we need to prepare an instance file corresponding to the DA market problem and multiple instance files corresponding to the RT market problems. The number of required files depends on the time granularity and window. For example, suppose that the DA problem is solved at hourly granularity and has 24 time periods, whereas the RT problems are solved at 5-minute granularity and have a single time period. Then we would need to prepare one files for the DA problem and 288 files $\left(24 \times \frac{60}{5}\right)$ for the RT market problems.
|
||||
|
||||
# ## A small example
|
||||
|
||||
# For simplicity, in this tutorial we illustate the usage of `UnitCommitment.solve_market` with a very small example, in which the DA problem has only two time periods. We start by creating the DA instance file:
|
||||
|
||||
da_contents = """
|
||||
{
|
||||
"Parameters": {
|
||||
"Version": "0.4",
|
||||
"Time horizon (h)": 2
|
||||
},
|
||||
"Buses": {
|
||||
"b1": {
|
||||
"Load (MW)": [200, 400]
|
||||
}
|
||||
},
|
||||
"Generators": {
|
||||
"g1": {
|
||||
"Bus": "b1",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [0, 200],
|
||||
"Production cost curve (\$)": [0, 1000],
|
||||
"Initial status (h)": -24,
|
||||
"Initial power (MW)": 0
|
||||
},
|
||||
"g2": {
|
||||
"Bus": "b1",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [0, 300],
|
||||
"Production cost curve (\$)": [0, 3000],
|
||||
"Initial status (h)": -24,
|
||||
"Initial power (MW)": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
open("da.json", "w") do file
|
||||
return write(file, da_contents)
|
||||
end;
|
||||
|
||||
# Next, we create eight single-period RT market problems, each one with a 15-minute time granularity:
|
||||
|
||||
for i in 1:8
|
||||
rt_contents = """
|
||||
{
|
||||
"Parameters": {
|
||||
"Version": "0.4",
|
||||
"Time horizon (min)": 15,
|
||||
"Time step (min)": 15
|
||||
},
|
||||
"Buses": {
|
||||
"b1": {
|
||||
"Load (MW)": [$(150 + 50 * i)]
|
||||
}
|
||||
},
|
||||
"Generators": {
|
||||
"g1": {
|
||||
"Bus": "b1",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [0, 200],
|
||||
"Production cost curve (\$)": [0, 1000],
|
||||
"Initial status (h)": -24,
|
||||
"Initial power (MW)": 0
|
||||
},
|
||||
"g2": {
|
||||
"Bus": "b1",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [0, 300],
|
||||
"Production cost curve (\$)": [0, 3000],
|
||||
"Initial status (h)": -24,
|
||||
"Initial power (MW)": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
open("rt_$i.json", "w") do file
|
||||
return write(file, rt_contents)
|
||||
end
|
||||
end
|
||||
|
||||
# Finally, we call `UnitCommitment.solve_market`, providing as arguments (1) the path to the DA problem; (2) a list of paths to the RT problems; (3) the mixed-integer linear optimizer.
|
||||
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
solution = UnitCommitment.solve_market(
|
||||
"da.json",
|
||||
[
|
||||
"rt_1.json",
|
||||
"rt_2.json",
|
||||
"rt_3.json",
|
||||
"rt_4.json",
|
||||
"rt_5.json",
|
||||
"rt_6.json",
|
||||
"rt_7.json",
|
||||
"rt_8.json",
|
||||
],
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# To retrieve the day-ahead market solution, we can query `solution["DA"]`:
|
||||
|
||||
@show solution["DA"]
|
||||
|
||||
# To query each real-time market solution, we can query `solution["RT"][i]`. Note that LMPs are automativally calculated.
|
||||
|
||||
@show solution["RT"][1]
|
||||
|
||||
# ## Customizing the model and LMPs
|
||||
|
||||
# When using the `solve_market` function it is still possible to customize the problem formulation and the LMP calculation method. In the next example, we use a custom formulation and explicitly specify the LMP method through the `settings` keyword argument:
|
||||
|
||||
UnitCommitment.solve_market(
|
||||
"da.json",
|
||||
[
|
||||
"rt_1.json",
|
||||
"rt_2.json",
|
||||
"rt_3.json",
|
||||
"rt_4.json",
|
||||
"rt_5.json",
|
||||
"rt_6.json",
|
||||
"rt_7.json",
|
||||
"rt_8.json",
|
||||
],
|
||||
settings = UnitCommitment.MarketSettings(
|
||||
lmp_method = UnitCommitment.ConventionalLMP(),
|
||||
formulation = UnitCommitment.Formulation(
|
||||
pwl_costs = UnitCommitment.KnuOstWat2018.PwlCosts(),
|
||||
ramping = UnitCommitment.MorLatRam2013.Ramping(),
|
||||
startup_costs = UnitCommitment.MorLatRam2013.StartupCosts(),
|
||||
transmission = UnitCommitment.ShiftFactorsFormulation(
|
||||
isf_cutoff = 0.008,
|
||||
lodf_cutoff = 0.003,
|
||||
),
|
||||
),
|
||||
),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# It is also possible to add custom variables and constraints to either the DA or RT market problems, through the usage of `after_build_da` and `after_build_rt` callback functions. Similarly, the `after_optimize_da` and `after_optimize_rt` can be used to directly analyze the JuMP models, after they have been optimized:
|
||||
|
||||
using JuMP
|
||||
|
||||
function after_build_da(model, instance)
|
||||
@constraint(model, model[:is_on]["g1", 1] <= model[:is_on]["g2", 1])
|
||||
end
|
||||
|
||||
function after_optimize_da(solution, model, instance)
|
||||
@show value(model[:is_on]["g1", 1])
|
||||
end
|
||||
|
||||
UnitCommitment.solve_market(
|
||||
"da.json",
|
||||
[
|
||||
"rt_1.json",
|
||||
"rt_2.json",
|
||||
"rt_3.json",
|
||||
"rt_4.json",
|
||||
"rt_5.json",
|
||||
"rt_6.json",
|
||||
"rt_7.json",
|
||||
"rt_8.json",
|
||||
],
|
||||
after_build_da = after_build_da,
|
||||
after_optimize_da = after_optimize_da,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# ## Additional considerations
|
||||
|
||||
# - UC.jl supports two-stage stochastic DA market problems. In this case, we need one file for each DA market scenario. All RT market problems must be deterministic.
|
||||
# - UC.jl also supports multi-period RT market problems. Assume, for example, that the DA market problem is an hourly problem with 24 time periods, whereas the RT market problem uses 5-minute granularity with 4 time periods. UC.jl assumes that the first RT file covers period `0:00` to `0:20`, the second covers `0:05` to `0:25` and so on. We therefore still need 288 RT market files. To avoid going beyond the 24-hour period covered by the DA market solution, however, the last few RT market problems must have only 3, 2, and 1 time periods, covering `23:45` to `24:00`, `23:50` to `24:00` and `23:55` to `24:00`, respectively.
|
||||
# - Some MILP solvers (such as Cbc) have issues handling linear programming problems, which are required for the RT market. In this case, a separate linear programming solver can be provided to `solve_market` using the `lp_optimizer` argument. For example, `solve_market(da_file, rt_files, optimizer=Cbc.Optimizer, lp_optimizer=Clp.Optimizer)`.
|
@ -1,115 +0,0 @@
|
||||
# Market clearing and LMPs
|
||||
|
||||
The UC.jl package offers a comprehensive set of functions for solving marketing problems. The primary function, `solve_market`, facilitates the solution of day-ahead (DA) markets, which can be either deterministic or stochastic in nature. Subsequently, it sequentially maps the commitment status obtained from the DA market to all the real-time (RT) markets, which are deterministic instances. It is essential to ensure that the time span of the DA market encompasses all the RT markets, and the file paths for the RT markets must be specified in chronological order. Each RT market should represent a single time slot, and it is recommended to include a few additional time slots to mitigate the closing window effect.
|
||||
|
||||
The `solve_market` function accepts several parameters, including the file path (or a list of file paths in the case of stochastic markets) for the DA market, a list of file paths for the RT markets, the market settings specified by the `MarketSettings` structure, and an optimizer. The `MarketSettings` structure itself requires three optional arguments: `inner_method`, `lmp_method`, and `formulation`. If the computation of Locational Marginal Prices (LMPs) is not desired, the `lmp_method` can be set to `nothing`. Additional optional parameters include a linear programming optimizer for solving LMPs (if a different optimizer than the required one is desired), callback functions `after_build_da` and `after_optimize_da`, which are invoked after the construction and optimization of the DA market, and callback functions `after_build_rt` and `after_optimize_rt`, which are invoked after the construction and optimization of each RT market. It is crucial to note that the `after_build` function requires its two arguments to consistently correspond to `model` and `instance`, while the `after_optimize` function requires its three arguments to consistently correspond to `solution`, `model`, and `instance`.
|
||||
|
||||
As an illustrative example, suppose the DA market predicts hourly data for a 24-hour period, while the RT markets represent 5-minute intervals. In this scenario, each RT market file corresponds to a specific 5-minute interval, with the first RT market representing the initial 5 minutes, the second RT market representing the subsequent 5 minutes, and so on. Consequently, there should be 12 RT market files for each hour. To mitigate the closing window effect, except for the last few RT markets, each RT market should contain three time slots, resulting in a total time span of 15 minutes. However, only the first time slot is considered in the final solution. The last two RT markets should only contain 2 and 1 time slot(s), respectively, to ensure that the total time covered by all RT markets does not exceed the time span of the DA market. The code snippet below demonstrates a simplified example of how to utilize the `solve_market` function. Please note that it only serves as a simplified example and may require further customization based on the specific requirements of your use case.
|
||||
|
||||
```julia
|
||||
using UnitCommitment, Cbc, HiGHS
|
||||
|
||||
import UnitCommitment:
|
||||
MarketSettings,
|
||||
XavQiuWanThi2019,
|
||||
ConventionalLMP,
|
||||
Formulation
|
||||
|
||||
solution = UnitCommitment.solve_market(
|
||||
"da_instance.json",
|
||||
["rt_instance_1.json", "rt_instance_2.json", "rt_instance_3.json"],
|
||||
MarketSettings(
|
||||
inner_method = XavQiuWanThi2019.Method(),
|
||||
lmp_method = ConventionalLMP(),
|
||||
formulation = Formulation(),
|
||||
),
|
||||
optimizer = Cbc.Optimizer,
|
||||
lp_optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
```
|
||||
|
||||
## Computing Locational Marginal Prices
|
||||
|
||||
Locational marginal prices (LMPs) refer to the cost of supplying electricity at a particular location of the network. Multiple methods for computing LMPs have been proposed in the literature. UnitCommitment.jl implements two commonly-used methods: conventional LMPs and Approximated Extended LMPs (AELMPs). To compute LMPs for a given unit commitment instance, the `compute_lmp` function can be used, as shown in the examples below. The function accepts three arguments -- a solved SCUC model, an LMP method, and a linear optimizer -- and it returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||
|
||||
!!! warning
|
||||
|
||||
Most mixed-integer linear optimizers, such as `HiGHS`, `Gurobi` and `CPLEX` can be used with `compute_lmp`, with the notable exception of `Cbc`, which does not support dual value evaluations. If using `Cbc`, please provide `Clp` as the linear optimizer.
|
||||
|
||||
### Conventional LMPs
|
||||
|
||||
LMPs are conventionally computed by: (1) solving the SCUC model, (2) fixing all binary variables to their optimal values, and (3) re-solving the resulting linear programming model. In this approach, the LMPs are defined as the dual variables' values associated with the net injection constraints. The example below shows how to compute conventional LMPs for a given unit commitment instance. First, we build and optimize the SCUC model. Then, we call the `compute_lmp` function, providing as the second argument `ConventionalLMP()`.
|
||||
|
||||
```julia
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
import UnitCommitment: ConventionalLMP
|
||||
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2018-01-01")
|
||||
|
||||
# Build the model
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Optimize the model
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Compute the LMPs using the conventional method
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
ConventionalLMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Access the LMPs
|
||||
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||
@show lmp["s1","b1", 1]
|
||||
```
|
||||
|
||||
### Approximate Extended LMPs
|
||||
|
||||
Approximate Extended LMPs (AELMPs) are an alternative method to calculate locational marginal prices which attemps to minimize uplift payments. The method internally works by modifying the instance data in three ways: (1) it sets the minimum power output of each generator to zero, (2) it averages the start-up cost over the offer blocks for each generator, and (3) it relaxes all integrality constraints. To compute AELMPs, as shown in the example below, we call `compute_lmp` and provide `AELMP()` as the second argument.
|
||||
|
||||
This method has two configurable parameters: `allow_offline_participation` and `consider_startup_costs`. If `allow_offline_participation = true`, then offline generators are allowed to participate in the pricing. If instead `allow_offline_participation = false`, offline generators are not allowed and therefore are excluded from the system. A solved UC model is optional if offline participation is allowed, but is required if not allowed. The method forces offline participation to be allowed if the UC model supplied by the user is not solved. For the second field, If `consider_startup_costs = true`, then start-up costs are integrated and averaged over each unit production; otherwise the production costs stay the same. By default, both fields are set to `true`.
|
||||
|
||||
!!! warning
|
||||
|
||||
This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1, the initial power of all generators must be 0, and the initial status of all generators must be negative. The method does not support time-varying start-up costs. The method does not support multiple scenarios. If offline participation is not allowed, AELMPs treats an asset to be offline if it is never on throughout all time periods.
|
||||
|
||||
```julia
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
import UnitCommitment: AELMP
|
||||
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
|
||||
# Build the model
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Optimize the model
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Compute the AELMPs
|
||||
aelmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
AELMP(
|
||||
allow_offline_participation = false,
|
||||
consider_startup_costs = true
|
||||
),
|
||||
optimizer = HiGHS.Optimizer
|
||||
)
|
||||
|
||||
# Access the AELMPs
|
||||
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||
# Note: although scenario is supported, the query still keeps the scenario keys for consistency.
|
||||
@show aelmp["s1", "b1", 1]
|
||||
```
|
Loading…
Reference in new issue