From afcf8cfabba71f8c20b05cf555b754f8b4d58b31 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 21 May 2024 10:33:51 -0500 Subject: [PATCH] Update docs; prepare for v0.4 release --- .gitignore | 4 +- CHANGELOG.md | 9 + README.md | 10 +- docs/make.jl | 5 +- docs/src/index.md | 6 +- docs/src/tutorials/customizing.jl | 40 ++-- docs/src/tutorials/lmp.jl | 57 ++++++ docs/src/tutorials/market.jl | 183 ++++++++++++++++++ docs/src/tutorials/market.md | 115 ----------- docs/src/tutorials/usage.jl | 31 ++- docs/src/tutorials/utils.jl | 8 +- src/instance/read.jl | 2 +- src/market/market.jl | 15 +- test/src/UnitCommitmentT.jl | 1 + test/src/market/market_test.jl | 66 +++---- .../methods/ProgressiveHedging/usage_test.jl | 2 +- 16 files changed, 330 insertions(+), 224 deletions(-) create mode 100644 docs/src/tutorials/lmp.jl create mode 100644 docs/src/tutorials/market.jl delete mode 100644 docs/src/tutorials/market.md diff --git a/.gitignore b/.gitignore index 96839c7..96671b6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ instances/_source local notebooks docs/src/tutorials/usage.md -docs/src/tutorials/customizing.md \ No newline at end of file +docs/src/tutorials/customizing.md +docs/src/tutorials/market.md +docs/src/tutorials/lmp.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c236429..f9d76bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ All notable changes to this project will be documented in this file. [semver]: https://semver.org/spec/v2.0.0.html [pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0 +## [0.4.0] - 2024-05-21 +### Added +- Add support for two-stage stochastic problems +- Add market clearing functions +- Add time decomposition +- Add scenario decomposition (progressive hedging) +- Add support for battery storage +- Add support for network interfaces + ## [0.3.0] - 2022-07-18 ### Added - Add support for multiple reserve products and zonal reserves. diff --git a/README.md b/README.md index 8b74be1..ccb3b81 100755 --- a/README.md +++ b/README.md @@ -87,11 +87,7 @@ UnitCommitment.write("/tmp/output.json", solution) ## Documentation -1. [Usage](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/usage/) -2. [Data Format](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/format/) -3. [Instances](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/instances/) -4. [JuMP Model](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/model/) -5. [API Reference](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/api/) +See official documentation at: https://anl-ceeesa.github.io/UnitCommitment.jl/ ## Authors * **Alinson S. Xavier** (Argonne National Laboratory) @@ -112,7 +108,7 @@ UnitCommitment.write("/tmp/output.json", solution) If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows: -* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874). +* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.4)". Zenodo (2024). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874). If you use the instances, we additionally request that you cite the original sources, as described in the documentation. @@ -120,7 +116,7 @@ If you use the instances, we additionally request that you cite the original sou ```text UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment -Copyright © 2020-2022, UChicago Argonne, LLC. All Rights Reserved. +Copyright © 2020-2024, UChicago Argonne, LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/docs/make.jl b/docs/make.jl index c91d204..8e67968 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -8,7 +8,9 @@ using Literate function make() literate_sources = [ "src/tutorials/usage.jl", - "src/tutorials/customizing.jl" + "src/tutorials/customizing.jl", + "src/tutorials/lmp.jl", + "src/tutorials/market.jl", ] for src in literate_sources Literate.markdown( @@ -25,6 +27,7 @@ function make() "Tutorials" => [ "tutorials/usage.md", "tutorials/customizing.md", + "tutorials/lmp.md", "tutorials/market.md", "tutorials/decomposition.md", ], diff --git a/docs/src/index.md b/docs/src/index.md index 86312e8..446d452 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,6 +1,6 @@ # UnitCommitment.jl -**UnitCommitment.jl** (UC.jl) is an optimization package for the Security-Constrained Unit Commitment Problem (SCUC), a fundamental optimization problem in power systems used, for example, to clear the day-ahead electricity markets. Both deterministic and two-stage stochastic versions of the problem are supported. The package provides benchmark instances for the problem, a flexible and well-documented data format for the problem, as well as Julia/JuMP implementations of state-of-the-art mixed-integer programming formulations and solution methods. +**UnitCommitment.jl** (UC.jl) is an optimization package for the Security-Constrained Unit Commitment Problem (SCUC), a fundamental optimization problem in power systems used, for example, to clear the electricity markets. Both deterministic and two-stage stochastic versions of the problem are supported. The package provides benchmark instances for the problem, a flexible and well-documented data format for the problem, as well as Julia/JuMP implementations of state-of-the-art mixed-integer programming formulations and solution methods. ## Package Components @@ -29,7 +29,7 @@ If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows: -- **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Jun He, Feng Qiu**, "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.4)". Zenodo (2023). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874). +- **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Jun He, Feng Qiu**, "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.4)". Zenodo (2024). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874). If you use the instances, we additionally request that you cite the original sources, as described in the [instances page](guides/instances.md). @@ -37,7 +37,7 @@ If you use the instances, we additionally request that you cite the original sou ```text UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment -Copyright © 2020-2023, UChicago Argonne, LLC. All Rights Reserved. +Copyright © 2020-2024, UChicago Argonne, LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/docs/src/tutorials/customizing.jl b/docs/src/tutorials/customizing.jl index 43fb98f..9ffc414 100644 --- a/docs/src/tutorials/customizing.jl +++ b/docs/src/tutorials/customizing.jl @@ -43,15 +43,13 @@ model = UnitCommitment.build_model( # First, we load a benchmark instance and solve it, as before. instance = UnitCommitment.read_benchmark("matpower/case14/2017-01-01"); -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -); +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer); UnitCommitment.optimize!(model) # At this point, it is possible to obtain a reference to the decision variables by calling `model[:varname][index]`. For example, `model[:is_on]["g1",1]` returns a direct reference to the JuMP variable indicating whether generator named "g1" is on at time 1. For a complete list of decision variables available, and how are they indexed, see the [problem definition](../guides/problem.md). -@show JuMP.value(model[:is_on]["g1",1]) +@show JuMP.value(model[:is_on]["g1", 1]) # To access second-stage decisions, it is necessary to specify the scenario name. UnitCommitment.jl models deterministic instances as a particular case in which there is a single scenario named "s1", so we need to use this key. @@ -62,33 +60,24 @@ UnitCommitment.optimize!(model) # When testing variations of the unit commitment problem, it is often necessary to modify the objective function, variables and constraints of the formulation. UnitCommitment.jl makes this process relatively easy. The first step is to construct the standard model using `UnitCommitment.build_model`: instance = UnitCommitment.read_benchmark("matpower/case14/2017-01-01"); -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -); +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer); # Now, before calling `UnitCommitment.optimize`, we can make any desired changes to the formulation. In the previous section, we saw how to obtain a direct reference to the decision variables. It is possible to modify them by using standard JuMP methods. For example, to fix the commitment status of a particular generator, we can use `JuMP.fix`: -JuMP.fix(model[:is_on]["g1",1], 1.0, force=true) +JuMP.fix(model[:is_on]["g1", 1], 1.0, force = true) # To modify the cost coefficient of a particular variable, we can use `JuMP.set_objective_coefficient`: -JuMP.set_objective_coefficient( - model, - model[:switch_on]["g1",1], - 1000.0, -) +JuMP.set_objective_coefficient(model, model[:switch_on]["g1", 1], 1000.0) # It is also possible to make changes to the set of constraints. For example, we can add a custom constraint, using the `JuMP.@constraint` macro: -@constraint( - model, - model[:is_on]["g3",1] + model[:is_on]["g4",1] <= 1, -); +@constraint(model, model[:is_on]["g3", 1] + model[:is_on]["g4", 1] <= 1,); # We can also remove an existing model constraint using `JuMP.delete`. See the [problem definition](../guides/problem.md) for a list of constraint names and indices. -JuMP.delete(model, model[:eq_min_uptime]["g1",1]) +JuMP.delete(model, model[:eq_min_uptime]["g1", 1]) # After we are done with all changes, we can call `UnitCommitment.optimize` and extract the optimal solution: @@ -100,16 +89,13 @@ UnitCommitment.optimize!(model) # In this section we demonstrate how to add a new grid component to a particular bus in the network. This is useful, for example, when developing formulations for a new type of generator, energy storage, or any other grid device. We start by reading the instance data and buliding a standard model: instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01") -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -); +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer); # Next, we create decision variables for the new grid component. In this example, we assume that the new component can inject up to 10 MW of power at each time step, so we create new continuous variables $0 \leq x_t \leq 10$. T = instance.time -@variable(model, x[1:T], lower_bound=0.0, upper_bound=10.0); - +@variable(model, x[1:T], lower_bound = 0.0, upper_bound = 10.0); # Next, we add the production costs to the objective function. In this example, we assume a generation cost of \$5/MW: @@ -133,4 +119,4 @@ UnitCommitment.optimize!(model) # We then finally extract the optimal value of the $x$ variables: -@show value.(x) \ No newline at end of file +@show value.(x) diff --git a/docs/src/tutorials/lmp.jl b/docs/src/tutorials/lmp.jl new file mode 100644 index 0000000..c330e8e --- /dev/null +++ b/docs/src/tutorials/lmp.jl @@ -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] diff --git a/docs/src/tutorials/market.jl b/docs/src/tutorials/market.jl new file mode 100644 index 0000000..95c84c3 --- /dev/null +++ b/docs/src/tutorials/market.jl @@ -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)`. diff --git a/docs/src/tutorials/market.md b/docs/src/tutorials/market.md deleted file mode 100644 index a6bd2d9..0000000 --- a/docs/src/tutorials/market.md +++ /dev/null @@ -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] -``` diff --git a/docs/src/tutorials/usage.jl b/docs/src/tutorials/usage.jl index b6a0f89..8a63576 100644 --- a/docs/src/tutorials/usage.jl +++ b/docs/src/tutorials/usage.jl @@ -8,7 +8,7 @@ # pkg> add UnitCommitment@0.4 # ``` -# To solve the optimization models, a mixed-integer linear programming (MILP) solver is also required. Please see the [JuMP installation guide](https://jump.dev/JuMP.jl/stable/installation/) for more instructions on installing a solver. Typical open-source choices are [HiGHS](https://github.com/jump-dev/HiGHS.jl), [Cbc](https://github.com/JuliaOpt/Cbc.jl) and [GLPK](https://github.com/JuliaOpt/GLPK.jl). In the instructions below, HiGHS will be used, but any other MILP solver listed in JuMP installation guide should also be compatible. +# To solve the optimization models, a mixed-integer linear programming (MILP) solver is also required. Please see the [JuMP installation guide](https://jump.dev/JuMP.jl/stable/installation/) for more instructions on installing a solver. Typical open-source choices are [HiGHS](https://github.com/jump-dev/HiGHS.jl), [Cbc](https://github.com/JuliaOpt/Cbc.jl) and [GLPK](https://github.com/JuliaOpt/GLPK.jl). In the instructions below, HiGHS will be used, but any other MILP solver should also be compatible. # ## Solving a benchmark instance @@ -17,16 +17,14 @@ using HiGHS using UnitCommitment -# Next, we use the function `read_benchmark` to read the instance. +# Next, we use the function `UnitCommitment.read_benchmark` to read the instance. instance = UnitCommitment.read_benchmark("matpower/case14/2017-01-01"); # Now that we have the instance loaded in memory, we build the JuMP optimization model using `UnitCommitment.build_model`: -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -); +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer); # Next, we run the optimization process, with `UnitCommitment.optimize!`: @@ -44,7 +42,6 @@ solution = UnitCommitment.solution(model) UnitCommitment.write("solution.json", solution) - # ## Solving a custom deterministic instance # In the previous example, we solved a benchmark instance provided by the package. To solve a custom instance, the first step is to create an input file describing the list of elements (generators, loads and transmission lines) in the network. See [Data Format](../guides/format.md) for a complete description of the data format UC.jl expects. To keep this tutorial self-contained, we will create the input JSON file using Julia; however, this step can also be done with a simple text editor. First, we define the contents of the file: @@ -84,16 +81,14 @@ json_contents = """ # Next, we write it to `example.json`. open("example.json", "w") do file - write(file, json_contents) + return write(file, json_contents) end; # Now that we have the input file, we can proceed as before, but using `UnitCommitment.read` instead of `UnitCommitment.read_benchmark`: instance = UnitCommitment.read("example.json"); -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -); +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer); UnitCommitment.optimize!(model) # Finally, we extract and display the solution: @@ -150,7 +145,7 @@ json_contents_s1 = """ } """ open("example_s1.json", "w") do file - write(file, json_contents_s1) + return write(file, json_contents_s1) end; # Next, we create `example_s2.json`, the second scenario file: @@ -189,7 +184,7 @@ json_contents_s2 = """ } """; open("example_s2.json", "w") do file - write(file, json_contents_s2) + return write(file, json_contents_s2) end; # Now that we have our two scenario files, we can read them using `UnitCommitment.read`. Note that, instead of a single file, we now provide a list. @@ -203,10 +198,8 @@ instance = UnitCommitment.read(glob("example_s*.json")) # Finally, we build the model and optimize as before: -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -); +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer); UnitCommitment.optimize!(model) # The solution to stochastic instances follows a slightly different format, as shown below: @@ -215,4 +208,4 @@ solution = UnitCommitment.solution(model) # The solution for each scenario can be accessed through `solution[scenario_name]`. For conveniance, this includes both first- and second-stage optimal decisions: -solution["s1"] \ No newline at end of file +solution["s1"] diff --git a/docs/src/tutorials/utils.jl b/docs/src/tutorials/utils.jl index c6b47b8..74ef246 100644 --- a/docs/src/tutorials/utils.jl +++ b/docs/src/tutorials/utils.jl @@ -33,7 +33,7 @@ json_contents = """ } """; open("example_initial.json", "w") do file - write(file, json_contents) + return write(file, json_contents) end; # Next, we read the instance and generate the initial conditions (in-place): @@ -43,10 +43,8 @@ UnitCommitment.generate_initial_conditions!(instance, HiGHS.Optimizer) # Finally, we optimize the resulting problem: -model = UnitCommitment.build_model( - instance=instance, - optimizer=HiGHS.Optimizer, -) +model = + UnitCommitment.build_model(instance = instance, optimizer = HiGHS.Optimizer) UnitCommitment.optimize!(model) # !!! warning diff --git a/src/instance/read.jl b/src/instance/read.jl index fcf8ee9..0f828d8 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -8,7 +8,7 @@ using DataStructures using GZip import Base: getindex, time -const INSTANCES_URL = "https://axavier.org/UnitCommitment.jl/0.3/instances" +const INSTANCES_URL = "https://axavier.org/UnitCommitment.jl/0.4/instances" """ read_benchmark(name::AbstractString)::UnitCommitmentInstance diff --git a/src/market/market.jl b/src/market/market.jl index a0d08f2..d55d55f 100644 --- a/src/market/market.jl +++ b/src/market/market.jl @@ -82,8 +82,8 @@ solution = UnitCommitment.solve_market( function solve_market( da_path::Union{String,Vector{String}}, - rt_paths::Vector{String}, - settings::MarketSettings; + rt_paths::Vector{String}; + settings::MarketSettings = MarketSettings(), optimizer, lp_optimizer = nothing, after_build_da = nothing, @@ -107,8 +107,8 @@ function solve_market( ) # prepare the final solution solution = OrderedDict() - solution["Day-ahead market"] = solution_da - solution["Real-time markets"] = OrderedDict() + solution["DA"] = solution_da + solution["RT"] = [] # count the time, sc.time = n-slots, sc.time_step = slot-interval # sufficient to look at only one scenario @@ -169,8 +169,7 @@ function solve_market( ) prev_initial_status = OrderedDict(g.name => g.initial_status for g in sc.thermal_units) - # rt_name = first(split(last(split(rt_path, "/")), ".")) - solution["Real-time markets"][rt_path] = solution_rt + push!(solution["RT"], solution_rt) end # end of for-loop that checks each RT market return solution end @@ -203,10 +202,10 @@ function _build_and_optimize( optimizer = lp_optimizer, ) if length(instance.scenarios) == 1 - solution["Locational marginal price"] = lmp + solution["LMP (\$/MW)"] = lmp else for sc in instance.scenarios - solution[sc.name]["Locational marginal price"] = OrderedDict( + solution[sc.name]["LMP (\$/MW)"] = OrderedDict( key => val for (key, val) in lmp if key[1] == sc.name ) end diff --git a/test/src/UnitCommitmentT.jl b/test/src/UnitCommitmentT.jl index ae28329..50bf1a7 100644 --- a/test/src/UnitCommitmentT.jl +++ b/test/src/UnitCommitmentT.jl @@ -61,6 +61,7 @@ end function format() JuliaFormatter.format(basedir, verbose = true) JuliaFormatter.format("$basedir/../../src", verbose = true) + JuliaFormatter.format("$basedir/../../docs/src", verbose = true) return end diff --git a/test/src/market/market_test.jl b/test/src/market/market_test.jl index 40a8d56..a5e6601 100644 --- a/test/src/market/market_test.jl +++ b/test/src/market/market_test.jl @@ -18,7 +18,7 @@ function simple_market_test() solution = UnitCommitment.solve_market( da_path, rt_paths, - MarketSettings(), # keep everything default + settings = MarketSettings(), # keep everything default optimizer = optimizer_with_attributes( Cbc.Optimizer, "logLevel" => 0, @@ -30,27 +30,27 @@ function simple_market_test() ) # the commitment status must agree with DA market - da_solution = solution["Day-ahead market"] + da_solution = solution["DA"] @test da_solution["Is on"]["GenY"] == [0.0, 1.0] - @test da_solution["Locational marginal price"][("s1", "B1", 1)] == 50.0 - @test da_solution["Locational marginal price"][("s1", "B1", 2)] == 56.0 + @test da_solution["LMP (\$/MW)"][("s1", "B1", 1)] == 50.0 + @test da_solution["LMP (\$/MW)"][("s1", "B1", 2)] == 56.0 - rt_solution = solution["Real-time markets"] + rt_solution = solution["RT"] @test length(rt_solution) == 4 - @test rt_solution[rt_paths[1]]["Is on"]["GenY"] == [0.0, 0.0] - @test rt_solution[rt_paths[2]]["Is on"]["GenY"] == [0.0, 1.0] - @test rt_solution[rt_paths[3]]["Is on"]["GenY"] == [1.0, 1.0] - @test rt_solution[rt_paths[4]]["Is on"]["GenY"] == [1.0] - @test length(rt_solution[rt_paths[1]]["Locational marginal price"]) == 2 - @test length(rt_solution[rt_paths[2]]["Locational marginal price"]) == 2 - @test length(rt_solution[rt_paths[3]]["Locational marginal price"]) == 2 - @test length(rt_solution[rt_paths[4]]["Locational marginal price"]) == 1 + @test rt_solution[1]["Is on"]["GenY"] == [0.0, 0.0] + @test rt_solution[2]["Is on"]["GenY"] == [0.0, 1.0] + @test rt_solution[3]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[4]["Is on"]["GenY"] == [1.0] + @test length(rt_solution[1]["LMP (\$/MW)"]) == 2 + @test length(rt_solution[2]["LMP (\$/MW)"]) == 2 + @test length(rt_solution[3]["LMP (\$/MW)"]) == 2 + @test length(rt_solution[4]["LMP (\$/MW)"]) == 1 # solve market with no lmp method solution_no_lmp = UnitCommitment.solve_market( da_path, rt_paths, - MarketSettings(lmp_method = nothing), # no lmp + settings = MarketSettings(lmp_method = nothing), # no lmp optimizer = optimizer_with_attributes( Cbc.Optimizer, "logLevel" => 0, @@ -58,10 +58,10 @@ function simple_market_test() ) # the commitment status must agree with DA market - da_solution = solution_no_lmp["Day-ahead market"] - @test haskey(da_solution, "Locational marginal price") == false - rt_solution = solution_no_lmp["Real-time markets"] - @test haskey(rt_solution, "Locational marginal price") == false + da_solution = solution_no_lmp["DA"] + @test haskey(da_solution, "LMP (\$/MW)") == false + rt_solution = solution_no_lmp["RT"][1] + @test haskey(rt_solution, "LMP (\$/MW)") == false end end @@ -113,7 +113,7 @@ function stochastic_market_test() solution = UnitCommitment.solve_market( da_path, rt_paths, - MarketSettings(), # keep everything default + settings = MarketSettings(), # keep everything default optimizer = optimizer_with_attributes( Cbc.Optimizer, "logLevel" => 0, @@ -127,25 +127,19 @@ function stochastic_market_test() after_optimize_rt = after_optimize_rt, ) # the commitment status must agree with DA market - da_solution_sp = solution["Day-ahead market"]["market_da_simple"] - da_solution_sc = solution["Day-ahead market"]["market_da_scenario"] + da_solution_sp = solution["DA"]["market_da_simple"] + da_solution_sc = solution["DA"]["market_da_scenario"] @test da_solution_sc["Is on"]["GenY"] == [1.0, 1.0] - @test da_solution_sp["Locational marginal price"][( - "market_da_simple", - "B1", - 1, - )] == 25.0 - @test da_solution_sc["Locational marginal price"][( - "market_da_scenario", - "B1", - 2, - )] == 0.0 + @test da_solution_sp["LMP (\$/MW)"][("market_da_simple", "B1", 1)] == + 25.0 + @test da_solution_sc["LMP (\$/MW)"][("market_da_scenario", "B1", 2)] == + 0.0 - rt_solution = solution["Real-time markets"] - @test rt_solution[rt_paths[1]]["Is on"]["GenY"] == [1.0, 1.0] - @test rt_solution[rt_paths[2]]["Is on"]["GenY"] == [1.0, 1.0] - @test rt_solution[rt_paths[3]]["Is on"]["GenY"] == [1.0, 1.0] - @test rt_solution[rt_paths[4]]["Is on"]["GenY"] == [1.0] + rt_solution = solution["RT"] + @test rt_solution[1]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[2]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[3]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[4]["Is on"]["GenY"] == [1.0] @test length(lmps_rt) == 4 end end diff --git a/test/src/solution/methods/ProgressiveHedging/usage_test.jl b/test/src/solution/methods/ProgressiveHedging/usage_test.jl index 418dda9..5cee953 100644 --- a/test/src/solution/methods/ProgressiveHedging/usage_test.jl +++ b/test/src/solution/methods/ProgressiveHedging/usage_test.jl @@ -9,7 +9,7 @@ function solution_methods_ProgressiveHedging_usage_test() @testset "ProgressiveHedging" begin mpiexec() do exe return run( - `$exe -n 2 $(Base.julia_cmd()) --project=test $basedir/ph.jl`, + `$exe -n 2 $(Base.julia_cmd()) --project=. $basedir/ph.jl`, ) end end