mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
@@ -97,6 +97,7 @@ UnitCommitment.write("/tmp/output.json", solution)
|
||||
* **Alinson S. Xavier** (Argonne National Laboratory)
|
||||
* **Aleksandr M. Kazachkov** (University of Florida)
|
||||
* **Ogün Yurdakul** (Technische Universität Berlin)
|
||||
* **Jun He** (Purdue University)
|
||||
* **Feng Qiu** (Argonne National Laboratory)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[deps]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Documenter, UnitCommitment
|
||||
using Documenter, UnitCommitment, JuMP
|
||||
|
||||
makedocs(
|
||||
sitename="UnitCommitment.jl",
|
||||
|
||||
@@ -12,6 +12,20 @@ UnitCommitment.validate
|
||||
UnitCommitment.write
|
||||
```
|
||||
|
||||
## Locational Marginal Prices
|
||||
|
||||
### Conventional LMPs
|
||||
```@docs
|
||||
UnitCommitment.compute_lmp(::JuMP.Model,::UnitCommitment.ConventionalLMP)
|
||||
```
|
||||
|
||||
### Approximated Extended LMPs
|
||||
```@docs
|
||||
UnitCommitment.AELMP
|
||||
UnitCommitment.compute_lmp(::JuMP.Model,::UnitCommitment.AELMP)
|
||||
```
|
||||
|
||||
|
||||
## Modify instance
|
||||
|
||||
```@docs
|
||||
|
||||
@@ -70,11 +70,14 @@ This section describes the characteristics of each bus in the system.
|
||||
|
||||
### Generators
|
||||
|
||||
This section describes all generators in the system, including thermal units, renewable units and virtual units.
|
||||
This section describes all generators in the system, including thermal units, renewable units and virtual units. Two types of generators can be specified - thermal units and profiled units. A thermal unit consists of different fields, while a profiled unit is a simple generator with only a production capacity and a per-unit cost.
|
||||
|
||||
#### Thermal Units
|
||||
|
||||
| Key | Description | Default | Time series?
|
||||
| :------------------------ | :------------------------------------------------| ------- | :-----------:
|
||||
| `Bus` | Identifier of the bus where this generator is located (string). | Required | N
|
||||
| `Type` | Type of the generator (string). For thermal generators, this must be `Thermal`. | Required | N
|
||||
| `Production cost curve (MW)` and `Production cost curve ($)` | Parameters describing the piecewise-linear production costs. See below for more details. | Required | Y
|
||||
| `Startup costs ($)` and `Startup delays (h)` | Parameters describing how much it costs to start the generator after it has been shut down for a certain amount of time. If `Startup costs ($)` and `Startup delays (h)` are set to `[300.0, 400.0]` and `[1, 4]`, for example, and the generator is shut down at time `00:00` (h:min), then it costs \$300 to start up the generator at any time between `01:00` and `03:59`, and \$400 to start the generator at time `04:00` or any time after that. The number of startup cost points is unlimited, and may be different for each generator. Startup delays must be strictly increasing and the first entry must equal `Minimum downtime (h)`. | `[0.0]` and `[1]` | N
|
||||
| `Minimum uptime (h)` | Minimum amount of time the generator must stay operational after starting up (in hours). For example, if the generator starts up at time `00:00` (h:min) and `Minimum uptime (h)` is set to 4, then the generator can only shut down at time `04:00`. | `1` | N
|
||||
@@ -88,6 +91,15 @@ This section describes all generators in the system, including thermal units, re
|
||||
| `Must run?` | If `true`, the generator should be committed, even if that is not economical (Boolean). | `false` | Y
|
||||
| `Reserve eligibility` | List of reserve products this generator is eligibe to provide. By default, the generator is not eligible to provide any reserves. | `[]` | N
|
||||
|
||||
#### Profiled Units
|
||||
|
||||
| Key | Description | Default | Time series?
|
||||
| :---------------- | :------------------------------------------------ | :------: | :------------:
|
||||
| `Bus` | Identifier of the bus where this generator is located (string). | Required | N
|
||||
| `Type` | Type of the generator (string). For profiled generators, this must be `Profiled`. | Required | N
|
||||
| `Cost ($/MW)` | Cost incurred for serving each MW of power by this generator. | Required | Y
|
||||
| `Maximum Capacity (MW)` | Maximum amount of power to be supplied by this generator. Any amount lower than this may be supplied. | Required | Y
|
||||
|
||||
#### Production costs and limits
|
||||
|
||||
Production costs are represented as piecewise-linear curves. Figure 1 shows an example cost curve with three segments, where it costs \$1400, \$1600, \$2200 and \$2400 to generate, respectively, 100, 110, 130 and 135 MW of power. To model this generator, `Production cost curve (MW)` should be set to `[100, 110, 130, 135]`, and `Production cost curve ($)` should be set to `[1400, 1600, 2200, 2400]`.
|
||||
@@ -115,6 +127,7 @@ Note that this curve also specifies the production limits. Specifically, the fir
|
||||
"Generators": {
|
||||
"gen1": {
|
||||
"Bus": "b1",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0],
|
||||
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0],
|
||||
"Startup costs ($)": [300.0, 400.0],
|
||||
@@ -126,14 +139,24 @@ Note that this curve also specifies the production limits. Specifically, the fir
|
||||
"Minimum downtime (h)": 4,
|
||||
"Minimum uptime (h)": 4,
|
||||
"Initial status (h)": 12,
|
||||
"Initial power (MW)": 115,
|
||||
"Must run?": false,
|
||||
"Reserve eligibility": ["r1"],
|
||||
"Reserve eligibility": ["r1"]
|
||||
},
|
||||
"gen2": {
|
||||
"Bus": "b5",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]],
|
||||
"Production cost curve ($)": [0.0, 0.0],
|
||||
"Reserve eligibility": ["r1", "r2"],
|
||||
"Initial status (h)": -100,
|
||||
"Initial power (MW)": 0,
|
||||
"Reserve eligibility": ["r1", "r2"]
|
||||
},
|
||||
"gen3": {
|
||||
"Bus": "b6",
|
||||
"Type": "Profiled",
|
||||
"Maximum power (MW)": 120.0,
|
||||
"Cost ($/MW)": 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ Depth = 3
|
||||
* **Alinson S. Xavier** (Argonne National Laboratory)
|
||||
* **Aleksandr M. Kazachkov** (University of Florida)
|
||||
* **Ogün Yurdakul** (Technische Universität Berlin)
|
||||
* **Jun He** (Purdue University)
|
||||
* **Feng Qiu** (Argonne National Laboratory)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
@@ -8,6 +8,8 @@ Decision variables
|
||||
|
||||
### Generators
|
||||
|
||||
#### Thermal Units
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
:-----|:--------:|:-------------|:------:
|
||||
`is_on[g,t]` | $u_{g}(t)$ | True if generator `g` is on at time `t`. | Binary
|
||||
@@ -19,6 +21,13 @@ Name | Symbol | Description | Unit
|
||||
`startup[g,t,s]` | $\delta^s_g(t)$ | True if generator `g` switches on at time `t` incurring start-up costs from start-up category `s`. | Binary
|
||||
|
||||
|
||||
#### Profiled Units
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
:-----|:------:|:-------------|:------:
|
||||
`prod_profiled[s,t]` | $p^{\dagger}_{g}(t)$ | Amount of power produced by profiled unit `g` at time `t`. | MW
|
||||
|
||||
|
||||
### Buses
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
|
||||
@@ -58,10 +58,7 @@ using UnitCommitment
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
```
|
||||
|
||||
Advanced usage
|
||||
--------------
|
||||
|
||||
### Customizing the formulation
|
||||
## Customizing the formulation
|
||||
|
||||
By default, `build_model` uses a formulation that combines modeling components from different publications, and that has been carefully tested, using our own benchmark scripts, to provide good performance across a wide variety of instances. This default formulation is expected to change over time, as new methods are proposed in the literature. You can, however, construct your own formulation, based on the modeling components that you choose, as shown in the next example.
|
||||
|
||||
@@ -94,7 +91,7 @@ model = UnitCommitment.build_model(
|
||||
)
|
||||
```
|
||||
|
||||
### Generating initial conditions
|
||||
## Generating initial conditions
|
||||
|
||||
When creating random unit commitment instances for benchmark purposes, it is often hard to compute, in advance, sensible initial conditions for all generators. Setting initial conditions naively (for example, making all generators initially off and producing no power) can easily cause the instance to become infeasible due to excessive ramping. Initial conditions can also make it hard to modify existing instances. For example, increasing the system load without carefully modifying the initial conditions may make the problem infeasible or unrealistically challenging to solve.
|
||||
|
||||
@@ -122,7 +119,7 @@ UnitCommitment.optimize!(model)
|
||||
|
||||
The function `generate_initial_conditions!` may return different initial conditions after each call, even if the same instance and the same optimizer is provided. The particular algorithm may also change in a future version of UC.jl. For these reasons, it is recommended that you generate initial conditions exactly once for each instance and store them for later use.
|
||||
|
||||
### Verifying solutions
|
||||
## Verifying solutions
|
||||
|
||||
When developing new formulations, it is very easy to introduce subtle errors in the model that result in incorrect solutions. To help with this, UC.jl includes a utility function that verifies if a given solution is feasible, and, if not, prints all the validation errors it found. The implementation of this function is completely independent from the implementation of the optimization model, and therefore can be used to validate it. The function can also be used to verify solutions produced by other optimization packages, as long as they follow the [UC.jl data format](format.md).
|
||||
|
||||
@@ -139,3 +136,91 @@ solution = JSON.parsefile("solution.json")
|
||||
# Validate solution and print validation errors
|
||||
UnitCommitment.validate(instance, solution)
|
||||
```
|
||||
|
||||
## 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]
|
||||
```
|
||||
@@ -9,6 +9,7 @@ using Base: String
|
||||
include("instance/structs.jl")
|
||||
include("model/formulations/base/structs.jl")
|
||||
include("solution/structs.jl")
|
||||
include("lmp/structs.jl")
|
||||
|
||||
include("model/formulations/ArrCon2000/structs.jl")
|
||||
include("model/formulations/CarArr2006/structs.jl")
|
||||
@@ -31,6 +32,7 @@ include("model/formulations/base/psload.jl")
|
||||
include("model/formulations/base/sensitivity.jl")
|
||||
include("model/formulations/base/system.jl")
|
||||
include("model/formulations/base/unit.jl")
|
||||
include("model/formulations/base/punit.jl")
|
||||
include("model/formulations/CarArr2006/pwlcosts.jl")
|
||||
include("model/formulations/DamKucRajAta2016/ramp.jl")
|
||||
include("model/formulations/Gar1962/pwlcosts.jl")
|
||||
@@ -58,5 +60,7 @@ include("utils/log.jl")
|
||||
include("utils/benchmark.jl")
|
||||
include("validation/repair.jl")
|
||||
include("validation/validate.jl")
|
||||
include("lmp/conventional.jl")
|
||||
include("lmp/aelmp.jl")
|
||||
|
||||
end
|
||||
|
||||
@@ -18,9 +18,9 @@ function read_egret_solution(path::String)::OrderedDict
|
||||
|
||||
solution = OrderedDict()
|
||||
is_on = solution["Is on"] = OrderedDict()
|
||||
production = solution["Production (MW)"] = OrderedDict()
|
||||
production = solution["Thermal production (MW)"] = OrderedDict()
|
||||
reserve = solution["Reserve (MW)"] = OrderedDict()
|
||||
production_cost = solution["Production cost (\$)"] = OrderedDict()
|
||||
production_cost = solution["Thermal production cost (\$)"] = OrderedDict()
|
||||
startup_cost = solution["Startup cost (\$)"] = OrderedDict()
|
||||
|
||||
for (gen_name, gen_dict) in egret["elements"]["generator"]
|
||||
|
||||
@@ -17,6 +17,7 @@ function _migrate(json)
|
||||
end
|
||||
version = VersionNumber(version)
|
||||
version >= v"0.3" || _migrate_to_v03(json)
|
||||
version >= v"0.4" || _migrate_to_v04(json)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -36,3 +37,14 @@ function _migrate_to_v03(json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _migrate_to_v04(json)
|
||||
# Migrate thermal units
|
||||
if json["Generators"] !== nothing
|
||||
for (gen_name, gen) in json["Generators"]
|
||||
if gen["Type"] === nothing
|
||||
gen["Type"] = "Thermal"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -129,12 +129,13 @@ end
|
||||
|
||||
function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
_migrate(json)
|
||||
units = Unit[]
|
||||
thermal_units = ThermalUnit[]
|
||||
buses = Bus[]
|
||||
contingencies = Contingency[]
|
||||
lines = TransmissionLine[]
|
||||
loads = PriceSensitiveLoad[]
|
||||
reserves = Reserve[]
|
||||
profiled_units = ProfiledUnit[]
|
||||
|
||||
function scalar(x; default = nothing)
|
||||
x !== nothing || return default
|
||||
@@ -159,7 +160,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
|
||||
name_to_bus = Dict{String,Bus}()
|
||||
name_to_line = Dict{String,TransmissionLine}()
|
||||
name_to_unit = Dict{String,Unit}()
|
||||
name_to_unit = Dict{String,ThermalUnit}()
|
||||
name_to_reserve = Dict{String,Reserve}()
|
||||
|
||||
function timeseries(x; default = nothing)
|
||||
@@ -180,8 +181,9 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
bus_name,
|
||||
length(buses),
|
||||
timeseries(dict["Load (MW)"]),
|
||||
Unit[],
|
||||
ThermalUnit[],
|
||||
PriceSensitiveLoad[],
|
||||
ProfiledUnit[],
|
||||
)
|
||||
name_to_bus[bus_name] = bus
|
||||
push!(buses, bus)
|
||||
@@ -194,7 +196,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
name = reserve_name,
|
||||
type = lowercase(dict["Type"]),
|
||||
amount = timeseries(dict["Amount (MW)"]),
|
||||
units = [],
|
||||
thermal_units = [],
|
||||
shortfall_penalty = scalar(
|
||||
dict["Shortfall penalty (\$/MW)"],
|
||||
default = -1,
|
||||
@@ -207,15 +209,25 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
|
||||
# Read units
|
||||
for (unit_name, dict) in json["Generators"]
|
||||
# Read and validate unit type
|
||||
unit_type = scalar(dict["Type"], default = nothing)
|
||||
unit_type !== nothing || error("unit $unit_name has no type specified")
|
||||
bus = name_to_bus[dict["Bus"]]
|
||||
|
||||
if lowercase(unit_type) === "thermal"
|
||||
# Read production cost curve
|
||||
K = length(dict["Production cost curve (MW)"])
|
||||
curve_mw = hcat(
|
||||
[timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]...,
|
||||
[
|
||||
timeseries(dict["Production cost curve (MW)"][k]) for
|
||||
k in 1:K
|
||||
]...,
|
||||
)
|
||||
curve_cost = hcat(
|
||||
[timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...,
|
||||
[
|
||||
timeseries(dict["Production cost curve (\$)"][k]) for
|
||||
k in 1:K
|
||||
]...,
|
||||
)
|
||||
min_power = curve_mw[:, 1]
|
||||
max_power = curve_mw[:, K]
|
||||
@@ -250,14 +262,18 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
end
|
||||
|
||||
# Read and validate initial conditions
|
||||
initial_power = scalar(dict["Initial power (MW)"], default = nothing)
|
||||
initial_status = scalar(dict["Initial status (h)"], default = nothing)
|
||||
initial_power =
|
||||
scalar(dict["Initial power (MW)"], default = nothing)
|
||||
initial_status =
|
||||
scalar(dict["Initial status (h)"], default = nothing)
|
||||
if initial_power === nothing
|
||||
initial_status === nothing ||
|
||||
error("unit $unit_name has initial status but no initial power")
|
||||
initial_status === nothing || error(
|
||||
"unit $unit_name has initial status but no initial power",
|
||||
)
|
||||
else
|
||||
initial_status !== nothing ||
|
||||
error("unit $unit_name has initial power but no initial status")
|
||||
initial_status !== nothing || error(
|
||||
"unit $unit_name has initial power but no initial status",
|
||||
)
|
||||
initial_status != 0 ||
|
||||
error("unit $unit_name has invalid initial status")
|
||||
if initial_status < 0 && initial_power > 1e-3
|
||||
@@ -266,7 +282,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
initial_status *= time_multiplier
|
||||
end
|
||||
|
||||
unit = Unit(
|
||||
unit = ThermalUnit(
|
||||
unit_name,
|
||||
bus,
|
||||
max_power,
|
||||
@@ -274,8 +290,10 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
timeseries(dict["Must run?"], default = [false for t in 1:T]),
|
||||
min_power_cost,
|
||||
segments,
|
||||
scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier,
|
||||
scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier,
|
||||
scalar(dict["Minimum uptime (h)"], default = 1) *
|
||||
time_multiplier,
|
||||
scalar(dict["Minimum downtime (h)"], default = 1) *
|
||||
time_multiplier,
|
||||
scalar(dict["Ramp up limit (MW)"], default = 1e6),
|
||||
scalar(dict["Ramp down limit (MW)"], default = 1e6),
|
||||
scalar(dict["Startup limit (MW)"], default = 1e6),
|
||||
@@ -285,12 +303,25 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
startup_categories,
|
||||
unit_reserves,
|
||||
)
|
||||
push!(bus.units, unit)
|
||||
push!(bus.thermal_units, unit)
|
||||
for r in unit_reserves
|
||||
push!(r.units, unit)
|
||||
push!(r.thermal_units, unit)
|
||||
end
|
||||
name_to_unit[unit_name] = unit
|
||||
push!(units, unit)
|
||||
push!(thermal_units, unit)
|
||||
elseif lowercase(unit_type) === "profiled"
|
||||
bus = name_to_bus[dict["Bus"]]
|
||||
pu = ProfiledUnit(
|
||||
unit_name,
|
||||
bus,
|
||||
timeseries(dict["Maximum power (MW)"]),
|
||||
timeseries(dict["Cost (\$/MW)"]),
|
||||
)
|
||||
push!(bus.profiled_units, pu)
|
||||
push!(profiled_units, pu)
|
||||
else
|
||||
error("unit $unit_name has an invalid type")
|
||||
end
|
||||
end
|
||||
|
||||
# Read transmission lines
|
||||
@@ -324,7 +355,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
# Read contingencies
|
||||
if "Contingencies" in keys(json)
|
||||
for (cont_name, dict) in json["Contingencies"]
|
||||
affected_units = Unit[]
|
||||
affected_units = ThermalUnit[]
|
||||
affected_lines = TransmissionLine[]
|
||||
if "Affected lines" in keys(dict)
|
||||
affected_lines =
|
||||
@@ -369,8 +400,10 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
reserves = reserves,
|
||||
reserves_by_name = name_to_reserve,
|
||||
time = T,
|
||||
units_by_name = Dict(g.name => g for g in units),
|
||||
units = units,
|
||||
thermal_units_by_name = Dict(g.name => g for g in thermal_units),
|
||||
thermal_units = thermal_units,
|
||||
profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units),
|
||||
profiled_units = profiled_units,
|
||||
isf = spzeros(Float64, length(lines), length(buses) - 1),
|
||||
lodf = spzeros(Float64, length(lines), length(lines)),
|
||||
)
|
||||
|
||||
@@ -6,8 +6,9 @@ mutable struct Bus
|
||||
name::String
|
||||
offset::Int
|
||||
load::Vector{Float64}
|
||||
units::Vector
|
||||
thermal_units::Vector
|
||||
price_sensitive_loads::Vector
|
||||
profiled_units::Vector
|
||||
end
|
||||
|
||||
mutable struct CostSegment
|
||||
@@ -24,11 +25,11 @@ Base.@kwdef mutable struct Reserve
|
||||
name::String
|
||||
type::String
|
||||
amount::Vector{Float64}
|
||||
units::Vector
|
||||
thermal_units::Vector
|
||||
shortfall_penalty::Float64
|
||||
end
|
||||
|
||||
mutable struct Unit
|
||||
mutable struct ThermalUnit
|
||||
name::String
|
||||
bus::Bus
|
||||
max_power::Vector{Float64}
|
||||
@@ -63,7 +64,7 @@ end
|
||||
mutable struct Contingency
|
||||
name::String
|
||||
lines::Vector{TransmissionLine}
|
||||
units::Vector{Unit}
|
||||
thermal_units::Vector{ThermalUnit}
|
||||
end
|
||||
|
||||
mutable struct PriceSensitiveLoad
|
||||
@@ -73,6 +74,13 @@ mutable struct PriceSensitiveLoad
|
||||
revenue::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct ProfiledUnit
|
||||
name::String
|
||||
bus::Bus
|
||||
capacity::Vector{Float64}
|
||||
cost::Vector{Float64}
|
||||
end
|
||||
|
||||
Base.@kwdef mutable struct UnitCommitmentScenario
|
||||
buses_by_name::Dict{AbstractString,Bus}
|
||||
buses::Vector{Bus}
|
||||
@@ -87,11 +95,13 @@ Base.@kwdef mutable struct UnitCommitmentScenario
|
||||
price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad}
|
||||
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
||||
probability::Float64
|
||||
profiled_units_by_name::Dict{AbstractString,ProfiledUnit}
|
||||
profiled_units::Vector{ProfiledUnit}
|
||||
reserves_by_name::Dict{AbstractString,Reserve}
|
||||
reserves::Vector{Reserve}
|
||||
thermal_units_by_name::Dict{AbstractString,ThermalUnit}
|
||||
thermal_units::Vector{ThermalUnit}
|
||||
time::Int
|
||||
units_by_name::Dict{AbstractString,Unit}
|
||||
units::Vector{Unit}
|
||||
end
|
||||
|
||||
Base.@kwdef mutable struct UnitCommitmentInstance
|
||||
@@ -103,7 +113,8 @@ function Base.show(io::IO, instance::UnitCommitmentInstance)
|
||||
sc = instance.scenarios[1]
|
||||
print(io, "UnitCommitmentInstance(")
|
||||
print(io, "$(length(instance.scenarios)) scenarios, ")
|
||||
print(io, "$(length(sc.units)) units, ")
|
||||
print(io, "$(length(sc.thermal_units)) thermal units, ")
|
||||
print(io, "$(length(sc.profiled_units)) profiled units, ")
|
||||
print(io, "$(length(sc.buses)) buses, ")
|
||||
print(io, "$(length(sc.lines)) lines, ")
|
||||
print(io, "$(length(sc.contingencies)) contingencies, ")
|
||||
|
||||
212
src/lmp/aelmp.jl
Normal file
212
src/lmp/aelmp.jl
Normal file
@@ -0,0 +1,212 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using JuMP
|
||||
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
method::AELMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,Int},Float64}
|
||||
|
||||
Calculates the approximate extended locational marginal prices of the given unit commitment instance.
|
||||
|
||||
The AELPM does the following three things:
|
||||
|
||||
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
|
||||
3. It relaxes all integrality constraints
|
||||
|
||||
Returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||
|
||||
WARNING: This approximation method is not fully developed. The implementation is based on MISO Phase I only.
|
||||
|
||||
1. It only supports Fast Start resources. More specifically, the minimum up/down time has to be zero.
|
||||
2. The method does NOT support time-varying start-up costs.
|
||||
3. An asset is considered offline if it is never on throughout all time periods.
|
||||
4. The method does NOT support multiple scenarios.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
- `model`:
|
||||
the UnitCommitment model, must be solved before calling this function if offline participation is not allowed.
|
||||
|
||||
- `method`:
|
||||
the AELMP method.
|
||||
|
||||
- `optimizer`:
|
||||
the optimizer for solving the LP problem.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```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]
|
||||
```
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
method::AELMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||
@info "Building the approximation model..."
|
||||
instance = deepcopy(model[:instance])
|
||||
_aelmp_check_parameters(instance, model, method)
|
||||
_modify_scenario!(instance.scenarios[1], model, method)
|
||||
|
||||
# prepare the result dictionary and solve the model
|
||||
elmp = OrderedDict()
|
||||
@info "Solving the approximation model."
|
||||
approx_model = build_model(instance = instance, variable_names = true)
|
||||
|
||||
# relax the binary constraint, and relax integrality
|
||||
for v in all_variables(approx_model)
|
||||
if is_binary(v)
|
||||
unset_binary(v)
|
||||
end
|
||||
end
|
||||
relax_integrality(approx_model)
|
||||
set_optimizer(approx_model, optimizer)
|
||||
|
||||
# solve the model
|
||||
set_silent(approx_model)
|
||||
optimize!(approx_model)
|
||||
|
||||
# access the dual values
|
||||
@info "Getting dual values (AELMPs)."
|
||||
for (key, val) in approx_model[:eq_net_injection]
|
||||
elmp[key] = dual(val)
|
||||
end
|
||||
return elmp
|
||||
end
|
||||
|
||||
function _aelmp_check_parameters(
|
||||
instance::UnitCommitmentInstance,
|
||||
model::JuMP.Model,
|
||||
method::AELMP,
|
||||
)
|
||||
# CHECK: model cannot have multiple scenarios
|
||||
if length(instance.scenarios) > 1
|
||||
error("The method does NOT support multiple scenarios.")
|
||||
end
|
||||
sc = instance.scenarios[1]
|
||||
# CHECK: model must be solved if allow_offline_participation=false
|
||||
if !method.allow_offline_participation
|
||||
if isnothing(model) || !has_values(model)
|
||||
error(
|
||||
"A solved UC model is required if allow_offline_participation=false.",
|
||||
)
|
||||
end
|
||||
end
|
||||
all_units = sc.thermal_units
|
||||
# CHECK: model cannot handle non-fast-starts (MISO Phase I: can ONLY solve fast-starts)
|
||||
if any(u -> u.min_uptime > 1 || u.min_downtime > 1, all_units)
|
||||
error(
|
||||
"The minimum up/down time of all generators must be 1. AELMP only supports fast-starts.",
|
||||
)
|
||||
end
|
||||
if any(u -> u.initial_power > 0, all_units)
|
||||
error("The initial power of all generators must be 0.")
|
||||
end
|
||||
if any(u -> u.initial_status >= 0, all_units)
|
||||
error("The initial status of all generators must be negative.")
|
||||
end
|
||||
# CHECK: model does not support startup costs (in time series)
|
||||
if any(u -> length(u.startup_categories) > 1, all_units)
|
||||
error("The method does NOT support time-varying start-up costs.")
|
||||
end
|
||||
end
|
||||
|
||||
function _modify_scenario!(
|
||||
sc::UnitCommitmentScenario,
|
||||
model::JuMP.Model,
|
||||
method::AELMP,
|
||||
)
|
||||
# this function modifies the sc units (generators)
|
||||
if !method.allow_offline_participation
|
||||
# 1. remove (if NOT allowing) the offline generators
|
||||
units_to_remove = []
|
||||
for unit in sc.thermal_units
|
||||
# remove based on the solved UC model result
|
||||
# remove the unit if it is never on
|
||||
if all(t -> value(model[:is_on][unit.name, t]) == 0, sc.time)
|
||||
# unregister from the bus
|
||||
filter!(x -> x.name != unit.name, unit.bus.thermal_units)
|
||||
# unregister from the reserve
|
||||
for r in unit.reserves
|
||||
filter!(x -> x.name != unit.name, r.thermal_units)
|
||||
end
|
||||
# append the name to the remove list
|
||||
push!(units_to_remove, unit.name)
|
||||
end
|
||||
end
|
||||
# unregister the units from the remove list
|
||||
filter!(x -> !(x.name in units_to_remove), sc.thermal_units)
|
||||
end
|
||||
|
||||
for unit in sc.thermal_units
|
||||
# 2. set min generation requirement to 0 by adding 0 to production curve and cost
|
||||
# min_power & min_costs are vectors with dimension T
|
||||
if unit.min_power[1] != 0
|
||||
first_cost_segment = unit.cost_segments[1]
|
||||
pushfirst!(
|
||||
unit.cost_segments,
|
||||
CostSegment(
|
||||
ones(size(first_cost_segment.mw)) * unit.min_power[1],
|
||||
ones(size(first_cost_segment.cost)) *
|
||||
unit.min_power_cost[1] / unit.min_power[1],
|
||||
),
|
||||
)
|
||||
unit.min_power = zeros(size(first_cost_segment.mw))
|
||||
unit.min_power_cost = zeros(size(first_cost_segment.cost))
|
||||
end
|
||||
|
||||
# 3. average the start-up costs (if considering)
|
||||
# if consider_startup_costs = false, then use the current first_startup_cost
|
||||
first_startup_cost = unit.startup_categories[1].cost
|
||||
if method.consider_startup_costs
|
||||
additional_unit_cost = first_startup_cost / unit.max_power[1]
|
||||
for i in eachindex(unit.cost_segments)
|
||||
unit.cost_segments[i].cost .+= additional_unit_cost
|
||||
end
|
||||
first_startup_cost = 0.0 # zero out the start up cost
|
||||
end
|
||||
unit.startup_categories =
|
||||
StartupCategory[StartupCategory(0, first_startup_cost)]
|
||||
end
|
||||
return sc.thermal_units_by_name =
|
||||
Dict(g.name => g for g in sc.thermal_units)
|
||||
end
|
||||
92
src/lmp/conventional.jl
Normal file
92
src/lmp/conventional.jl
Normal file
@@ -0,0 +1,92 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using JuMP
|
||||
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
method::ConventionalLMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||
|
||||
Calculates conventional locational marginal prices of the given unit commitment
|
||||
instance. Returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
- `model`:
|
||||
the UnitCommitment model, must be solved before calling this function.
|
||||
|
||||
- `method`:
|
||||
the LMP method.
|
||||
|
||||
- `optimizer`:
|
||||
the optimizer for solving the LP problem.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```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]
|
||||
```
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
::ConventionalLMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||
if !has_values(model)
|
||||
error("The UC model must be solved before calculating the LMPs.")
|
||||
end
|
||||
lmp = OrderedDict()
|
||||
|
||||
@info "Fixing binary variables and relaxing integrality..."
|
||||
vals = Dict(v => value(v) for v in all_variables(model))
|
||||
for v in all_variables(model)
|
||||
if is_binary(v)
|
||||
unset_binary(v)
|
||||
fix(v, vals[v])
|
||||
end
|
||||
end
|
||||
relax_integrality(model)
|
||||
set_optimizer(model, optimizer)
|
||||
|
||||
@info "Solving the LP..."
|
||||
JuMP.optimize!(model)
|
||||
|
||||
@info "Getting dual values (LMPs)..."
|
||||
for (key, val) in model[:eq_net_injection]
|
||||
lmp[key] = dual(val)
|
||||
end
|
||||
|
||||
return lmp
|
||||
end
|
||||
28
src/lmp/structs.jl
Normal file
28
src/lmp/structs.jl
Normal file
@@ -0,0 +1,28 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
abstract type PricingMethod end
|
||||
|
||||
struct ConventionalLMP <: PricingMethod end
|
||||
|
||||
"""
|
||||
struct AELMP <: PricingMethod
|
||||
allow_offline_participation::Bool = true
|
||||
consider_startup_costs::Bool = true
|
||||
end
|
||||
|
||||
Approximate Extended LMPs.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
- `allow_offline_participation`:
|
||||
If true, offline assets are allowed to participate in pricing.
|
||||
- `consider_startup_costs`:
|
||||
If true, the start-up costs are averaged over each unit production; otherwise the production costs stay the same.
|
||||
"""
|
||||
Base.@kwdef struct AELMP <: PricingMethod
|
||||
allow_offline_participation::Bool = true
|
||||
consider_startup_costs::Bool = true
|
||||
end
|
||||
@@ -77,7 +77,7 @@ function build_model(;
|
||||
end
|
||||
model[:obj] = AffExpr()
|
||||
model[:instance] = instance
|
||||
for g in instance.scenarios[1].units
|
||||
for g in instance.scenarios[1].thermal_units
|
||||
_add_unit_commitment!(model, g, formulation)
|
||||
end
|
||||
for sc in instance.scenarios
|
||||
@@ -93,9 +93,12 @@ function build_model(;
|
||||
for ps in sc.price_sensitive_loads
|
||||
_add_price_sensitive_load!(model, ps, sc)
|
||||
end
|
||||
for g in sc.units
|
||||
for g in sc.thermal_units
|
||||
_add_unit_dispatch!(model, g, formulation, sc)
|
||||
end
|
||||
for pu in sc.profiled_units
|
||||
_add_profiled_unit!(model, pu, sc)
|
||||
end
|
||||
_add_system_wide_eqs!(model, sc)
|
||||
end
|
||||
@objective(model, Min, model[:obj])
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::ArrCon2000.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_production_piecewise_linear_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_pwl_costs::CarArr2006.PwlCosts,
|
||||
formulation_status_vars::StatusVarsFormulation,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::DamKucRajAta2016.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_production_vars!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
@@ -21,7 +21,7 @@ end
|
||||
|
||||
function _add_production_limit_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_production_piecewise_linear_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_pwl_costs::Gar1962.PwlCosts,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_status_vars!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
)::Nothing
|
||||
is_on = _init(model, :is_on)
|
||||
@@ -27,7 +27,7 @@ end
|
||||
|
||||
function _add_status_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
)::Nothing
|
||||
eq_binary_link = _init(model, :eq_binary_link)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_production_piecewise_linear_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_pwl_costs::KnuOstWat2018.PwlCosts,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::MorLatRam2013.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_startup_cost_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation::MorLatRam2013.StartupCosts,
|
||||
)::Nothing
|
||||
eq_startup_choose = _init(model, :eq_startup_choose)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::PanGua2016.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
::Gar1962.ProdVars,
|
||||
::WanHob2016.Ramping,
|
||||
::Gar1962.StatusVars,
|
||||
|
||||
32
src/model/formulations/base/punit.jl
Normal file
32
src/model/formulations/base/punit.jl
Normal file
@@ -0,0 +1,32 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function _add_profiled_unit!(
|
||||
model::JuMP.Model,
|
||||
pu::ProfiledUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
punits = _init(model, :prod_profiled)
|
||||
net_injection = _init(model, :expr_net_injection)
|
||||
for t in 1:model[:instance].time
|
||||
# Decision variable
|
||||
punits[sc.name, pu.name, t] =
|
||||
@variable(model, lower_bound = 0, upper_bound = pu.capacity[t])
|
||||
|
||||
# Objective function terms
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
punits[sc.name, pu.name, t],
|
||||
pu.cost[t] * sc.probability,
|
||||
)
|
||||
|
||||
# Net injection
|
||||
add_to_expression!(
|
||||
net_injection[sc.name, pu.bus.name, t],
|
||||
punits[sc.name, pu.name, t],
|
||||
1.0,
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -53,7 +53,7 @@ function _add_spinning_reserve_eqs!(
|
||||
model,
|
||||
sum(
|
||||
model[:reserve][sc.name, r.name, g.name, t] for
|
||||
g in r.units
|
||||
g in r.thermal_units
|
||||
) + model[:reserve_shortfall][sc.name, r.name, t] >=
|
||||
r.amount[t]
|
||||
)
|
||||
@@ -91,7 +91,7 @@ function _add_flexiramp_reserve_eqs!(
|
||||
model,
|
||||
sum(
|
||||
model[:upflexiramp][sc.name, r.name, g.name, t] for
|
||||
g in r.units
|
||||
g in r.thermal_units
|
||||
) + model[:upflexiramp_shortfall][sc.name, r.name, t] >=
|
||||
r.amount[t]
|
||||
)
|
||||
@@ -100,7 +100,7 @@ function _add_flexiramp_reserve_eqs!(
|
||||
model,
|
||||
sum(
|
||||
model[:dwflexiramp][sc.name, r.name, g.name, t] for
|
||||
g in r.units
|
||||
g in r.thermal_units
|
||||
) + model[:dwflexiramp_shortfall][sc.name, r.name, t] >=
|
||||
r.amount[t]
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# related to the binary commitment, startup and shutdown decisions of units
|
||||
function _add_unit_commitment!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation::Formulation,
|
||||
)
|
||||
if !all(g.must_run) && any(g.must_run)
|
||||
@@ -31,7 +31,7 @@ end
|
||||
# related to the continuous dispatch decisions of units
|
||||
function _add_unit_dispatch!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation::Formulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)
|
||||
@@ -64,11 +64,11 @@ function _add_unit_dispatch!(
|
||||
return
|
||||
end
|
||||
|
||||
_is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
|
||||
_is_initially_on(g::ThermalUnit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
|
||||
|
||||
function _add_spinning_reserve_vars!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
reserve = _init(model, :reserve)
|
||||
@@ -92,7 +92,7 @@ end
|
||||
|
||||
function _add_flexiramp_reserve_vars!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
upflexiramp = _init(model, :upflexiramp)
|
||||
@@ -128,7 +128,7 @@ function _add_flexiramp_reserve_vars!(
|
||||
return
|
||||
end
|
||||
|
||||
function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_startup_shutdown_vars!(model::JuMP.Model, g::ThermalUnit)::Nothing
|
||||
startup = _init(model, :startup)
|
||||
for t in 1:model[:instance].time
|
||||
for s in 1:length(g.startup_categories)
|
||||
@@ -140,7 +140,7 @@ end
|
||||
|
||||
function _add_startup_shutdown_limit_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
eq_shutdown_limit = _init(model, :eq_shutdown_limit)
|
||||
@@ -179,7 +179,7 @@ end
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation::RampingFormulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
@@ -224,7 +224,10 @@ function _add_ramp_eqs!(
|
||||
end
|
||||
end
|
||||
|
||||
function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_min_uptime_downtime_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
)::Nothing
|
||||
is_on = model[:is_on]
|
||||
switch_off = model[:switch_off]
|
||||
switch_on = model[:switch_on]
|
||||
@@ -269,7 +272,7 @@ end
|
||||
|
||||
function _add_net_injection_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
expr_net_injection = model[:expr_net_injection]
|
||||
|
||||
@@ -10,17 +10,17 @@ solution. Useful for computing LMPs.
|
||||
"""
|
||||
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
"Production (MW)" ∈ keys(solution) ? solution = Dict("s1" => solution) :
|
||||
nothing
|
||||
"Thermal production (MW)" ∈ keys(solution) ?
|
||||
solution = Dict("s1" => solution) : nothing
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = model[:reserve]
|
||||
for sc in instance.scenarios
|
||||
for g in sc.units
|
||||
for g in sc.thermal_units
|
||||
for t in 1:T
|
||||
is_on_value = round(solution[sc.name]["Is on"][g.name][t])
|
||||
prod_value = round(
|
||||
solution[sc.name]["Production (MW)"][g.name][t],
|
||||
solution[sc.name]["Thermal production (MW)"][g.name][t],
|
||||
digits = 5,
|
||||
)
|
||||
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
|
||||
@@ -33,7 +33,7 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
end
|
||||
for r in sc.reserves
|
||||
r.type == "spinning" || continue
|
||||
for g in r.units
|
||||
for g in r.thermal_units
|
||||
for t in 1:T
|
||||
reserve_value = round(
|
||||
solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t],
|
||||
|
||||
@@ -65,19 +65,26 @@ function solution(model::JuMP.Model)::OrderedDict
|
||||
sol = OrderedDict()
|
||||
for sc in instance.scenarios
|
||||
sol[sc.name] = OrderedDict()
|
||||
sol[sc.name]["Production (MW)"] =
|
||||
OrderedDict(g.name => production(g, sc) for g in sc.units)
|
||||
sol[sc.name]["Production cost (\$)"] =
|
||||
OrderedDict(g.name => production_cost(g, sc) for g in sc.units)
|
||||
sol[sc.name]["Startup cost (\$)"] =
|
||||
OrderedDict(g.name => startup_cost(g, sc) for g in sc.units)
|
||||
sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units)
|
||||
sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units)
|
||||
sol[sc.name]["Switch off"] = timeseries(model[:switch_off], sc.units)
|
||||
if !isempty(sc.thermal_units)
|
||||
sol[sc.name]["Thermal production (MW)"] = OrderedDict(
|
||||
g.name => production(g, sc) for g in sc.thermal_units
|
||||
)
|
||||
sol[sc.name]["Thermal production cost (\$)"] = OrderedDict(
|
||||
g.name => production_cost(g, sc) for g in sc.thermal_units
|
||||
)
|
||||
sol[sc.name]["Startup cost (\$)"] = OrderedDict(
|
||||
g.name => startup_cost(g, sc) for g in sc.thermal_units
|
||||
)
|
||||
sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.thermal_units)
|
||||
sol[sc.name]["Switch on"] =
|
||||
timeseries(model[:switch_on], sc.thermal_units)
|
||||
sol[sc.name]["Switch off"] =
|
||||
timeseries(model[:switch_off], sc.thermal_units)
|
||||
sol[sc.name]["Net injection (MW)"] =
|
||||
timeseries(model[:net_injection], sc.buses, sc = sc)
|
||||
sol[sc.name]["Load curtail (MW)"] =
|
||||
timeseries(model[:curtail], sc.buses, sc = sc)
|
||||
end
|
||||
if !isempty(sc.lines)
|
||||
sol[sc.name]["Line overflow (MW)"] =
|
||||
timeseries(model[:overflow], sc.lines, sc = sc)
|
||||
@@ -86,11 +93,21 @@ function solution(model::JuMP.Model)::OrderedDict
|
||||
sol[sc.name]["Price-sensitive loads (MW)"] =
|
||||
timeseries(model[:loads], sc.price_sensitive_loads, sc = sc)
|
||||
end
|
||||
if !isempty(sc.profiled_units)
|
||||
sol[sc.name]["Profiled production (MW)"] =
|
||||
timeseries(model[:prod_profiled], sc.profiled_units, sc = sc)
|
||||
sol[sc.name]["Profiled production cost (\$)"] = OrderedDict(
|
||||
pu.name => [
|
||||
value(model[:prod_profiled][sc.name, pu.name, t]) *
|
||||
pu.cost[t] for t in 1:instance.time
|
||||
] for pu in sc.profiled_units
|
||||
)
|
||||
end
|
||||
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:reserve][sc.name, r.name, g.name, t]) for t in 1:instance.time
|
||||
] for g in r.units
|
||||
] for g in r.thermal_units
|
||||
) for r in sc.reserves if r.type == "spinning"
|
||||
)
|
||||
sol[sc.name]["Spinning reserve shortfall (MW)"] = OrderedDict(
|
||||
@@ -103,7 +120,7 @@ function solution(model::JuMP.Model)::OrderedDict
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:upflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time
|
||||
] for g in r.units
|
||||
] for g in r.thermal_units
|
||||
) for r in sc.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol[sc.name]["Up-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
@@ -115,7 +132,7 @@ function solution(model::JuMP.Model)::OrderedDict
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:dwflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time
|
||||
] for g in r.units
|
||||
] for g in r.thermal_units
|
||||
) for r in sc.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol[sc.name]["Down-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
is_on = model[:is_on]
|
||||
for g in instance.units
|
||||
for g in instance.thermal_units
|
||||
for t in 1:T
|
||||
JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][t])
|
||||
JuMP.set_start_value(
|
||||
|
||||
@@ -15,7 +15,7 @@ function generate_initial_conditions!(
|
||||
sc::UnitCommitmentScenario,
|
||||
optimizer,
|
||||
)::Nothing
|
||||
G = sc.units
|
||||
G = sc.thermal_units
|
||||
B = sc.buses
|
||||
t = 1
|
||||
mip = JuMP.Model(optimizer)
|
||||
|
||||
@@ -123,7 +123,7 @@ function _randomize_costs(
|
||||
sc::UnitCommitmentScenario,
|
||||
distribution,
|
||||
)::Nothing
|
||||
for unit in sc.units
|
||||
for unit in sc.thermal_units
|
||||
α = rand(rng, distribution)
|
||||
unit.min_power_cost *= α
|
||||
for k in unit.cost_segments
|
||||
@@ -168,7 +168,7 @@ function _randomize_load_profile(
|
||||
)
|
||||
push!(system_load, system_load[t-1] * gamma)
|
||||
end
|
||||
capacity = sum(maximum(u.max_power) for u in sc.units)
|
||||
capacity = sum(maximum(u.max_power) for u in sc.thermal_units)
|
||||
peak_load = rand(rng, params.peak_load) * capacity
|
||||
system_load = system_load ./ maximum(system_load) .* peak_load
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function slice(
|
||||
for r in sc.reserves
|
||||
r.amount = r.amount[range]
|
||||
end
|
||||
for u in sc.units
|
||||
for u in sc.thermal_units
|
||||
u.max_power = u.max_power[range]
|
||||
u.min_power = u.min_power[range]
|
||||
u.must_run = u.must_run[range]
|
||||
|
||||
@@ -15,7 +15,7 @@ Returns the number of validation errors found.
|
||||
function repair!(sc::UnitCommitmentScenario)::Int
|
||||
n_errors = 0
|
||||
|
||||
for g in sc.units
|
||||
for g in sc.thermal_units
|
||||
|
||||
# Startup costs and delays must be increasing
|
||||
for s in 2:length(g.startup_categories)
|
||||
|
||||
@@ -28,8 +28,8 @@ function validate(
|
||||
instance::UnitCommitmentInstance,
|
||||
solution::Union{Dict,OrderedDict},
|
||||
)::Bool
|
||||
"Production (MW)" ∈ keys(solution) ? solution = Dict("s1" => solution) :
|
||||
nothing
|
||||
"Thermal production (MW)" ∈ keys(solution) ?
|
||||
solution = Dict("s1" => solution) : nothing
|
||||
err_count = 0
|
||||
err_count += _validate_units(instance, solution)
|
||||
err_count += _validate_reserve_and_demand(instance, solution)
|
||||
@@ -45,8 +45,8 @@ end
|
||||
function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
|
||||
err_count = 0
|
||||
for sc in instance.scenarios
|
||||
for unit in sc.units
|
||||
production = solution[sc.name]["Production (MW)"][unit.name]
|
||||
for unit in sc.thermal_units
|
||||
production = solution[sc.name]["Thermal production (MW)"][unit.name]
|
||||
reserve = [0.0 for _ in 1:instance.time]
|
||||
spinning_reserves =
|
||||
[r for r in unit.reserves if r.type == "spinning"]
|
||||
@@ -57,7 +57,7 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
|
||||
)
|
||||
end
|
||||
actual_production_cost =
|
||||
solution[sc.name]["Production cost (\$)"][unit.name]
|
||||
solution[sc.name]["Thermal production cost (\$)"][unit.name]
|
||||
actual_startup_cost =
|
||||
solution[sc.name]["Startup cost (\$)"][unit.name]
|
||||
is_on = bin(solution[sc.name]["Is on"][unit.name])
|
||||
@@ -114,7 +114,7 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
|
||||
# Verify reserve eligibility
|
||||
for r in sc.reserves
|
||||
if r.type == "spinning"
|
||||
if unit ∉ r.units && (
|
||||
if unit ∉ r.thermal_units && (
|
||||
unit in keys(
|
||||
solution[sc.name]["Spinning reserve (MW)"][r.name],
|
||||
)
|
||||
@@ -323,8 +323,8 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
||||
)
|
||||
end
|
||||
production = sum(
|
||||
solution[sc.name]["Production (MW)"][g.name][t] for
|
||||
g in sc.units
|
||||
solution[sc.name]["Thermal production (MW)"][g.name][t] for
|
||||
g in sc.thermal_units
|
||||
)
|
||||
if "Load curtail (MW)" in keys(solution)
|
||||
load_curtail = sum(
|
||||
@@ -352,7 +352,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
||||
if r.type == "spinning"
|
||||
provided = sum(
|
||||
solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t]
|
||||
for g in r.units
|
||||
for g in r.thermal_units
|
||||
)
|
||||
shortfall =
|
||||
solution[sc.name]["Spinning reserve shortfall (MW)"][r.name][t]
|
||||
@@ -371,7 +371,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
||||
elseif r.type == "flexiramp"
|
||||
upflexiramp = sum(
|
||||
solution[sc.name]["Up-flexiramp (MW)"][r.name][g.name][t]
|
||||
for g in r.units
|
||||
for g in r.thermal_units
|
||||
)
|
||||
upflexiramp_shortfall =
|
||||
solution[sc.name]["Up-flexiramp shortfall (MW)"][r.name][t]
|
||||
@@ -389,7 +389,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
||||
|
||||
dwflexiramp = sum(
|
||||
solution[sc.name]["Down-flexiramp (MW)"][r.name][g.name][t]
|
||||
for g in r.units
|
||||
for g in r.thermal_units
|
||||
)
|
||||
dwflexiramp_shortfall =
|
||||
solution[sc.name]["Down-flexiramp shortfall (MW)"][r.name][t]
|
||||
|
||||
@@ -3,6 +3,7 @@ Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
||||
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
|
||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
|
||||
BIN
test/fixtures/aelmp_simple.json.gz
vendored
Normal file
BIN
test/fixtures/aelmp_simple.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14-profiled.json.gz
vendored
Normal file
BIN
test/fixtures/case14-profiled.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_1.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_1.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_2.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_2.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_3.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_3.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_4.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_4.json.gz
vendored
Normal file
Binary file not shown.
@@ -7,12 +7,13 @@ using UnitCommitment
|
||||
@testset "read_egret_solution" begin
|
||||
solution =
|
||||
UnitCommitment.read_egret_solution("$FIXTURES/egret_output.json.gz")
|
||||
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
||||
for attr in
|
||||
["Is on", "Thermal production (MW)", "Thermal production cost (\$)"]
|
||||
@test attr in keys(solution)
|
||||
@test "115_STEAM_1" in keys(solution[attr])
|
||||
@test length(solution[attr]["115_STEAM_1"]) == 48
|
||||
end
|
||||
@test solution["Production cost (\$)"]["315_CT_6"][15:20] ==
|
||||
@test solution["Thermal production cost (\$)"]["315_CT_6"][15:20] ==
|
||||
[0.0, 0.0, 884.44, 1470.71, 1470.71, 884.44]
|
||||
@test solution["Startup cost (\$)"]["315_CT_6"][15:20] ==
|
||||
[0.0, 0.0, 5665.23, 0.0, 0.0, 0.0]
|
||||
|
||||
@@ -9,14 +9,14 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
@test length(instance.scenarios) == 1
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.reserves_by_name["r1"].amount) == 4
|
||||
@test sc.units_by_name["g2"].reserves[1].name == "r1"
|
||||
@test sc.thermal_units_by_name["g2"].reserves[1].name == "r1"
|
||||
end
|
||||
|
||||
@testset "read v0.3" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/ucjl-0.3.json.gz")
|
||||
@test length(instance.scenarios) == 1
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.units) == 6
|
||||
@test length(sc.thermal_units) == 6
|
||||
@test length(sc.buses) == 14
|
||||
@test length(sc.lines) == 20
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
|
||||
@test repr(instance) == (
|
||||
"UnitCommitmentInstance(1 scenarios, 6 units, 14 buses, " *
|
||||
"UnitCommitmentInstance(1 scenarios, 6 thermal units, 0 profiled units, 14 buses, " *
|
||||
"20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.lines) == 20
|
||||
@test length(sc.buses) == 14
|
||||
@test length(sc.units) == 6
|
||||
@test length(sc.thermal_units) == 6
|
||||
@test length(sc.contingencies) == 19
|
||||
@test length(sc.price_sensitive_loads) == 1
|
||||
@test instance.time == 4
|
||||
@@ -49,7 +49,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
@test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0]
|
||||
@test sc.reserves_by_name["r1"].name == "r1"
|
||||
|
||||
unit = sc.units[1]
|
||||
unit = sc.thermal_units[1]
|
||||
@test unit.name == "g1"
|
||||
@test unit.bus.name == "b1"
|
||||
@test unit.ramp_up_limit == 1e6
|
||||
@@ -76,14 +76,14 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
@test unit.startup_categories[2].cost == 1500.0
|
||||
@test unit.startup_categories[3].cost == 2000.0
|
||||
@test length(unit.reserves) == 0
|
||||
@test sc.units_by_name["g1"].name == "g1"
|
||||
@test sc.thermal_units_by_name["g1"].name == "g1"
|
||||
|
||||
unit = sc.units[2]
|
||||
unit = sc.thermal_units[2]
|
||||
@test unit.name == "g2"
|
||||
@test unit.must_run == [false for t in 1:4]
|
||||
@test length(unit.reserves) == 1
|
||||
|
||||
unit = sc.units[3]
|
||||
unit = sc.thermal_units[3]
|
||||
@test unit.name == "g3"
|
||||
@test unit.bus.name == "b3"
|
||||
@test unit.ramp_up_limit == 70.0
|
||||
@@ -106,7 +106,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
@test unit.reserves[1].name == "r1"
|
||||
|
||||
@test sc.contingencies[1].lines == [sc.lines[1]]
|
||||
@test sc.contingencies[1].units == []
|
||||
@test sc.contingencies[1].thermal_units == []
|
||||
@test sc.contingencies[1].name == "c1"
|
||||
@test sc.contingencies_by_name["c1"].name == "c1"
|
||||
|
||||
@@ -121,7 +121,7 @@ end
|
||||
@testset "read_benchmark sub-hourly" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz")
|
||||
@test instance.time == 4
|
||||
unit = instance.scenarios[1].units[1]
|
||||
unit = instance.scenarios[1].thermal_units[1]
|
||||
@test unit.name == "g1"
|
||||
@test unit.min_uptime == 2
|
||||
@test unit.min_downtime == 2
|
||||
@@ -131,3 +131,23 @@ end
|
||||
@test unit.startup_categories[3].delay == 6
|
||||
@test unit.initial_status == -200
|
||||
end
|
||||
|
||||
@testset "read_benchmark profiled-units" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14-profiled.json.gz")
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.profiled_units) == 2
|
||||
|
||||
first_pu = sc.profiled_units[1]
|
||||
@test first_pu.name == "g7"
|
||||
@test first_pu.bus.name == "b4"
|
||||
@test first_pu.cost == [100.0 for t in 1:4]
|
||||
@test first_pu.capacity == [100.0 for t in 1:4]
|
||||
@test sc.profiled_units_by_name["g7"].name == "g7"
|
||||
|
||||
second_pu = sc.profiled_units[2]
|
||||
@test second_pu.name == "g8"
|
||||
@test second_pu.bus.name == "b5"
|
||||
@test second_pu.cost == [50.0 for t in 1:4]
|
||||
@test second_pu.capacity == [120.0 for t in 1:4]
|
||||
@test sc.profiled_units_by_name["g8"].name == "g8"
|
||||
end
|
||||
|
||||
35
test/lmp/aelmp_test.jl
Normal file
35
test/lmp/aelmp_test.jl
Normal file
@@ -0,0 +1,35 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using UnitCommitment, Cbc, HiGHS, JuMP
|
||||
import UnitCommitment: AELMP
|
||||
|
||||
@testset "aelmp" begin
|
||||
path = "$FIXTURES/aelmp_simple.json.gz"
|
||||
# model has to be solved first
|
||||
instance = UnitCommitment.read(path)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
JuMP.set_silent(model)
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# policy 1: allow offlines; consider startups
|
||||
aelmp_1 =
|
||||
UnitCommitment.compute_lmp(model, AELMP(), optimizer = HiGHS.Optimizer)
|
||||
@test aelmp_1["s1", "B1", 1] ≈ 231.7 atol = 0.1
|
||||
|
||||
# policy 2: do not allow offlines; but consider startups
|
||||
aelmp_2 = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
AELMP(
|
||||
allow_offline_participation = false,
|
||||
consider_startup_costs = true,
|
||||
),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
@test aelmp_2["s1", "B1", 1] ≈ 274.3 atol = 0.1
|
||||
end
|
||||
51
test/lmp/conventional_test.jl
Normal file
51
test/lmp/conventional_test.jl
Normal file
@@ -0,0 +1,51 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using UnitCommitment, Cbc, HiGHS, JuMP
|
||||
import UnitCommitment: ConventionalLMP
|
||||
|
||||
function solve_conventional_testcase(path::String)
|
||||
instance = UnitCommitment.read(path)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
JuMP.set_silent(model)
|
||||
UnitCommitment.optimize!(model)
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
ConventionalLMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
return lmp
|
||||
end
|
||||
|
||||
@testset "conventional" begin
|
||||
# instance 1
|
||||
path = "$FIXTURES/lmp_simple_test_1.json.gz"
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 50.0
|
||||
|
||||
# instance 2
|
||||
path = "$FIXTURES/lmp_simple_test_2.json.gz"
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 60.0
|
||||
|
||||
# instance 3
|
||||
path = "$FIXTURES/lmp_simple_test_3.json.gz"
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 70.0
|
||||
@test lmp["s1", "C", 1] == 100.0
|
||||
|
||||
# instance 4
|
||||
path = "$FIXTURES/lmp_simple_test_4.json.gz"
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 70.0
|
||||
@test lmp["s1", "C", 1] == 90.0
|
||||
end
|
||||
@@ -39,4 +39,8 @@ FIXTURES = "$(@__DIR__)/fixtures"
|
||||
@testset "validation" begin
|
||||
include("validation/repair_test.jl")
|
||||
end
|
||||
@testset "lmp" begin
|
||||
include("lmp/conventional_test.jl")
|
||||
include("lmp/aelmp_test.jl")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ using UnitCommitment, Cbc, JuMP
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
sc = instance.scenarios[1]
|
||||
# All units should have unknown initial conditions
|
||||
for g in sc.units
|
||||
for g in sc.thermal_units
|
||||
@test g.initial_power === nothing
|
||||
@test g.initial_status === nothing
|
||||
end
|
||||
@@ -19,7 +19,7 @@ using UnitCommitment, Cbc, JuMP
|
||||
UnitCommitment.generate_initial_conditions!(sc, optimizer)
|
||||
|
||||
# All units should now have known initial conditions
|
||||
for g in sc.units
|
||||
for g in sc.thermal_units
|
||||
@test g.initial_power !== nothing
|
||||
@test g.initial_status !== nothing
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
|
||||
@testset "cost and load share" begin
|
||||
sc = get_scenario()
|
||||
# Check original costs
|
||||
unit = sc.units[10]
|
||||
unit = sc.thermal_units[10]
|
||||
test_approx(unit.min_power_cost[1], 825.023)
|
||||
test_approx(unit.cost_segments[1].cost[1], 36.659)
|
||||
test_approx(unit.startup_categories[1].cost[1], 7570.42)
|
||||
|
||||
@@ -13,7 +13,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
@test modified.time == 2
|
||||
@test length(sc.power_balance_penalty) == 2
|
||||
@test length(sc.reserves_by_name["r1"].amount) == 2
|
||||
for u in sc.units
|
||||
for u in sc.thermal_units
|
||||
@test length(u.max_power) == 2
|
||||
@test length(u.min_power) == 2
|
||||
@test length(u.must_run) == 2
|
||||
|
||||
Reference in New Issue
Block a user