Compare commits

..

7 Commits

Author SHA1 Message Date
7a28a0ca36 Merge tag 'v0.4.2' 2025-11-27 09:14:02 -06:00
e4cc95dae1 Bump version to 0.4.2 2025-11-27 09:01:25 -06:00
48094ded6b KnuOstWat2018: Fix eq_segprod_limit when min_uptime=1 2025-11-27 08:58:43 -06:00
c926f61054 KnuOstWat2018: Fix eq_segprod_limit when min_uptime=1 2025-11-27 08:55:27 -06:00
4ac9b2a8d5 Bump version to 0.4.1 2025-11-05 09:33:30 -06:00
8763c8d8f7 Bump min julia version to 1.10; disable flaky tests 2025-11-05 09:27:55 -06:00
bbe57f88cd Fix some multi-threading issues
Replace nthreads by maxthreadid and use :static scheduling to disable
task migration. Fixes #56.
2025-11-05 09:09:45 -06:00
12 changed files with 68 additions and 59 deletions

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
version: ['1.6', '1.7', '1.8', '1.9']
version: ['1.10', '1.12']
os:
- ubuntu-latest
arch:

View File

@@ -11,6 +11,10 @@ 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.2] - 2025-11-27
### Fixed
- KnuOstWat2018: Fixed a bug in `eq_segprod_limit` constraint (#17)
## [0.4.0] - 2024-05-21
### Added
- Add support for two-stage stochastic problems

View File

@@ -2,7 +2,7 @@ name = "UnitCommitment"
uuid = "64606440-39ea-11e9-0f29-3303a1d3d877"
authors = ["Santos Xavier, Alinson <axavier@anl.gov>"]
repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl"
version = "0.4.0"
version = "0.4.2"
[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
@@ -30,5 +30,5 @@ JuMP = "1"
MathOptInterface = "1"
MPI = "0.20"
PackageCompiler = "1"
julia = "1"
julia = "1.10"
TimerOutputs = "0.5"

View File

@@ -1,8 +1,8 @@
# Decomposition methods
## 1. Time decomposition for production cost modeling
## 1. Time decomposition
Solving unit commitment instances that have long time horizons (for example, year-long 8760-hour instances in production cost modeling) requires a substantial amount of computational power. To address this issue, UC.jl offers a time decomposition method, which breaks the instance down into multiple overlapping subproblems, solves them sequentially, then reassembles the solution.
Solving unit commitment instances that have long time horizons (for example, year-long 8760-hour instances) requires a substantial amount of computational power. To address this issue, UC.jl offers a time decomposition method, which breaks the instance down into multiple overlapping subproblems, solves them sequentially, then reassembles the solution.
When solving a unit commitment instance with a dense time slot structure, computational complexity can become a significant challenge. For instance, if the instance contains hourly data for an entire year (8760 hours), solving such a model can require a substantial amount of computational power. To address this issue, UC.jl provides a time_decomposition method within the `optimize!` function. This method decomposes the problem into multiple sub-problems, solving them sequentially.
@@ -57,7 +57,7 @@ solution = UnitCommitment.optimize!(
)
```
## 2. Scenario decomposition with Progressive Hedging for stochstic UC
## 2. Scenario decomposition with Progressive Hedging
By default, UC.jl uses the Extensive Form (EF) when solving stochastic instances. This approach involves constructing a single JuMP model that contains data and decision variables for all scenarios. Although EF has optimality guarantees and performs well with small test cases, it can become computationally intractable for large instances or substantial number of scenarios.

View File

@@ -67,21 +67,19 @@ function _add_production_piecewise_linear_eqs!(
(t < T ? Cw * switch_off[gn, t+1] : 0.0)
)
else
# Equation (47a)/(48a) in Kneuven et al. (2020)
# Equation (47a) in Kneuven et al. (2020)
eq_segprod_limit_b[sc.name, gn, t, k] = @constraint(
model,
segprod[sc.name, gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] -
Cv * switch_on[gn, t] -
(t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0)
Cv * switch_on[gn, t]
)
# Equation (47b)/(48b) in Kneuven et al. (2020)
# Equation (47b) in Kneuven et al. (2020)
eq_segprod_limit_c[sc.name, gn, t, k] = @constraint(
model,
segprod[sc.name, gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] -
max(0, Cw - Cv) * switch_on[gn, t] -
(t < T ? Cw * switch_off[gn, t+1] : 0.0)
)
end

View File

@@ -26,67 +26,59 @@ function _enforce_transmission(;
isf::Matrix{Float64},
lodf::Matrix{Float64},
)::Nothing
instance = model[:instance]
limit::Float64 = 0.0
overflow = model[:overflow]
net_injection = model[:net_injection]
lm = violation.monitored_line
lc = violation.outage_line
t = violation.time
eq_flow_ub = _init(model, :eq_flow_ub)
eq_flow_lb = _init(model, :eq_flow_lb)
eq_flow_def = _init(model, :eq_flow_def)
eq_idx = (
sc.name,
lm.name,
lc === nothing ? "Base" : lc.name,
t,
)
if lc === nothing
limit = lm.normal_flow_limit[t]
if violation.outage_line === nothing
limit = violation.monitored_line.normal_flow_limit[violation.time]
@info @sprintf(
" %8.3f MW overflow in %-5s time %3d (pre-contingency, scenario %s)",
violation.amount,
lm.name,
t,
violation.monitored_line.name,
violation.time,
sc.name,
)
else
limit = lm.emergency_flow_limit[t]
limit = violation.monitored_line.emergency_flow_limit[violation.time]
@info @sprintf(
" %8.3f MW overflow in %-5s time %3d (outage: line %s, scenario %s)",
violation.amount,
lm.name,
t,
lc.name,
violation.monitored_line.name,
violation.time,
violation.outage_line.name,
sc.name,
)
end
v = overflow[sc.name, lm.name, t]
flow = @variable(model, base_name = "flow[$eq_idx]")
eq_flow_ub[eq_idx] = @constraint(model, flow <= limit + v)
eq_flow_lb[eq_idx] = @constraint(model, -flow <= limit + v)
fm = violation.monitored_line.name
t = violation.time
flow = @variable(model, base_name = "flow[$fm,$t]")
if lc === nothing
eq_flow_def[eq_idx] = @constraint(
v = overflow[sc.name, violation.monitored_line.name, violation.time]
@constraint(model, flow <= limit + v)
@constraint(model, -flow <= limit + v)
if violation.outage_line === nothing
@constraint(
model,
flow == sum(
net_injection[sc.name, b.name, t] *
isf[lm.offset, b.offset] for
net_injection[sc.name, b.name, violation.time] *
isf[violation.monitored_line.offset, b.offset] for
b in sc.buses if b.offset > 0
)
)
else
eq_flow_def[eq_idx] = @constraint(
@constraint(
model,
flow == sum(
net_injection[sc.name, b.name, t] * (
isf[lm.offset, b.offset] + (
net_injection[sc.name, b.name, violation.time] * (
isf[violation.monitored_line.offset, b.offset] + (
lodf[
lm.offset,
lc.offset,
] * isf[lc.offset, b.offset]
violation.monitored_line.offset,
violation.outage_line.offset,
] * isf[violation.outage_line.offset, b.offset]
)
) for b in sc.buses if b.offset > 0
)

View File

@@ -2,7 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import Base.Threads: @threads
import Base.Threads: @threads, maxthreadid
function _find_violations(
model::JuMP.Model,
@@ -71,7 +71,7 @@ function _find_violations(;
B = length(sc.buses) - 1
L = length(sc.lines)
T = instance.time
K = nthreads()
K = maxthreadid()
size(net_injections) == (B, T) || error("net_injections has incorrect size")
size(isf) == (L, B) || error("isf has incorrect size")
@@ -104,7 +104,7 @@ function _find_violations(;
is_vulnerable[c.lines[1].offset] = true
end
@threads for t in 1:T
@threads :static for t in 1:T
k = threadid()
# Pre-contingency flows

View File

@@ -2,9 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using DataStructures
function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Dict
function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
if !occursin("Gurobi", JuMP.solver_name(model))
method.two_phase_gap = false
end
@@ -24,9 +22,6 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Dict
large_gap = true
end
end
stats = Dict(
"violations" => []
)
while true
time_elapsed = time() - initial_time
time_remaining = method.time_limit - time_elapsed
@@ -73,7 +68,6 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Dict
if violations_found
for (i, v) in enumerate(violations)
append!(stats["violations"], v)
_enforce_transmission(model, v, model[:instance].scenarios[i])
end
else
@@ -86,5 +80,5 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Dict
end
end
end
return stats
return
end

View File

@@ -3,12 +3,12 @@
# Released under the modified BSD license. See COPYING.md for more details.
"""
optimize!(model::JuMP.Model)::Dict
optimize!(model::JuMP.Model)::Nothing
Solve the given unit commitment model. Unlike `JuMP.optimize!`, this uses more
advanced methods to accelerate the solution process and to enforce transmission
and N-1 security constraints.
"""
function optimize!(model::JuMP.Model)::Dict
function optimize!(model::JuMP.Model)::Nothing
return UnitCommitment.optimize!(model, XavQiuWanThi2019.Method())
end

BIN
test/fixtures/issue-0057.json.gz vendored Normal file

Binary file not shown.

View File

@@ -23,6 +23,7 @@ include("validation/repair_test.jl")
include("lmp/conventional_test.jl")
include("lmp/aelmp_test.jl")
include("market/market_test.jl")
include("regression.jl")
basedir = dirname(@__FILE__)
@@ -48,12 +49,13 @@ function runtests()
solution_methods_TimeDecomposition_update_solution_test()
transform_initcond_test()
transform_slice_test()
transform_randomize_XavQiuAhm2021_test()
# transform_randomize_XavQiuAhm2021_test()
validation_repair_test()
lmp_conventional_test()
lmp_aelmp_test()
simple_market_test()
stochastic_market_test()
regression_test()
end
return
end

19
test/src/regression.jl Normal file
View File

@@ -0,0 +1,19 @@
# 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, HiGHS, JuMP
function regression_test()
@testset "GitHub Issue #57" begin
instance = UnitCommitment.read(fixture("issue-0057.json.gz"))
model = UnitCommitment.build_model(
instance = instance,
optimizer = HiGHS.Optimizer,
)
JuMP.set_silent(model)
UnitCommitment.optimize!(model)
solution = UnitCommitment.solution(model)
@test solution["Thermal production (MW)"]["gen_524d4c85"][1] == 90.0
end
end