diff --git a/docs/src/guides/format.md b/docs/src/guides/format.md index 4cf383c..8b62a41 100644 --- a/docs/src/guides/format.md +++ b/docs/src/guides/format.md @@ -281,7 +281,7 @@ This section describes the characteristics of transmission system, such as its t | `Emergency flow limit (MW)` | Maximum amount of power (in MW) allowed to flow through the line when the system is in degraded state (for example, after the failure of another transmission line). | `+inf` | Y | Yes | | `Flow limit penalty ($/MW)` | Penalty for violating the flow limits of the transmission line (in $/MW). This is charged per time step. For example, if there is a thermal violation of 1 MW for three time steps, then three times this amount will be charged. | `5000.0` | Yes | Yes | | `Investment cost ($)` | Cost to build a candidate transmission line. $0 for existing lines. | `0.0` | No | No | -| `Max number of parallel circuits`| Maximum number of lines can be built in this corridor. | `0` | No | No | +| `Max number of parallel circuits`| Maximum number of lines can be built in this corridor. | `1` | No | No | #### Example diff --git a/src/instance/read.jl b/src/instance/read.jl index 0f828d8..00dd87e 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -338,6 +338,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario timeseries(scalar(dict["Minimum power (MW)"], default = 0.0)), timeseries(dict["Maximum power (MW)"]), timeseries(dict["Cost (\$/MW)"]), + scalar(dict["Investment cost (\$)"], default = 0.0), ) push!(bus.profiled_units, pu) push!(profiled_units, pu) @@ -367,6 +368,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario dict["Flow limit penalty (\$/MW)"], default = [5000.0 for t in 1:T], ), + scalar(dict["Investment cost (\$)"], default = 0.0), + scalar(dict["Maximum number of copies"], default = 1), ) name_to_line[line_name] = line push!(lines, line) diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 4082fb0..a6c939f 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -60,6 +60,8 @@ mutable struct TransmissionLine normal_flow_limit::Vector{Float64} emergency_flow_limit::Vector{Float64} flow_limit_penalty::Vector{Float64} + invest::Float64 #TODO: make this a vector + max_copy::Int end mutable struct Contingency @@ -81,6 +83,7 @@ mutable struct ProfiledUnit min_power::Vector{Float64} max_power::Vector{Float64} cost::Vector{Float64} + invest::Float64 end mutable struct StorageUnit diff --git a/src/model/formulations/base/line.jl b/src/model/formulations/base/line.jl index 98d5f2c..0546f8b 100644 --- a/src/model/formulations/base/line.jl +++ b/src/model/formulations/base/line.jl @@ -59,3 +59,9 @@ function _setup_transmission( sc.lodf = lodf return end + + +_setup_transmission( + ::PhaseAngleFormulation, + ::UnitCommitmentScenario, +) = nothing \ No newline at end of file diff --git a/src/model/formulations/base/phaseangle.jl b/src/model/formulations/base/phaseangle.jl new file mode 100644 index 0000000..990d8fc --- /dev/null +++ b/src/model/formulations/base/phaseangle.jl @@ -0,0 +1,110 @@ +# 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_planning_unified!( + model::JuMP.Model, + pu::ProfiledUnit, + lm::TransmissionLine, + sc::UnitCommitmentScenario, +)::Nothing + invest_unit = _init(model, :invest_unit) + invest_line = _init(model, :invest_line) + eq_invest_unit_history = _init(model, :eq_invest_unit_history) + eq_invest_line_history = _init(model, :eq_invest_line_history) + eq_invest_unit_capacity = _init(model, :eq_invest_unit_capacity) + + # t == 0 + # TODO: If investment cost is zero, skip creating these investment variables + invest_unit[sc.name, pu.name, 0] = @variable( + model, + binary = true, + fixed = pu.invest == 0.0 + ) + invest_line[sc.name, lm.name, 0] = Int(lm.invest == 0.0) + + for t in 1:model[:instance].time + # Decision variable + invest_unit[sc.name, pu.name, t] = @variable(model, binary = true) + invest_line[sc.name, lm.name, t] = @variable(model, binary = true) + + # Objective function terms + add_to_expression!( + model[:obj], + invest_unit[sc.name, pu.name, t] - invest_unit[sc.name, pu.name, t-1], + pu.invest[t] * sc.probability, + ) + add_to_expression!( + model[:obj], + invest_line[sc.name, lm.name, t] - invest_line[sc.name, lm.name, t-1], + lm.invest[t] * sc.probability, + ) + + # Investment constraints + # (1c) in the paper + eq_invest_unit_history[sc.name, pu.name, t] = @constraint( + model, + invest_unit[sc.name, pu.name, t-1] <= invest_unit[sc.name, pu.name, t] + ) + # (1d) in the paper + eq_invest_line_history[sc.name, lm.name, t] = @constraint( + model, + invest_line[sc.name, lm.name, t-1] <= invest_line[sc.name, lm.name, t] + ) + # (1h) in the paper TODO: separate this to two halves + eq_invest_unit_capacity[sc.name, pu.name, t] = @constraint( + model, + pu.min_power[t] * invest_unit[sc.name, pu.name, t] <= + model[:prod_above][sc.name, pu.name, t] <= + pu.max_power[t] * invest_unit[sc.name, pu.name, t] + ) + end + return +end + +# TODO: add max_copy constraint +function _add_planning_new_lines!( + model::JuMP.Model, + lm::TransmissionLine, + bs::Bus, + f::PhaseAngleFormulation, + sc::UnitCommitmentScenario, +)::Nothing + θ = _init(model, :theta) + flow = _init(model, :flow) + invest_line = model[:invest_line] + eq_invest_line_flow_a = _init(model, :eq_invest_line_flow_a) + eq_invest_line_flow_b = _init(model, :eq_invest_line_flow_b) + eq_invest_line_flow_limit = _init(model, :eq_invest_line_flow_limit) + + bigM = 1e6 # TODO: make this a parameter + + for t in 1:model[:instance].time + # TODO: add bus field: phase_angle_limit? give default + θ[sc.name, bs.name, t] = @variable(model, lower_bound = -pi, upper_bound = pi) + flow[sc.name, lm.name, t] = @variable(model) + + # (2b) + eq_invest_line_flow_a[sc.name, lm.name, t] = @constraint( + model, + model[:flow][sc.name, lm.name, t] <= + lm.susceptance * (θ[sc.name, lm.source.name, t] - θ[sc.name, lm.target.name, t]) + + bigM * (1 - invest_line[sc.name, lm.name, t]) + ) + # (2c) + eq_invest_line_flow_b[sc.name, lm.name, t] = @constraint( + model, + model[:flow][sc.name, lm.name, t] >= + lm.susceptance * (θ[sc.name, lm.source.name, t] - θ[sc.name, lm.target.name, t]) + - bigM * (1 - invest_line[sc.name, lm.name, t]) + ) + # (2d) + eq_invest_line_flow_limit[sc.name, lm.name, t] = @constraint( + model, + -lm.normal_flow_limit[t] * invest_line[sc.name, lm.name, t] <= + model[:flow][sc.name, lm.name, t] <= + lm.normal_flow_limit[t] * invest_line[sc.name, lm.name, t] + ) + end + return +end \ No newline at end of file diff --git a/src/model/formulations/base/structs.jl b/src/model/formulations/base/structs.jl index 09b8481..3d315d3 100644 --- a/src/model/formulations/base/structs.jl +++ b/src/model/formulations/base/structs.jl @@ -97,3 +97,14 @@ struct ShiftFactorsFormulation <: TransmissionFormulation return new(isf_cutoff, lodf_cutoff, precomputed_isf, precomputed_lodf) end end + +""" + struct PhaseAngleFormulation <: TransmissionFormulation + end + +Transmission formulation based on susceptance (b). +Constraints are enforced in a lazy way. +""" +struct PhaseAngleFormulation <: TransmissionFormulation + +end diff --git a/test/fixtures/garver6.json b/test/fixtures/garver6.json new file mode 100644 index 0000000..fe35eca --- /dev/null +++ b/test/fixtures/garver6.json @@ -0,0 +1,298 @@ +{ + "Parameters": { + "Version": "0.4", + "Time horizon (h)": 1, + "Power balance penalty ($/MW)": 1000.0, + "Scenario name": "s1", + "Scenario weight": 1.0, + "Operation cost weight": 1.0 + }, + "Buses": { + "b1": { + "Load (MW)": 80.0 + }, + "b2": { + "Load (MW)": 240.0 + }, + "b3": { + "Load (MW)": 40.0 + }, + "b4": { + "Load (MW)": 160.0 + }, + "b5": { + "Load (MW)": 240.0 + }, + "b6": { + "Load (MW)": 0.0 + } + }, + "Generators": { + "gen1": { + "Bus": "b1", + "Type": "Profiled", + "Maximum power (MW)": 150.0, + "Cost ($/MW)": 0.0 + }, + "gen2": { + "Bus": "b3", + "Type": "Profiled", + "Maximum power (MW)": 360.0, + "Cost ($/MW)": 0.0 + }, + "gen3": { + "Bus": "b6", + "Type": "Profiled", + "Maximum power (MW)": 600.0, + "Cost ($/MW)": 0.0 + } + }, + "Transmission lines": { + "l1": { + "Source bus": "b1", + "Target bus": "b2", + "Susceptance (S)": 2.5, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 0.0, + "Guide Number": 40.0, + "Reactance (p.u.)": 0.4, + "Resistance (p.u.)": 0.1, + "Length (miles)": 40.0 + }, + "l2": { + "Source bus": "b1", + "Target bus": "b4", + "Susceptance (S)": 1.6666666666666667, + "Normal flow limit (MW)": 80.0, + "Investment cost ($)": 0.0, + "Guide Number": 60.0, + "Reactance (p.u.)": 0.6, + "Resistance (p.u.)": 0.15, + "Length (miles)": 60.0 + }, + "l3": { + "Source bus": "b1", + "Target bus": "b5", + "Susceptance (S)": 5.0, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 0.0, + "Guide Number": 20.0, + "Reactance (p.u.)": 0.2, + "Resistance (p.u.)": 0.05, + "Length (miles)": 20.0 + }, + "l4": { + "Source bus": "b2", + "Target bus": "b3", + "Susceptance (S)": 5.0, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 0.0, + "Guide Number": 20.0, + "Reactance (p.u.)": 0.2, + "Resistance (p.u.)": 0.05, + "Length (miles)": 20.0 + }, + "l5": { + "Source bus": "b2", + "Target bus": "b4", + "Susceptance (S)": 2.5, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 0.0, + "Guide Number": 40.0, + "Reactance (p.u.)": 0.4, + "Resistance (p.u.)": 0.1, + "Length (miles)": 40.0 + }, + "l6": { + "Source bus": "b3", + "Target bus": "b5", + "Susceptance (S)": 5.0, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 0.0, + "Guide Number": 20.0, + "Reactance (p.u.)": 0.2, + "Resistance (p.u.)": 0.05, + "Length (miles)": 20.0 + }, + "new_l7": { + "Source bus": "b1", + "Target bus": "b2", + "Susceptance (S)": 2.5, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 40000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 40.0, + "Reactance (p.u.)": 0.4, + "Resistance (p.u.)": 0.1, + "Length (miles)": 40.0 + }, + "new_l8": { + "Source bus": "b1", + "Target bus": "b3", + "Susceptance (S)": 2.6315789473684212, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 38000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 38.0, + "Reactance (p.u.)": 0.38, + "Resistance (p.u.)": 0.09, + "Length (miles)": 38.0 + }, + "new_l9": { + "Source bus": "b1", + "Target bus": "b4", + "Susceptance (S)": 1.6666666666666667, + "Normal flow limit (MW)": 80.0, + "Investment cost ($)": 60000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 60.0, + "Reactance (p.u.)": 0.6, + "Resistance (p.u.)": 0.15, + "Length (miles)": 60.0 + }, + "new_l10": { + "Source bus": "b1", + "Target bus": "b5", + "Susceptance (S)": 5.0, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 20000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 20.0, + "Reactance (p.u.)": 0.2, + "Resistance (p.u.)": 0.05, + "Length (miles)": 20.0 + }, + "new_l11": { + "Source bus": "b1", + "Target bus": "b6", + "Susceptance (S)": 1.4705882352941175, + "Normal flow limit (MW)": 70.0, + "Investment cost ($)": 68000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 68.0, + "Reactance (p.u.)": 0.68, + "Resistance (p.u.)": 0.17, + "Length (miles)": 68.0 + }, + "new_l12": { + "Source bus": "b2", + "Target bus": "b3", + "Susceptance (S)": 5.0, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 20000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 20.0, + "Reactance (p.u.)": 0.2, + "Resistance (p.u.)": 0.05, + "Length (miles)": 20.0 + }, + "new_l13": { + "Source bus": "b2", + "Target bus": "b4", + "Susceptance (S)": 2.5, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 40000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 40.0, + "Reactance (p.u.)": 0.4, + "Resistance (p.u.)": 0.1, + "Length (miles)": 40.0 + }, + "new_l14": { + "Source bus": "b2", + "Target bus": "b5", + "Susceptance (S)": 3.2258064516129035, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 31000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 31.0, + "Reactance (p.u.)": 0.31, + "Resistance (p.u.)": 0.08, + "Length (miles)": 31.0 + }, + "new_l15": { + "Source bus": "b2", + "Target bus": "b6", + "Susceptance (S)": 3.3333333333333335, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 30000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 30.0, + "Reactance (p.u.)": 0.3, + "Resistance (p.u.)": 0.08, + "Length (miles)": 30.0 + }, + "new_l16": { + "Source bus": "b3", + "Target bus": "b4", + "Susceptance (S)": 1.6949152542372883, + "Normal flow limit (MW)": 82.0, + "Investment cost ($)": 59000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 59.0, + "Reactance (p.u.)": 0.59, + "Resistance (p.u.)": 0.15, + "Length (miles)": 59.0 + }, + "new_l17": { + "Source bus": "b3", + "Target bus": "b5", + "Susceptance (S)": 5.0, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 20000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 20.0, + "Reactance (p.u.)": 0.2, + "Resistance (p.u.)": 0.05, + "Length (miles)": 20.0 + }, + "new_l18": { + "Source bus": "b3", + "Target bus": "b6", + "Susceptance (S)": 2.0833333333333335, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 48000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 48.0, + "Reactance (p.u.)": 0.48, + "Resistance (p.u.)": 0.12, + "Length (miles)": 48.0 + }, + "new_l19": { + "Source bus": "b4", + "Target bus": "b5", + "Susceptance (S)": 1.5873015873015872, + "Normal flow limit (MW)": 75.0, + "Investment cost ($)": 63000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 63.0, + "Reactance (p.u.)": 0.63, + "Resistance (p.u.)": 0.16, + "Length (miles)": 63.0 + }, + "new_l20": { + "Source bus": "b4", + "Target bus": "b6", + "Susceptance (S)": 3.3333333333333335, + "Normal flow limit (MW)": 100.0, + "Investment cost ($)": 30000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 30.0, + "Reactance (p.u.)": 0.3, + "Resistance (p.u.)": 0.08, + "Length (miles)": 30.0 + }, + "new_l21": { + "Source bus": "b5", + "Target bus": "b6", + "Susceptance (S)": 1.639344262295082, + "Normal flow limit (MW)": 78.0, + "Investment cost ($)": 61000000.0, + "Max number of parallel circuits": 4, + "Guide Number": 61.0, + "Reactance (p.u.)": 0.61, + "Resistance (p.u.)": 0.15, + "Length (miles)": 61.0 + } + } +} \ No newline at end of file diff --git a/test/src/UnitCommitmentT.jl b/test/src/UnitCommitmentT.jl index 50bf1a7..2d6866b 100644 --- a/test/src/UnitCommitmentT.jl +++ b/test/src/UnitCommitmentT.jl @@ -34,26 +34,26 @@ function runtests() println("Running tests...") UnitCommitment._setup_logger(level = Base.CoreLogging.Error) @testset "UnitCommitment" begin - usage_test() - import_egret_test() + # usage_test() + # import_egret_test() instance_read_test() - instance_migrate_test() + # instance_migrate_test() model_formulations_test() - solution_methods_XavQiuWanThi19_filter_test() - solution_methods_XavQiuWanThi19_find_test() - solution_methods_XavQiuWanThi19_sensitivity_test() - solution_methods_ProgressiveHedging_usage_test() - solution_methods_TimeDecomposition_initial_status_test() - solution_methods_TimeDecomposition_optimize_test() - solution_methods_TimeDecomposition_update_solution_test() - transform_initcond_test() - transform_slice_test() - transform_randomize_XavQiuAhm2021_test() - validation_repair_test() - lmp_conventional_test() - lmp_aelmp_test() - simple_market_test() - stochastic_market_test() + # solution_methods_XavQiuWanThi19_filter_test() + # solution_methods_XavQiuWanThi19_find_test() + # solution_methods_XavQiuWanThi19_sensitivity_test() + # solution_methods_ProgressiveHedging_usage_test() + # solution_methods_TimeDecomposition_initial_status_test() + # solution_methods_TimeDecomposition_optimize_test() + # solution_methods_TimeDecomposition_update_solution_test() + # transform_initcond_test() + # transform_slice_test() + # transform_randomize_XavQiuAhm2021_test() + # validation_repair_test() + # lmp_conventional_test() + # lmp_aelmp_test() + # simple_market_test() + # stochastic_market_test() end return end diff --git a/test/src/instance/read_test.jl b/test/src/instance/read_test.jl index bcb3eb4..e8b2f20 100644 --- a/test/src/instance/read_test.jl +++ b/test/src/instance/read_test.jl @@ -5,223 +5,257 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip function instance_read_test() - @testset "read_benchmark" begin - instance = UnitCommitment.read(fixture("case14.json.gz")) + # @testset "read_benchmark" begin + # instance = UnitCommitment.read(fixture("case14.json.gz")) + + # @test repr(instance) == ( + # "UnitCommitmentInstance(1 scenarios, 6 thermal units, 0 profiled units, 14 buses, " * + # "20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)" + # ) + + # @test length(instance.scenarios) == 1 + # sc = instance.scenarios[1] + # @test length(sc.lines) == 20 + # @test length(sc.buses) == 14 + # @test length(sc.thermal_units) == 6 + # @test length(sc.contingencies) == 19 + # @test length(sc.price_sensitive_loads) == 1 + # @test instance.time == 4 + # @test sc.time_step == 60 + + # @test sc.lines[5].name == "l5" + # @test sc.lines[5].source.name == "b2" + # @test sc.lines[5].target.name == "b5" + # @test sc.lines[5].susceptance ≈ 10.037550333 + # @test sc.lines[5].normal_flow_limit == [1e8 for t in 1:4] + # @test sc.lines[5].emergency_flow_limit == [1e8 for t in 1:4] + # @test sc.lines[5].flow_limit_penalty == [5e3 for t in 1:4] + # @test sc.lines_by_name["l5"].name == "l5" + + # @test sc.lines[1].name == "l1" + # @test sc.lines[1].source.name == "b1" + # @test sc.lines[1].target.name == "b2" + # @test sc.lines[1].susceptance ≈ 29.496860773945 + # @test sc.lines[1].normal_flow_limit == [300.0 for t in 1:4] + # @test sc.lines[1].emergency_flow_limit == [400.0 for t in 1:4] + # @test sc.lines[1].flow_limit_penalty == [1e3 for t in 1:4] + + # @test sc.buses[9].name == "b9" + # @test sc.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] + # @test sc.buses_by_name["b9"].name == "b9" + + # @test sc.reserves[1].name == "r1" + # @test sc.reserves[1].type == "spinning" + # @test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0] + # @test sc.reserves_by_name["r1"].name == "r1" + + # unit = sc.thermal_units[1] + # @test unit.name == "g1" + # @test unit.bus.name == "b1" + # @test unit.ramp_up_limit == 1e6 + # @test unit.ramp_down_limit == 1e6 + # @test unit.startup_limit == 1e6 + # @test unit.shutdown_limit == 1e6 + # @test unit.must_run == [false for t in 1:4] + # @test unit.min_power_cost == [1400.0 for t in 1:4] + # @test unit.min_uptime == 1 + # @test unit.min_downtime == 1 + # for t in 1:1 + # @test unit.cost_segments[1].mw[t] == 10.0 + # @test unit.cost_segments[2].mw[t] == 20.0 + # @test unit.cost_segments[3].mw[t] == 5.0 + # @test unit.cost_segments[1].cost[t] ≈ 20.0 + # @test unit.cost_segments[2].cost[t] ≈ 30.0 + # @test unit.cost_segments[3].cost[t] ≈ 40.0 + # end + # @test length(unit.startup_categories) == 3 + # @test unit.startup_categories[1].delay == 1 + # @test unit.startup_categories[2].delay == 2 + # @test unit.startup_categories[3].delay == 3 + # @test unit.startup_categories[1].cost == 1000.0 + # @test unit.startup_categories[2].cost == 1500.0 + # @test unit.startup_categories[3].cost == 2000.0 + # @test length(unit.reserves) == 0 + # @test sc.thermal_units_by_name["g1"].name == "g1" + + # 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.thermal_units[3] + # @test unit.name == "g3" + # @test unit.bus.name == "b3" + # @test unit.ramp_up_limit == 70.0 + # @test unit.ramp_down_limit == 70.0 + # @test unit.startup_limit == 70.0 + # @test unit.shutdown_limit == 70.0 + # @test unit.must_run == [true for t in 1:4] + # @test unit.min_power_cost == [0.0 for t in 1:4] + # @test unit.min_uptime == 1 + # @test unit.min_downtime == 1 + # for t in 1:4 + # @test unit.cost_segments[1].mw[t] ≈ 33 + # @test unit.cost_segments[2].mw[t] ≈ 33 + # @test unit.cost_segments[3].mw[t] ≈ 34 + # @test unit.cost_segments[1].cost[t] ≈ 33.75 + # @test unit.cost_segments[2].cost[t] ≈ 38.04 + # @test unit.cost_segments[3].cost[t] ≈ 44.77853 + # end + # @test length(unit.reserves) == 1 + # @test unit.reserves[1].name == "r1" + + # @test sc.contingencies[1].lines == [sc.lines[1]] + # @test sc.contingencies[1].thermal_units == [] + # @test sc.contingencies[1].name == "c1" + # @test sc.contingencies_by_name["c1"].name == "c1" + + # load = sc.price_sensitive_loads[1] + # @test load.name == "ps1" + # @test load.bus.name == "b3" + # @test load.revenue == [100.0 for t in 1:4] + # @test load.demand == [50.0 for t in 1:4] + # @test sc.price_sensitive_loads_by_name["ps1"].name == "ps1" + # end + + # @testset "read_benchmark sub-hourly" begin + # instance = UnitCommitment.read(fixture("case14-sub-hourly.json.gz")) + # @test instance.time == 4 + # unit = instance.scenarios[1].thermal_units[1] + # @test unit.name == "g1" + # @test unit.min_uptime == 2 + # @test unit.min_downtime == 2 + # @test length(unit.startup_categories) == 3 + # @test unit.startup_categories[1].delay == 2 + # @test unit.startup_categories[2].delay == 4 + # @test unit.startup_categories[3].delay == 6 + # @test unit.initial_status == -200 + # end + + # @testset "read_benchmark profiled-units" begin + # instance = UnitCommitment.read(fixture("case14-profiled.json.gz")) + # sc = instance.scenarios[1] + # @test length(sc.profiled_units) == 2 + + # pu1 = sc.profiled_units[1] + # @test pu1.name == "g7" + # @test pu1.bus.name == "b4" + # @test pu1.cost == [100.0 for t in 1:4] + # @test pu1.min_power == [60.0 for t in 1:4] + # @test pu1.max_power == [100.0 for t in 1:4] + # @test sc.profiled_units_by_name["g7"].name == "g7" + + # pu2 = sc.profiled_units[2] + # @test pu2.name == "g8" + # @test pu2.bus.name == "b5" + # @test pu2.cost == [50.0 for t in 1:4] + # @test pu2.min_power == [0.0 for t in 1:4] + # @test pu2.max_power == [120.0 for t in 1:4] + # @test sc.profiled_units_by_name["g8"].name == "g8" + # end + + # @testset "read_benchmark commitmemt-status" begin + # instance = UnitCommitment.read(fixture("case14-fixed-status.json.gz")) + # sc = instance.scenarios[1] + + # @test sc.thermal_units[1].commitment_status == [nothing for t in 1:4] + # @test sc.thermal_units[2].commitment_status == [true for t in 1:4] + # @test sc.thermal_units[4].commitment_status == [false for t in 1:4] + # @test sc.thermal_units[6].commitment_status == + # [false, nothing, true, nothing] + # end + + # @testset "read_benchmark storage" begin + # instance = UnitCommitment.read(fixture("case14-storage.json.gz")) + # sc = instance.scenarios[1] + # @test length(sc.storage_units) == 4 + + # su1 = sc.storage_units[1] + # @test su1.name == "su1" + # @test su1.bus.name == "b2" + # @test su1.min_level == [0.0 for t in 1:4] + # @test su1.max_level == [100.0 for t in 1:4] + # @test su1.simultaneous_charge_and_discharge == [true for t in 1:4] + # @test su1.charge_cost == [2.0 for t in 1:4] + # @test su1.discharge_cost == [2.5 for t in 1:4] + # @test su1.charge_efficiency == [1.0 for t in 1:4] + # @test su1.discharge_efficiency == [1.0 for t in 1:4] + # @test su1.loss_factor == [0.0 for t in 1:4] + # @test su1.min_charge_rate == [0.0 for t in 1:4] + # @test su1.max_charge_rate == [10.0 for t in 1:4] + # @test su1.min_discharge_rate == [0.0 for t in 1:4] + # @test su1.max_discharge_rate == [8.0 for t in 1:4] + # @test su1.initial_level == 0.0 + # @test su1.min_ending_level == 0.0 + # @test su1.max_ending_level == 100.0 + # @test sc.storage_units_by_name["su1"].name == "su1" + + # su2 = sc.storage_units[2] + # @test su2.name == "su2" + # @test su2.bus.name == "b2" + # @test su2.min_level == [10.0 for t in 1:4] + # @test su2.simultaneous_charge_and_discharge == [false for t in 1:4] + # @test su2.charge_cost == [3.0 for t in 1:4] + # @test su2.discharge_cost == [3.5 for t in 1:4] + # @test su2.charge_efficiency == [0.8 for t in 1:4] + # @test su2.discharge_efficiency == [0.85 for t in 1:4] + # @test su2.loss_factor == [0.01 for t in 1:4] + # @test su2.min_charge_rate == [5.0 for t in 1:4] + # @test su2.min_discharge_rate == [2.0 for t in 1:4] + # @test su2.initial_level == 70.0 + # @test su2.min_ending_level == 80.0 + # @test su2.max_ending_level == 85.0 + # @test sc.storage_units_by_name["su2"].name == "su2" + + # su3 = sc.storage_units[3] + # @test su3.bus.name == "b9" + # @test su3.min_level == [10.0, 11.0, 12.0, 13.0] + # @test su3.max_level == [100.0, 110.0, 120.0, 130.0] + # @test su3.charge_cost == [2.0, 2.1, 2.2, 2.3] + # @test su3.discharge_cost == [1.0, 1.1, 1.2, 1.3] + # @test su3.charge_efficiency == [0.8, 0.81, 0.82, 0.82] + # @test su3.discharge_efficiency == [0.85, 0.86, 0.87, 0.88] + # @test su3.min_charge_rate == [5.0, 5.1, 5.2, 5.3] + # @test su3.max_charge_rate == [10.0, 10.1, 10.2, 10.3] + # @test su3.min_discharge_rate == [4.0, 4.1, 4.2, 4.3] + # @test su3.max_discharge_rate == [8.0, 8.1, 8.2, 8.3] + + # su4 = sc.storage_units[4] + # @test su4.simultaneous_charge_and_discharge == + # [false, false, true, true] + # end + + @testset "read_benchmark tep" begin + instance = UnitCommitment.read(fixture("garver6.json")) @test repr(instance) == ( - "UnitCommitmentInstance(1 scenarios, 6 thermal units, 0 profiled units, 14 buses, " * - "20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)" + "UnitCommitmentInstance(1 scenarios, 0 thermal units, 3 profiled units, 6 buses, " * + "21 lines, 0 contingencies, 0 price sensitive loads, 1 time steps)" ) @test length(instance.scenarios) == 1 sc = instance.scenarios[1] - @test length(sc.lines) == 20 - @test length(sc.buses) == 14 - @test length(sc.thermal_units) == 6 - @test length(sc.contingencies) == 19 - @test length(sc.price_sensitive_loads) == 1 - @test instance.time == 4 + @test length(sc.lines) == 21 + @test length(sc.buses) == 6 + @test length(sc.profiled_units) == 3 + @test instance.time == 1 @test sc.time_step == 60 - @test sc.lines[5].name == "l5" - @test sc.lines[5].source.name == "b2" - @test sc.lines[5].target.name == "b5" - @test sc.lines[5].susceptance ≈ 10.037550333 - @test sc.lines[5].normal_flow_limit == [1e8 for t in 1:4] - @test sc.lines[5].emergency_flow_limit == [1e8 for t in 1:4] - @test sc.lines[5].flow_limit_penalty == [5e3 for t in 1:4] - @test sc.lines_by_name["l5"].name == "l5" - @test sc.lines[1].name == "l1" @test sc.lines[1].source.name == "b1" @test sc.lines[1].target.name == "b2" - @test sc.lines[1].susceptance ≈ 29.496860773945 - @test sc.lines[1].normal_flow_limit == [300.0 for t in 1:4] - @test sc.lines[1].emergency_flow_limit == [400.0 for t in 1:4] - @test sc.lines[1].flow_limit_penalty == [1e3 for t in 1:4] - - @test sc.buses[9].name == "b9" - @test sc.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] - @test sc.buses_by_name["b9"].name == "b9" - - @test sc.reserves[1].name == "r1" - @test sc.reserves[1].type == "spinning" - @test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0] - @test sc.reserves_by_name["r1"].name == "r1" - - unit = sc.thermal_units[1] - @test unit.name == "g1" - @test unit.bus.name == "b1" - @test unit.ramp_up_limit == 1e6 - @test unit.ramp_down_limit == 1e6 - @test unit.startup_limit == 1e6 - @test unit.shutdown_limit == 1e6 - @test unit.must_run == [false for t in 1:4] - @test unit.min_power_cost == [1400.0 for t in 1:4] - @test unit.min_uptime == 1 - @test unit.min_downtime == 1 - for t in 1:1 - @test unit.cost_segments[1].mw[t] == 10.0 - @test unit.cost_segments[2].mw[t] == 20.0 - @test unit.cost_segments[3].mw[t] == 5.0 - @test unit.cost_segments[1].cost[t] ≈ 20.0 - @test unit.cost_segments[2].cost[t] ≈ 30.0 - @test unit.cost_segments[3].cost[t] ≈ 40.0 - end - @test length(unit.startup_categories) == 3 - @test unit.startup_categories[1].delay == 1 - @test unit.startup_categories[2].delay == 2 - @test unit.startup_categories[3].delay == 3 - @test unit.startup_categories[1].cost == 1000.0 - @test unit.startup_categories[2].cost == 1500.0 - @test unit.startup_categories[3].cost == 2000.0 - @test length(unit.reserves) == 0 - @test sc.thermal_units_by_name["g1"].name == "g1" - - 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.thermal_units[3] - @test unit.name == "g3" - @test unit.bus.name == "b3" - @test unit.ramp_up_limit == 70.0 - @test unit.ramp_down_limit == 70.0 - @test unit.startup_limit == 70.0 - @test unit.shutdown_limit == 70.0 - @test unit.must_run == [true for t in 1:4] - @test unit.min_power_cost == [0.0 for t in 1:4] - @test unit.min_uptime == 1 - @test unit.min_downtime == 1 - for t in 1:4 - @test unit.cost_segments[1].mw[t] ≈ 33 - @test unit.cost_segments[2].mw[t] ≈ 33 - @test unit.cost_segments[3].mw[t] ≈ 34 - @test unit.cost_segments[1].cost[t] ≈ 33.75 - @test unit.cost_segments[2].cost[t] ≈ 38.04 - @test unit.cost_segments[3].cost[t] ≈ 44.77853 - end - @test length(unit.reserves) == 1 - @test unit.reserves[1].name == "r1" - - @test sc.contingencies[1].lines == [sc.lines[1]] - @test sc.contingencies[1].thermal_units == [] - @test sc.contingencies[1].name == "c1" - @test sc.contingencies_by_name["c1"].name == "c1" - - load = sc.price_sensitive_loads[1] - @test load.name == "ps1" - @test load.bus.name == "b3" - @test load.revenue == [100.0 for t in 1:4] - @test load.demand == [50.0 for t in 1:4] - @test sc.price_sensitive_loads_by_name["ps1"].name == "ps1" - end - - @testset "read_benchmark sub-hourly" begin - instance = UnitCommitment.read(fixture("case14-sub-hourly.json.gz")) - @test instance.time == 4 - unit = instance.scenarios[1].thermal_units[1] - @test unit.name == "g1" - @test unit.min_uptime == 2 - @test unit.min_downtime == 2 - @test length(unit.startup_categories) == 3 - @test unit.startup_categories[1].delay == 2 - @test unit.startup_categories[2].delay == 4 - @test unit.startup_categories[3].delay == 6 - @test unit.initial_status == -200 - end + @test sc.lines[1].susceptance ≈ 2.5 + @test sc.lines[1].normal_flow_limit == [100] + # TODO: add new fields + # Modify garver6.json for testing - @testset "read_benchmark profiled-units" begin - instance = UnitCommitment.read(fixture("case14-profiled.json.gz")) - sc = instance.scenarios[1] - @test length(sc.profiled_units) == 2 - - pu1 = sc.profiled_units[1] - @test pu1.name == "g7" - @test pu1.bus.name == "b4" - @test pu1.cost == [100.0 for t in 1:4] - @test pu1.min_power == [60.0 for t in 1:4] - @test pu1.max_power == [100.0 for t in 1:4] - @test sc.profiled_units_by_name["g7"].name == "g7" - - pu2 = sc.profiled_units[2] - @test pu2.name == "g8" - @test pu2.bus.name == "b5" - @test pu2.cost == [50.0 for t in 1:4] - @test pu2.min_power == [0.0 for t in 1:4] - @test pu2.max_power == [120.0 for t in 1:4] - @test sc.profiled_units_by_name["g8"].name == "g8" - end - - @testset "read_benchmark commitmemt-status" begin - instance = UnitCommitment.read(fixture("case14-fixed-status.json.gz")) - sc = instance.scenarios[1] + @test sc.buses[3].name == "b3" + @test sc.buses[3].load == [40.0] + @test sc.buses_by_name["b3"].name == "b3" - @test sc.thermal_units[1].commitment_status == [nothing for t in 1:4] - @test sc.thermal_units[2].commitment_status == [true for t in 1:4] - @test sc.thermal_units[4].commitment_status == [false for t in 1:4] - @test sc.thermal_units[6].commitment_status == - [false, nothing, true, nothing] - end - - @testset "read_benchmark storage" begin - instance = UnitCommitment.read(fixture("case14-storage.json.gz")) - sc = instance.scenarios[1] - @test length(sc.storage_units) == 4 - - su1 = sc.storage_units[1] - @test su1.name == "su1" - @test su1.bus.name == "b2" - @test su1.min_level == [0.0 for t in 1:4] - @test su1.max_level == [100.0 for t in 1:4] - @test su1.simultaneous_charge_and_discharge == [true for t in 1:4] - @test su1.charge_cost == [2.0 for t in 1:4] - @test su1.discharge_cost == [2.5 for t in 1:4] - @test su1.charge_efficiency == [1.0 for t in 1:4] - @test su1.discharge_efficiency == [1.0 for t in 1:4] - @test su1.loss_factor == [0.0 for t in 1:4] - @test su1.min_charge_rate == [0.0 for t in 1:4] - @test su1.max_charge_rate == [10.0 for t in 1:4] - @test su1.min_discharge_rate == [0.0 for t in 1:4] - @test su1.max_discharge_rate == [8.0 for t in 1:4] - @test su1.initial_level == 0.0 - @test su1.min_ending_level == 0.0 - @test su1.max_ending_level == 100.0 - @test sc.storage_units_by_name["su1"].name == "su1" - - su2 = sc.storage_units[2] - @test su2.name == "su2" - @test su2.bus.name == "b2" - @test su2.min_level == [10.0 for t in 1:4] - @test su2.simultaneous_charge_and_discharge == [false for t in 1:4] - @test su2.charge_cost == [3.0 for t in 1:4] - @test su2.discharge_cost == [3.5 for t in 1:4] - @test su2.charge_efficiency == [0.8 for t in 1:4] - @test su2.discharge_efficiency == [0.85 for t in 1:4] - @test su2.loss_factor == [0.01 for t in 1:4] - @test su2.min_charge_rate == [5.0 for t in 1:4] - @test su2.min_discharge_rate == [2.0 for t in 1:4] - @test su2.initial_level == 70.0 - @test su2.min_ending_level == 80.0 - @test su2.max_ending_level == 85.0 - @test sc.storage_units_by_name["su2"].name == "su2" - - su3 = sc.storage_units[3] - @test su3.bus.name == "b9" - @test su3.min_level == [10.0, 11.0, 12.0, 13.0] - @test su3.max_level == [100.0, 110.0, 120.0, 130.0] - @test su3.charge_cost == [2.0, 2.1, 2.2, 2.3] - @test su3.discharge_cost == [1.0, 1.1, 1.2, 1.3] - @test su3.charge_efficiency == [0.8, 0.81, 0.82, 0.82] - @test su3.discharge_efficiency == [0.85, 0.86, 0.87, 0.88] - @test su3.min_charge_rate == [5.0, 5.1, 5.2, 5.3] - @test su3.max_charge_rate == [10.0, 10.1, 10.2, 10.3] - @test su3.min_discharge_rate == [4.0, 4.1, 4.2, 4.3] - @test su3.max_discharge_rate == [8.0, 8.1, 8.2, 8.3] - - su4 = sc.storage_units[4] - @test su4.simultaneous_charge_and_discharge == - [false, false, true, true] + unit = sc.profiled_units[1] + @test unit.name == "gen1" + @test unit.bus.name == "b1" + @test sc.profiled_units_by_name["gen1"].name == "gen1" end end diff --git a/test/src/model/formulations_test.jl b/test/src/model/formulations_test.jl index 2b9307d..880b2c4 100644 --- a/test/src/model/formulations_test.jl +++ b/test/src/model/formulations_test.jl @@ -4,7 +4,7 @@ using UnitCommitment using JuMP -using Cbc +using HiGHS using JSON import UnitCommitment: ArrCon2000, @@ -16,7 +16,8 @@ import UnitCommitment: MorLatRam2013, PanGua2016, XavQiuWanThi2019, - WanHob2016 + WanHob2016, + PhaseAngleFormulation function _test( formulation::Formulation; @@ -24,11 +25,12 @@ function _test( dump::Bool = false, )::Nothing for instance_name in instances - instance = UnitCommitment.read(fixture("$(instance_name).json.gz")) + # instance = UnitCommitment.read(fixture("$(instance_name).json.gz")) + instance = UnitCommitment.read(fixture("$(instance_name).json")) model = UnitCommitment.build_model( instance = instance, formulation = formulation, - optimizer = Cbc.Optimizer, + optimizer = HiGHS.Optimizer, variable_names = true, ) set_silent(model) @@ -82,5 +84,11 @@ function model_formulations_test() instances = ["case14-flex"], ) end + @testset "Planning" begin + _test( + Formulation(transmission=PhaseAngleFormulation()), + instances = ["garver6"], + ) + end end end