mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-07 00:38:51 -06:00
Compare commits
1 Commits
feature/ju
...
feature/ra
| Author | SHA1 | Date | |
|---|---|---|---|
| e183a5d878 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,4 +18,3 @@ TODO.md
|
|||||||
docs/_build
|
docs/_build
|
||||||
.vscode
|
.vscode
|
||||||
Manifest.toml
|
Manifest.toml
|
||||||
*/Manifest.toml
|
|
||||||
|
|||||||
23
Makefile
23
Makefile
@@ -2,22 +2,31 @@
|
|||||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
JULIA := julia --color=yes --project=@.
|
||||||
VERSION := 0.2
|
VERSION := 0.2
|
||||||
|
|
||||||
|
build/sysimage.so: src/utils/sysimage.jl Project.toml Manifest.toml
|
||||||
|
mkdir -p build
|
||||||
|
mkdir -p benchmark/results/test
|
||||||
|
cd benchmark; $(JULIA) --trace-compile=../build/precompile.jl benchmark.jl test/case14
|
||||||
|
$(JULIA) src/utils/sysimage.jl
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rfv build
|
rm -rf build/*
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
cd docs; make clean; make dirhtml
|
cd docs; make clean; make dirhtml
|
||||||
rsync -avP --delete-after docs/_build/dirhtml/ ../docs/$(VERSION)/
|
rsync -avP --delete-after docs/_build/dirhtml/ ../docs/$(VERSION)/
|
||||||
|
|
||||||
|
test: build/sysimage.so
|
||||||
|
@echo Running tests...
|
||||||
|
$(JULIA) --sysimage build/sysimage.so -e 'using Pkg; Pkg.test("UnitCommitment")' | tee build/test.log
|
||||||
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
cd deps/formatter; ../../juliaw format.jl
|
julia -e 'using JuliaFormatter; format(["src", "test", "benchmark"], verbose=true);'
|
||||||
|
|
||||||
test: test/Manifest.toml
|
install-deps:
|
||||||
./juliaw test/runtests.jl
|
julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter", version="0.14.4"))'
|
||||||
|
|
||||||
test/Manifest.toml: test/Project.toml
|
|
||||||
julia --project=test -e "using Pkg; Pkg.instantiate()"
|
|
||||||
|
|
||||||
.PHONY: docs test format install-deps
|
.PHONY: docs test format install-deps
|
||||||
|
|||||||
10
Project.toml
10
Project.toml
@@ -6,7 +6,6 @@ version = "0.2.2"
|
|||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||||
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
|
|
||||||
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||||
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
||||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
@@ -20,6 +19,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
|||||||
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
||||||
|
|
||||||
[compat]
|
[compat]
|
||||||
|
Cbc = "0.7"
|
||||||
DataStructures = "0.18"
|
DataStructures = "0.18"
|
||||||
Distributions = "0.25"
|
Distributions = "0.25"
|
||||||
GZip = "0.5"
|
GZip = "0.5"
|
||||||
@@ -28,3 +28,11 @@ JuMP = "0.21"
|
|||||||
MathOptInterface = "0.9"
|
MathOptInterface = "0.9"
|
||||||
PackageCompiler = "1"
|
PackageCompiler = "1"
|
||||||
julia = "1"
|
julia = "1"
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
||||||
|
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
|
||||||
|
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||||
|
|
||||||
|
[targets]
|
||||||
|
test = ["Cbc", "Test", "Gurobi"]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
[deps]
|
[deps]
|
||||||
DocOpt = "968ba79b-81e4-546f-ab3a-2eecfa62a9db"
|
|
||||||
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
|
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
|
||||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||||
|
|||||||
158
benchmark/benchmark.jl
Normal file
158
benchmark/benchmark.jl
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 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 Distributed
|
||||||
|
using Pkg
|
||||||
|
Pkg.activate(".")
|
||||||
|
|
||||||
|
@everywhere using Pkg
|
||||||
|
@everywhere Pkg.activate(".")
|
||||||
|
|
||||||
|
@everywhere using UnitCommitment
|
||||||
|
@everywhere using JuMP
|
||||||
|
@everywhere using Gurobi
|
||||||
|
@everywhere using JSON
|
||||||
|
@everywhere using Logging
|
||||||
|
@everywhere using Printf
|
||||||
|
@everywhere using LinearAlgebra
|
||||||
|
@everywhere using Random
|
||||||
|
|
||||||
|
@everywhere import UnitCommitment:
|
||||||
|
ArrCon2000,
|
||||||
|
CarArr2006,
|
||||||
|
DamKucRajAta2016,
|
||||||
|
Formulation,
|
||||||
|
Gar1962,
|
||||||
|
KnuOstWat2018,
|
||||||
|
MorLatRam2013,
|
||||||
|
PanGua2016,
|
||||||
|
XavQiuWanThi2019
|
||||||
|
|
||||||
|
@everywhere UnitCommitment._setup_logger()
|
||||||
|
|
||||||
|
function main()
|
||||||
|
cases = [
|
||||||
|
"pglib-uc/ca/2014-09-01_reserves_0",
|
||||||
|
"pglib-uc/ca/2014-09-01_reserves_1",
|
||||||
|
"pglib-uc/ca/2015-03-01_reserves_0",
|
||||||
|
"pglib-uc/ca/2015-06-01_reserves_0",
|
||||||
|
"pglib-uc/ca/Scenario400_reserves_1",
|
||||||
|
"pglib-uc/ferc/2015-01-01_lw",
|
||||||
|
"pglib-uc/ferc/2015-05-01_lw",
|
||||||
|
"pglib-uc/ferc/2015-07-01_hw",
|
||||||
|
"pglib-uc/ferc/2015-10-01_lw",
|
||||||
|
"pglib-uc/ferc/2015-12-01_lw",
|
||||||
|
"pglib-uc/rts_gmlc/2020-04-03",
|
||||||
|
"pglib-uc/rts_gmlc/2020-09-20",
|
||||||
|
"pglib-uc/rts_gmlc/2020-10-27",
|
||||||
|
"pglib-uc/rts_gmlc/2020-11-25",
|
||||||
|
"pglib-uc/rts_gmlc/2020-12-23",
|
||||||
|
"or-lib/20_0_1_w",
|
||||||
|
"or-lib/20_0_5_w",
|
||||||
|
"or-lib/50_0_2_w",
|
||||||
|
"or-lib/75_0_2_w",
|
||||||
|
"or-lib/100_0_1_w",
|
||||||
|
"or-lib/100_0_4_w",
|
||||||
|
"or-lib/100_0_5_w",
|
||||||
|
"or-lib/200_0_3_w",
|
||||||
|
"or-lib/200_0_7_w",
|
||||||
|
"or-lib/200_0_9_w",
|
||||||
|
"tejada19/UC_24h_290g",
|
||||||
|
"tejada19/UC_24h_623g",
|
||||||
|
"tejada19/UC_24h_959g",
|
||||||
|
"tejada19/UC_24h_1577g",
|
||||||
|
"tejada19/UC_24h_1888g",
|
||||||
|
"tejada19/UC_168h_72g",
|
||||||
|
"tejada19/UC_168h_86g",
|
||||||
|
"tejada19/UC_168h_130g",
|
||||||
|
"tejada19/UC_168h_131g",
|
||||||
|
"tejada19/UC_168h_199g",
|
||||||
|
]
|
||||||
|
formulations = Dict(
|
||||||
|
"Default" => Formulation(),
|
||||||
|
"ArrCon2000" => Formulation(ramping = ArrCon2000.Ramping()),
|
||||||
|
"CarArr2006" => Formulation(pwl_costs = CarArr2006.PwlCosts()),
|
||||||
|
"DamKucRajAta2016" =>
|
||||||
|
Formulation(ramping = DamKucRajAta2016.Ramping()),
|
||||||
|
"Gar1962" => Formulation(pwl_costs = Gar1962.PwlCosts()),
|
||||||
|
"KnuOstWat2018" =>
|
||||||
|
Formulation(pwl_costs = KnuOstWat2018.PwlCosts()),
|
||||||
|
"MorLatRam2013" => Formulation(ramping = MorLatRam2013.Ramping()),
|
||||||
|
"PanGua2016" => Formulation(ramping = PanGua2016.Ramping()),
|
||||||
|
)
|
||||||
|
trials = [i for i in 1:5]
|
||||||
|
combinations = [
|
||||||
|
(c, f.first, f.second, t) for c in cases for f in formulations for
|
||||||
|
t in trials
|
||||||
|
]
|
||||||
|
shuffle!(combinations)
|
||||||
|
@sync @distributed for c in combinations
|
||||||
|
_run_combination(c...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@everywhere function _run_combination(
|
||||||
|
case,
|
||||||
|
formulation_name,
|
||||||
|
formulation,
|
||||||
|
trial,
|
||||||
|
)
|
||||||
|
name = "$formulation_name/$case"
|
||||||
|
dirname = "results/$name"
|
||||||
|
mkpath(dirname)
|
||||||
|
if isfile("$dirname/$trial.json")
|
||||||
|
@info @sprintf("%-4s %-16s %s", "skip", formulation_name, case)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@info @sprintf("%-4s %-16s %s", "run", formulation_name, case)
|
||||||
|
open("$dirname/$trial.log", "w") do file
|
||||||
|
redirect_stdout(file) do
|
||||||
|
redirect_stderr(file) do
|
||||||
|
return _run_sample(case, formulation, "$dirname/$trial")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@info @sprintf("%-4s %-16s %s", "done", formulation_name, case)
|
||||||
|
end
|
||||||
|
|
||||||
|
@everywhere function _run_sample(case, formulation, prefix)
|
||||||
|
total_time = @elapsed begin
|
||||||
|
@info "Reading: $case"
|
||||||
|
time_read = @elapsed begin
|
||||||
|
instance = UnitCommitment.read_benchmark(case)
|
||||||
|
end
|
||||||
|
@info @sprintf("Read problem in %.2f seconds", time_read)
|
||||||
|
BLAS.set_num_threads(4)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
formulation = formulation,
|
||||||
|
optimizer = optimizer_with_attributes(
|
||||||
|
Gurobi.Optimizer,
|
||||||
|
"Threads" => 4,
|
||||||
|
"Seed" => rand(1:1000),
|
||||||
|
),
|
||||||
|
variable_names = true,
|
||||||
|
)
|
||||||
|
@info "Optimizing..."
|
||||||
|
BLAS.set_num_threads(1)
|
||||||
|
UnitCommitment.optimize!(
|
||||||
|
model,
|
||||||
|
XavQiuWanThi2019.Method(time_limit = 3600.0, gap_limit = 1e-4),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
@info @sprintf("Total time was %.2f seconds", total_time)
|
||||||
|
@info "Writing solution: $prefix.json"
|
||||||
|
solution = UnitCommitment.solution(model)
|
||||||
|
UnitCommitment.write("$prefix.json", solution)
|
||||||
|
@info "Verifying solution..."
|
||||||
|
return UnitCommitment.validate(instance, solution)
|
||||||
|
# @info "Exporting model..."
|
||||||
|
# return JuMP.write_to_file(model, model_filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
if length(ARGS) > 0
|
||||||
|
_run_sample(ARGS[1], UnitCommitment.Formulation(), "tmp")
|
||||||
|
else
|
||||||
|
main()
|
||||||
|
end
|
||||||
207
benchmark/run.jl
207
benchmark/run.jl
@@ -1,207 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
doc = """UnitCommitment.jl Benchmark Runner
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
run.jl [-s ARG]... [-m ARG]... [-c ARG]... [-f ARG]... [options]
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
1. Benchmark all solvers, methods and formulations:
|
|
||||||
|
|
||||||
julia run.jl
|
|
||||||
|
|
||||||
2. Benchmark formulations "default" and "ArrCon200" using Gurobi:
|
|
||||||
|
|
||||||
julia run.jl -s gurobi -f default -f ArrCon2000
|
|
||||||
|
|
||||||
3. Benchmark a few test cases, using all solvers, methods and formulations:
|
|
||||||
|
|
||||||
julia run.jl -c or-lib/20_0_1_w -c matpower/case1888rte/2017-02-01
|
|
||||||
|
|
||||||
4. Solve 4 test cases in parallel, with 2 threads available per worker:
|
|
||||||
|
|
||||||
JULIA_NUM_THREADS=2 julia --procs 4 run.jl
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h --help Show this screen.
|
|
||||||
-s --solver=ARG Mixed-integer linear solver (e.g. gurobi)
|
|
||||||
-c --case=ARG Unit commitment test case (e.g. or-lib/20_0_1_w)
|
|
||||||
-m --method=ARG Solution method (e.g. default)
|
|
||||||
-f --formulation=ARG Formulation (e.g. ArrCon2000)
|
|
||||||
--time-limit=ARG Time limit in seconds [default: 3600]
|
|
||||||
--gap=ARG Relative MIP gap tolerance [default: 0.001]
|
|
||||||
--trials=ARG Number of trials [default: 5]
|
|
||||||
"""
|
|
||||||
|
|
||||||
using Distributed
|
|
||||||
using Pkg
|
|
||||||
Pkg.activate(".")
|
|
||||||
@everywhere using Pkg
|
|
||||||
@everywhere Pkg.activate(".")
|
|
||||||
|
|
||||||
using DocOpt
|
|
||||||
args = docopt(doc)
|
|
||||||
|
|
||||||
@everywhere using UnitCommitment
|
|
||||||
@everywhere UnitCommitment._setup_logger()
|
|
||||||
|
|
||||||
using UnitCommitment
|
|
||||||
using Gurobi
|
|
||||||
using Logging
|
|
||||||
using JuMP
|
|
||||||
|
|
||||||
import UnitCommitment:
|
|
||||||
ArrCon2000,
|
|
||||||
CarArr2006,
|
|
||||||
DamKucRajAta2016,
|
|
||||||
Formulation,
|
|
||||||
Gar1962,
|
|
||||||
KnuOstWat2018,
|
|
||||||
MorLatRam2013,
|
|
||||||
PanGua2016,
|
|
||||||
XavQiuWanThi2019
|
|
||||||
|
|
||||||
# Benchmark test cases
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
cases = [
|
|
||||||
"pglib-uc/ca/2014-09-01_reserves_0",
|
|
||||||
"pglib-uc/ca/2014-09-01_reserves_1",
|
|
||||||
"pglib-uc/ca/2015-03-01_reserves_0",
|
|
||||||
"pglib-uc/ca/2015-06-01_reserves_0",
|
|
||||||
"pglib-uc/ca/Scenario400_reserves_1",
|
|
||||||
"pglib-uc/ferc/2015-01-01_lw",
|
|
||||||
"pglib-uc/ferc/2015-05-01_lw",
|
|
||||||
"pglib-uc/ferc/2015-07-01_hw",
|
|
||||||
"pglib-uc/ferc/2015-10-01_lw",
|
|
||||||
"pglib-uc/ferc/2015-12-01_lw",
|
|
||||||
"pglib-uc/rts_gmlc/2020-04-03",
|
|
||||||
"pglib-uc/rts_gmlc/2020-09-20",
|
|
||||||
"pglib-uc/rts_gmlc/2020-10-27",
|
|
||||||
"pglib-uc/rts_gmlc/2020-11-25",
|
|
||||||
"pglib-uc/rts_gmlc/2020-12-23",
|
|
||||||
"or-lib/20_0_1_w",
|
|
||||||
"or-lib/20_0_5_w",
|
|
||||||
"or-lib/50_0_2_w",
|
|
||||||
"or-lib/75_0_2_w",
|
|
||||||
"or-lib/100_0_1_w",
|
|
||||||
"or-lib/100_0_4_w",
|
|
||||||
"or-lib/100_0_5_w",
|
|
||||||
"or-lib/200_0_3_w",
|
|
||||||
"or-lib/200_0_7_w",
|
|
||||||
"or-lib/200_0_9_w",
|
|
||||||
"tejada19/UC_24h_290g",
|
|
||||||
"tejada19/UC_24h_623g",
|
|
||||||
"tejada19/UC_24h_959g",
|
|
||||||
"tejada19/UC_24h_1577g",
|
|
||||||
"tejada19/UC_24h_1888g",
|
|
||||||
"tejada19/UC_168h_72g",
|
|
||||||
"tejada19/UC_168h_86g",
|
|
||||||
"tejada19/UC_168h_130g",
|
|
||||||
"tejada19/UC_168h_131g",
|
|
||||||
"tejada19/UC_168h_199g",
|
|
||||||
"matpower/case1888rte/2017-02-01",
|
|
||||||
"matpower/case1951rte/2017-02-01",
|
|
||||||
"matpower/case2848rte/2017-02-01",
|
|
||||||
"matpower/case3012wp/2017-02-01",
|
|
||||||
"matpower/case3375wp/2017-02-01",
|
|
||||||
"matpower/case6468rte/2017-02-01",
|
|
||||||
"matpower/case6515rte/2017-02-01",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Formulations
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
formulations = Dict(
|
|
||||||
"default" => Formulation(),
|
|
||||||
"ArrCon2000" => Formulation(ramping = ArrCon2000.Ramping()),
|
|
||||||
"CarArr2006" => Formulation(pwl_costs = CarArr2006.PwlCosts()),
|
|
||||||
"DamKucRajAta2016" => Formulation(ramping = DamKucRajAta2016.Ramping()),
|
|
||||||
"Gar1962" => Formulation(pwl_costs = Gar1962.PwlCosts()),
|
|
||||||
"KnuOstWat2018" => Formulation(pwl_costs = KnuOstWat2018.PwlCosts()),
|
|
||||||
"MorLatRam2013" => Formulation(ramping = MorLatRam2013.Ramping()),
|
|
||||||
"PanGua2016" => Formulation(ramping = PanGua2016.Ramping()),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Solution methods
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
methods = Dict(
|
|
||||||
"default" =>
|
|
||||||
XavQiuWanThi2019.Method(time_limit = 3600.0, gap_limit = 1e-4),
|
|
||||||
)
|
|
||||||
|
|
||||||
# MIP solvers
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
optimizers = Dict(
|
|
||||||
"gurobi" => optimizer_with_attributes(
|
|
||||||
Gurobi.Optimizer,
|
|
||||||
"Threads" => Threads.nthreads(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
if !isempty(args["--case"])
|
|
||||||
cases = args["--case"]
|
|
||||||
end
|
|
||||||
if !isempty(args["--formulation"])
|
|
||||||
formulations = filter(p -> p.first in args["--formulation"], formulations)
|
|
||||||
end
|
|
||||||
if !isempty(args["--method"])
|
|
||||||
methods = filter(p -> p.first in args["--method"], methods)
|
|
||||||
end
|
|
||||||
if !isempty(args["--solver"])
|
|
||||||
optimizers = filter(p -> p.first in args["--solver"], optimizers)
|
|
||||||
end
|
|
||||||
const time_limit = parse(Float64, args["--time-limit"])
|
|
||||||
const gap_limit = parse(Float64, args["--gap"])
|
|
||||||
const ntrials = parse(Int, args["--trials"])
|
|
||||||
|
|
||||||
# Print benchmark settings
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
function printlist(d::Dict)
|
|
||||||
for key in keys(d)
|
|
||||||
@info " - $key"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function printlist(d::Vector)
|
|
||||||
for key in d
|
|
||||||
@info " - $key"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@info "Computational environment:"
|
|
||||||
@info " - CPU: $(Sys.cpu_info()[1].model)"
|
|
||||||
@info " - Logical CPU cores: $(length(Sys.cpu_info()))"
|
|
||||||
@info " - System memory: $(round(Sys.total_memory() / 2^30, digits=2)) GiB"
|
|
||||||
@info " - Available workers: $(nworkers())"
|
|
||||||
@info " - Available threads per worker: $(Threads.nthreads())"
|
|
||||||
|
|
||||||
@info "Parameters:"
|
|
||||||
@info " - Number of trials: $ntrials"
|
|
||||||
@info " - Time limit (s): $time_limit"
|
|
||||||
@info " - Relative MIP gap tolerance: $gap_limit"
|
|
||||||
|
|
||||||
@info "Solvers:"
|
|
||||||
printlist(optimizers)
|
|
||||||
|
|
||||||
@info "Methods:"
|
|
||||||
printlist(methods)
|
|
||||||
|
|
||||||
@info "Formulations:"
|
|
||||||
printlist(formulations)
|
|
||||||
|
|
||||||
@info "Cases:"
|
|
||||||
printlist(cases)
|
|
||||||
|
|
||||||
# Run benchmarks
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
UnitCommitment._run_benchmarks(
|
|
||||||
cases = cases,
|
|
||||||
formulations = formulations,
|
|
||||||
methods = methods,
|
|
||||||
optimizers = optimizers,
|
|
||||||
trials = 1:ntrials,
|
|
||||||
)
|
|
||||||
5
deps/formatter/Project.toml
vendored
5
deps/formatter/Project.toml
vendored
@@ -1,5 +0,0 @@
|
|||||||
[deps]
|
|
||||||
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
|
|
||||||
|
|
||||||
[compat]
|
|
||||||
JuliaFormatter = "0.14.4"
|
|
||||||
9
deps/formatter/format.jl
vendored
9
deps/formatter/format.jl
vendored
@@ -1,9 +0,0 @@
|
|||||||
using JuliaFormatter
|
|
||||||
format(
|
|
||||||
[
|
|
||||||
"../../src",
|
|
||||||
"../../test",
|
|
||||||
"../../benchmark/run.jl",
|
|
||||||
],
|
|
||||||
verbose=true,
|
|
||||||
)
|
|
||||||
68
juliaw
68
juliaw
@@ -1,68 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
|
||||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
|
||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
|
||||||
|
|
||||||
if [ ! -e Project.toml ]; then
|
|
||||||
echo "juliaw: Project.toml not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -e Manifest.toml ]; then
|
|
||||||
julia --project=. -e 'using Pkg; Pkg.instantiate()' || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -e build/sysimage.so -o Project.toml -nt build/sysimage.so ]; then
|
|
||||||
echo "juliaw: rebuilding system image..."
|
|
||||||
|
|
||||||
# Generate temporary project folder
|
|
||||||
rm -rf $HOME/.juliaw
|
|
||||||
mkdir -p $HOME/.juliaw/src
|
|
||||||
cp Project.toml Manifest.toml $HOME/.juliaw
|
|
||||||
NAME=$(julia -e 'using TOML; toml = TOML.parsefile("Project.toml"); "name" in keys(toml) && print(toml["name"])')
|
|
||||||
if [ ! -z $NAME ]; then
|
|
||||||
cat > $HOME/.juliaw/src/$NAME.jl << EOF
|
|
||||||
module $NAME
|
|
||||||
end
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add PackageCompiler dependencies to temporary project
|
|
||||||
julia --project=$HOME/.juliaw -e 'using Pkg; Pkg.add(["PackageCompiler", "TOML", "Logging"])'
|
|
||||||
|
|
||||||
# Generate system image scripts
|
|
||||||
cat > $HOME/.juliaw/sysimage.jl << EOF
|
|
||||||
using PackageCompiler
|
|
||||||
using TOML
|
|
||||||
using Logging
|
|
||||||
|
|
||||||
Logging.disable_logging(Logging.Info)
|
|
||||||
mkpath("$PWD/build")
|
|
||||||
|
|
||||||
println("juliaw: generating precompilation statements...")
|
|
||||||
run(\`julia --project="$PWD" --trace-compile="$PWD"/build/precompile.jl \$(ARGS)\`)
|
|
||||||
|
|
||||||
println("juliaw: finding dependencies...")
|
|
||||||
project = TOML.parsefile("Project.toml")
|
|
||||||
manifest = TOML.parsefile("Manifest.toml")
|
|
||||||
deps = Symbol[]
|
|
||||||
for dep in keys(project["deps"])
|
|
||||||
if "path" in keys(manifest[dep][1])
|
|
||||||
println(" - \$(dep) [skip]")
|
|
||||||
else
|
|
||||||
println(" - \$(dep)")
|
|
||||||
push!(deps, Symbol(dep))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
println("juliaw: building system image...")
|
|
||||||
create_sysimage(
|
|
||||||
deps,
|
|
||||||
precompile_statements_file = "$PWD/build/precompile.jl",
|
|
||||||
sysimage_path = "$PWD/build/sysimage.so",
|
|
||||||
)
|
|
||||||
EOF
|
|
||||||
julia --project=$HOME/.juliaw $HOME/.juliaw/sysimage.jl $*
|
|
||||||
else
|
|
||||||
julia --project=. --sysimage build/sysimage.so $*
|
|
||||||
fi
|
|
||||||
@@ -50,7 +50,6 @@ include("transform/initcond.jl")
|
|||||||
include("transform/slice.jl")
|
include("transform/slice.jl")
|
||||||
include("transform/randomize/XavQiuAhm2021.jl")
|
include("transform/randomize/XavQiuAhm2021.jl")
|
||||||
include("utils/log.jl")
|
include("utils/log.jl")
|
||||||
include("utils/benchmark.jl")
|
|
||||||
include("validation/repair.jl")
|
include("validation/repair.jl")
|
||||||
include("validation/validate.jl")
|
include("validation/validate.jl")
|
||||||
|
|
||||||
|
|||||||
@@ -266,20 +266,15 @@ function _from_json(json; repair = true)
|
|||||||
end
|
end
|
||||||
|
|
||||||
instance = UnitCommitmentInstance(
|
instance = UnitCommitmentInstance(
|
||||||
buses_by_name = Dict(b.name => b for b in buses),
|
T,
|
||||||
buses = buses,
|
power_balance_penalty,
|
||||||
contingencies_by_name = Dict(c.name => c for c in contingencies),
|
shortfall_penalty,
|
||||||
contingencies = contingencies,
|
units,
|
||||||
lines_by_name = Dict(l.name => l for l in lines),
|
buses,
|
||||||
lines = lines,
|
lines,
|
||||||
power_balance_penalty = power_balance_penalty,
|
reserves,
|
||||||
price_sensitive_loads_by_name = Dict(ps.name => ps for ps in loads),
|
contingencies,
|
||||||
price_sensitive_loads = loads,
|
loads,
|
||||||
reserves = reserves,
|
|
||||||
shortfall_penalty = shortfall_penalty,
|
|
||||||
time = T,
|
|
||||||
units_by_name = Dict(g.name => g for g in units),
|
|
||||||
units = units,
|
|
||||||
)
|
)
|
||||||
if repair
|
if repair
|
||||||
UnitCommitment.repair!(instance)
|
UnitCommitment.repair!(instance)
|
||||||
|
|||||||
@@ -69,21 +69,17 @@ mutable struct PriceSensitiveLoad
|
|||||||
revenue::Vector{Float64}
|
revenue::Vector{Float64}
|
||||||
end
|
end
|
||||||
|
|
||||||
Base.@kwdef mutable struct UnitCommitmentInstance
|
mutable struct UnitCommitmentInstance
|
||||||
buses_by_name::Dict{AbstractString,Bus}
|
|
||||||
buses::Vector{Bus}
|
|
||||||
contingencies_by_name::Dict{AbstractString,Contingency}
|
|
||||||
contingencies::Vector{Contingency}
|
|
||||||
lines_by_name::Dict{AbstractString,TransmissionLine}
|
|
||||||
lines::Vector{TransmissionLine}
|
|
||||||
power_balance_penalty::Vector{Float64}
|
|
||||||
price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad}
|
|
||||||
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
|
||||||
reserves::Reserves
|
|
||||||
shortfall_penalty::Vector{Float64}
|
|
||||||
time::Int
|
time::Int
|
||||||
units_by_name::Dict{AbstractString,Unit}
|
power_balance_penalty::Vector{Float64}
|
||||||
|
"Penalty for failing to meet reserve requirement."
|
||||||
|
shortfall_penalty::Vector{Float64}
|
||||||
units::Vector{Unit}
|
units::Vector{Unit}
|
||||||
|
buses::Vector{Bus}
|
||||||
|
lines::Vector{TransmissionLine}
|
||||||
|
reserves::Reserves
|
||||||
|
contingencies::Vector{Contingency}
|
||||||
|
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
||||||
end
|
end
|
||||||
|
|
||||||
function Base.show(io::IO, instance::UnitCommitmentInstance)
|
function Base.show(io::IO, instance::UnitCommitmentInstance)
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
# 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 Distributed
|
|
||||||
using Random
|
|
||||||
|
|
||||||
function _run_benchmark_sample(;
|
|
||||||
case::String,
|
|
||||||
method::SolutionMethod,
|
|
||||||
formulation::Formulation,
|
|
||||||
solution_filename::String,
|
|
||||||
optimizer,
|
|
||||||
)::Nothing
|
|
||||||
total_time = @elapsed begin
|
|
||||||
@info "Reading: $case"
|
|
||||||
time_read = @elapsed begin
|
|
||||||
instance = read_benchmark(case)
|
|
||||||
end
|
|
||||||
@info @sprintf("Read problem in %.2f seconds", time_read)
|
|
||||||
BLAS.set_num_threads(Threads.nthreads())
|
|
||||||
model = build_model(
|
|
||||||
instance = instance,
|
|
||||||
formulation = formulation,
|
|
||||||
optimizer = optimizer,
|
|
||||||
variable_names = true,
|
|
||||||
)
|
|
||||||
@info "Optimizing..."
|
|
||||||
BLAS.set_num_threads(1)
|
|
||||||
optimize!(model, method)
|
|
||||||
end
|
|
||||||
@info @sprintf("Total time was %.2f seconds", total_time)
|
|
||||||
|
|
||||||
@info "Writing solution: $solution_filename"
|
|
||||||
solution = UnitCommitment.solution(model)
|
|
||||||
write("$solution_filename", solution)
|
|
||||||
|
|
||||||
@info "Verifying solution..."
|
|
||||||
validate(instance, solution)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
function _run_benchmark_combination(
|
|
||||||
case::String,
|
|
||||||
optimizer_name::String,
|
|
||||||
optimizer,
|
|
||||||
method_name::String,
|
|
||||||
method::SolutionMethod,
|
|
||||||
formulation_name::String,
|
|
||||||
formulation::Formulation,
|
|
||||||
trial,
|
|
||||||
)
|
|
||||||
dirname = "results/$optimizer_name/$method_name/$formulation_name/$case"
|
|
||||||
function info(msg)
|
|
||||||
@info @sprintf(
|
|
||||||
"%-8s %-16s %-16s %-16s %-8s %s",
|
|
||||||
msg,
|
|
||||||
optimizer_name,
|
|
||||||
method_name,
|
|
||||||
formulation_name,
|
|
||||||
trial,
|
|
||||||
case
|
|
||||||
)
|
|
||||||
end
|
|
||||||
mkpath(dirname)
|
|
||||||
trial_filename = @sprintf("%s/%03d.json", dirname, trial)
|
|
||||||
if isfile(trial_filename)
|
|
||||||
info("skip")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
info("run")
|
|
||||||
open("$trial_filename.log", "w") do file
|
|
||||||
redirect_stdout(file) do
|
|
||||||
redirect_stderr(file) do
|
|
||||||
return _run_benchmark_sample(
|
|
||||||
case = case,
|
|
||||||
method = method,
|
|
||||||
formulation = formulation,
|
|
||||||
solution_filename = trial_filename,
|
|
||||||
optimizer = optimizer,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return info("done")
|
|
||||||
end
|
|
||||||
|
|
||||||
function _run_benchmarks(;
|
|
||||||
cases::Vector{String},
|
|
||||||
optimizers::Dict,
|
|
||||||
formulations::Dict,
|
|
||||||
methods::Dict,
|
|
||||||
trials,
|
|
||||||
)
|
|
||||||
combinations = [
|
|
||||||
(c, s.first, s.second, m.first, m.second, f.first, f.second, t) for
|
|
||||||
c in cases for s in optimizers for f in formulations for
|
|
||||||
m in methods for t in trials
|
|
||||||
]
|
|
||||||
shuffle!(combinations)
|
|
||||||
if nworkers() > 1
|
|
||||||
@printf("%24s", "")
|
|
||||||
end
|
|
||||||
@info @sprintf(
|
|
||||||
"%-8s %-16s %-16s %-16s %-8s %s",
|
|
||||||
"STATUS",
|
|
||||||
"SOLVER",
|
|
||||||
"METHOD",
|
|
||||||
"FORMULATION",
|
|
||||||
"TRIAL",
|
|
||||||
"CASE"
|
|
||||||
)
|
|
||||||
@sync @distributed for c in combinations
|
|
||||||
_run_benchmark_combination(c...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
28
src/utils/sysimage.jl
Normal file
28
src/utils/sysimage.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.
|
||||||
|
|
||||||
|
using PackageCompiler
|
||||||
|
|
||||||
|
using DataStructures
|
||||||
|
using Distributions
|
||||||
|
using JSON
|
||||||
|
using JuMP
|
||||||
|
using MathOptInterface
|
||||||
|
using SparseArrays
|
||||||
|
|
||||||
|
pkg = [
|
||||||
|
:DataStructures,
|
||||||
|
:Distributions,
|
||||||
|
:JSON,
|
||||||
|
:JuMP,
|
||||||
|
:MathOptInterface,
|
||||||
|
:SparseArrays,
|
||||||
|
]
|
||||||
|
|
||||||
|
@info "Building system image..."
|
||||||
|
create_sysimage(
|
||||||
|
pkg,
|
||||||
|
precompile_statements_file = "build/precompile.jl",
|
||||||
|
sysimage_path = "build/sysimage.so",
|
||||||
|
)
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[deps]
|
|
||||||
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
|
||||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
|
||||||
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
|
||||||
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
|
||||||
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
|
|
||||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
|
||||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
|
||||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
|
||||||
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
|
|
||||||
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
|
|
||||||
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
|
|
||||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
|
||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
|
||||||
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
|
||||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
|
||||||
|
|
||||||
[compat]
|
|
||||||
DataStructures = "0.18"
|
|
||||||
Distributions = "0.25"
|
|
||||||
GZip = "0.5"
|
|
||||||
JSON = "0.21"
|
|
||||||
JuMP = "0.21"
|
|
||||||
MathOptInterface = "0.9"
|
|
||||||
PackageCompiler = "1"
|
|
||||||
julia = "1"
|
|
||||||
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
using UnitCommitment
|
using UnitCommitment
|
||||||
|
|
||||||
basedir = @__DIR__
|
|
||||||
|
|
||||||
@testset "read_egret_solution" begin
|
@testset "read_egret_solution" begin
|
||||||
solution = UnitCommitment.read_egret_solution(
|
solution =
|
||||||
"$basedir/../fixtures/egret_output.json.gz",
|
UnitCommitment.read_egret_solution("fixtures/egret_output.json.gz")
|
||||||
)
|
|
||||||
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
||||||
@test attr in keys(solution)
|
@test attr in keys(solution)
|
||||||
@test "115_STEAM_1" in keys(solution[attr])
|
@test "115_STEAM_1" in keys(solution[attr])
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
|||||||
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4]
|
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4]
|
||||||
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
|
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
|
||||||
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
|
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
|
||||||
@test instance.lines_by_name["l5"].name == "l5"
|
|
||||||
|
|
||||||
@test instance.lines[1].name == "l1"
|
@test instance.lines[1].name == "l1"
|
||||||
@test instance.lines[1].source.name == "b1"
|
@test instance.lines[1].source.name == "b1"
|
||||||
@@ -35,7 +34,6 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
|||||||
|
|
||||||
@test instance.buses[9].name == "b9"
|
@test instance.buses[9].name == "b9"
|
||||||
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
|
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
|
||||||
@test instance.buses_by_name["b9"].name == "b9"
|
|
||||||
|
|
||||||
unit = instance.units[1]
|
unit = instance.units[1]
|
||||||
@test unit.name == "g1"
|
@test unit.name == "g1"
|
||||||
@@ -64,7 +62,6 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
|||||||
@test unit.startup_categories[1].cost == 1000.0
|
@test unit.startup_categories[1].cost == 1000.0
|
||||||
@test unit.startup_categories[2].cost == 1500.0
|
@test unit.startup_categories[2].cost == 1500.0
|
||||||
@test unit.startup_categories[3].cost == 2000.0
|
@test unit.startup_categories[3].cost == 2000.0
|
||||||
@test instance.units_by_name["g1"].name == "g1"
|
|
||||||
|
|
||||||
unit = instance.units[2]
|
unit = instance.units[2]
|
||||||
@test unit.name == "g2"
|
@test unit.name == "g2"
|
||||||
@@ -95,15 +92,12 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
|||||||
|
|
||||||
@test instance.contingencies[1].lines == [instance.lines[1]]
|
@test instance.contingencies[1].lines == [instance.lines[1]]
|
||||||
@test instance.contingencies[1].units == []
|
@test instance.contingencies[1].units == []
|
||||||
@test instance.contingencies[1].name == "c1"
|
|
||||||
@test instance.contingencies_by_name["c1"].name == "c1"
|
|
||||||
|
|
||||||
load = instance.price_sensitive_loads[1]
|
load = instance.price_sensitive_loads[1]
|
||||||
@test load.name == "ps1"
|
@test load.name == "ps1"
|
||||||
@test load.bus.name == "b3"
|
@test load.bus.name == "b3"
|
||||||
@test load.revenue == [100.0 for t in 1:4]
|
@test load.revenue == [100.0 for t in 1:4]
|
||||||
@test load.demand == [50.0 for t in 1:4]
|
@test load.demand == [50.0 for t in 1:4]
|
||||||
@test instance.price_sensitive_loads_by_name["ps1"].name == "ps1"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "read_benchmark sub-hourly" begin
|
@testset "read_benchmark sub-hourly" begin
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
using Test
|
using Test
|
||||||
using UnitCommitment
|
using UnitCommitment
|
||||||
|
|
||||||
push!(Base.LOAD_PATH, @__DIR__)
|
|
||||||
UnitCommitment._setup_logger()
|
UnitCommitment._setup_logger()
|
||||||
|
|
||||||
const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
|
const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
|
||||||
|
|||||||
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
using UnitCommitment, Cbc, JuMP
|
using UnitCommitment, Cbc, JuMP
|
||||||
|
|
||||||
basedir = @__DIR__
|
|
||||||
|
|
||||||
@testset "generate_initial_conditions!" begin
|
@testset "generate_initial_conditions!" begin
|
||||||
# Load instance
|
# Load instance
|
||||||
instance =
|
instance = UnitCommitment.read("$(pwd())/fixtures/case118-initcond.json.gz")
|
||||||
UnitCommitment.read("$basedir/../fixtures/case118-initcond.json.gz")
|
|
||||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||||
|
|
||||||
# All units should have unknown initial conditions
|
# All units should have unknown initial conditions
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
|
|
||||||
using UnitCommitment, JSON, GZip, DataStructures
|
using UnitCommitment, JSON, GZip, DataStructures
|
||||||
|
|
||||||
basedir = @__DIR__
|
|
||||||
|
|
||||||
function parse_case14()
|
function parse_case14()
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
GZip.gzopen("$basedir/../../instances/test/case14.json.gz"),
|
GZip.gzopen("../instances/test/case14.json.gz"),
|
||||||
dicttype = () -> DefaultOrderedDict(nothing),
|
dicttype = () -> DefaultOrderedDict(nothing),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user