From ca092a67ce812c0157b0ada06e1e560f16b3158b Mon Sep 17 00:00:00 2001 From: Jun He Date: Mon, 17 Jul 2023 11:39:31 -0400 Subject: [PATCH] storage units --- src/UnitCommitment.jl | 1 + src/instance/read.jl | 50 +++++ src/instance/structs.jl | 23 ++ src/model/build.jl | 3 + src/model/formulations/base/storage.jl | 125 +++++++++++ src/solution/solution.jl | 24 ++ src/transform/randomize/XavQiuAhm2021.jl | 5 + src/transform/slice.jl | 15 ++ src/validation/validate.jl | 207 +++++++++++++++++- test/fixtures/case14-storage.json.gz | Bin 0 -> 2284 bytes test/src/instance/read_test.jl | 84 ++++++- .../transform/randomize/XavQiuAhm2021_test.jl | 23 ++ test/src/transform/slice_test.jl | 30 +++ 13 files changed, 576 insertions(+), 14 deletions(-) create mode 100644 src/model/formulations/base/storage.jl create mode 100644 test/fixtures/case14-storage.json.gz diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 033c6df..d46d71e 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -36,6 +36,7 @@ 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/base/storage.jl") include("model/formulations/CarArr2006/pwlcosts.jl") include("model/formulations/DamKucRajAta2016/ramp.jl") include("model/formulations/Gar1962/pwlcosts.jl") diff --git a/src/instance/read.jl b/src/instance/read.jl index 8c33657..10365ab 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -136,6 +136,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario loads = PriceSensitiveLoad[] reserves = Reserve[] profiled_units = ProfiledUnit[] + storage_units = StorageUnit[] function scalar(x; default = nothing) x !== nothing || return default @@ -196,6 +197,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario ThermalUnit[], PriceSensitiveLoad[], ProfiledUnit[], + StorageUnit[], ) name_to_bus[bus_name] = bus push!(buses, bus) @@ -405,6 +407,52 @@ function _from_json(json; repair = true)::UnitCommitmentScenario end end + # Read storage units + if "Storage units" in keys(json) + for (storage_name, dict) in json["Storage units"] + bus = name_to_bus[dict["Bus"]] + min_level = + timeseries(scalar(dict["Minimum level (MWh)"], default = 0.0)) + max_level = timeseries(dict["Maximum level (MWh)"]) + storage = StorageUnit( + storage_name, + bus, + min_level, + max_level, + timeseries( + scalar( + dict["Allow simultaneous charging and discharging"], + default = true, + ), + ), + timeseries(dict["Charge cost (\$/MW)"]), + timeseries(dict["Discharge cost (\$/MW)"]), + timeseries(scalar(dict["Charge efficiency"], default = 1.0)), + timeseries(scalar(dict["Discharge efficiency"], default = 1.0)), + timeseries(scalar(dict["Loss factor"], default = 0.0)), + timeseries( + scalar(dict["Minimum charge rate (MW)"], default = 0.0), + ), + timeseries(dict["Maximum charge rate (MW)"]), + timeseries( + scalar(dict["Minimum discharge rate (MW)"], default = 0.0), + ), + timeseries(dict["Maximum discharge rate (MW)"]), + scalar(dict["Initial level (MWh)"], default = 0.0), + scalar( + dict["Last period minimum level (MWh)"], + default = min_level[T], + ), + scalar( + dict["Last period maximum level (MWh)"], + default = max_level[T], + ), + ) + push!(bus.storage_units, storage) + push!(storage_units, storage) + end + end + scenario = UnitCommitmentScenario( name = scenario_name, probability = probability, @@ -425,6 +473,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario thermal_units = thermal_units, profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units), profiled_units = profiled_units, + storage_units_by_name = Dict(su.name => su for su in storage_units), + storage_units = storage_units, isf = spzeros(Float64, length(lines), length(buses) - 1), lodf = spzeros(Float64, length(lines), length(lines)), ) diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 39725af..3c1b635 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -9,6 +9,7 @@ mutable struct Bus thermal_units::Vector price_sensitive_loads::Vector profiled_units::Vector + storage_units::Vector end mutable struct CostSegment @@ -83,6 +84,26 @@ mutable struct ProfiledUnit cost::Vector{Float64} end +mutable struct StorageUnit + name::String + bus::Bus + min_level::Vector{Float64} + max_level::Vector{Float64} + simultaneous_charge_and_discharge::Vector{Bool} + charge_cost::Vector{Float64} + discharge_cost::Vector{Float64} + charge_efficiency::Vector{Float64} + discharge_efficiency::Vector{Float64} + loss_factor::Vector{Float64} + min_charge_rate::Vector{Float64} + max_charge_rate::Vector{Float64} + min_discharge_rate::Vector{Float64} + max_discharge_rate::Vector{Float64} + initial_level::Float64 + min_ending_level::Float64 + max_ending_level::Float64 +end + Base.@kwdef mutable struct UnitCommitmentScenario buses_by_name::Dict{AbstractString,Bus} buses::Vector{Bus} @@ -103,6 +124,8 @@ Base.@kwdef mutable struct UnitCommitmentScenario reserves::Vector{Reserve} thermal_units_by_name::Dict{AbstractString,ThermalUnit} thermal_units::Vector{ThermalUnit} + storage_units_by_name::Dict{AbstractString,StorageUnit} + storage_units::Vector{StorageUnit} time::Int time_step::Int end diff --git a/src/model/build.jl b/src/model/build.jl index a812011..0e27d42 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -99,6 +99,9 @@ function build_model(; for pu in sc.profiled_units _add_profiled_unit!(model, pu, sc) end + for su in sc.storage_units + _add_storage_unit!(model, su, sc) + end _add_system_wide_eqs!(model, sc) end @objective(model, Min, model[:obj]) diff --git a/src/model/formulations/base/storage.jl b/src/model/formulations/base/storage.jl new file mode 100644 index 0000000..93cf297 --- /dev/null +++ b/src/model/formulations/base/storage.jl @@ -0,0 +1,125 @@ +# 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_storage_unit!( + model::JuMP.Model, + su::StorageUnit, + sc::UnitCommitmentScenario, +)::Nothing + # Initialize variables + storage_level = _init(model, :storage_level) + charge_rate = _init(model, :charge_rate) + discharge_rate = _init(model, :discharge_rate) + is_charging = _init(model, :is_charging) + is_discharging = _init(model, :is_discharging) + eq_min_charge_rate = _init(model, :eq_min_charge_rate) + eq_max_charge_rate = _init(model, :eq_max_charge_rate) + eq_min_discharge_rate = _init(model, :eq_min_discharge_rate) + eq_max_discharge_rate = _init(model, :eq_max_discharge_rate) + # Initialize constraints + net_injection = _init(model, :expr_net_injection) + eq_storage_transition = _init(model, :eq_storage_transition) + eq_ending_level = _init(model, :eq_ending_level) + # time in hours + time_step = sc.time_step / 60 + + for t in 1:model[:instance].time + # Decision variable + storage_level[sc.name, su.name, t] = @variable( + model, + lower_bound = su.min_level[t], + upper_bound = su.max_level[t] + ) + charge_rate[sc.name, su.name, t] = @variable(model) + discharge_rate[sc.name, su.name, t] = @variable(model) + is_charging[sc.name, su.name, t] = @variable(model, binary = true) + is_discharging[sc.name, su.name, t] = @variable(model, binary = true) + + # Objective function terms ##### CHECK & FIXME + add_to_expression!( + model[:obj], + charge_rate[sc.name, su.name, t], + su.charge_cost[t] * sc.probability, + ) + + add_to_expression!( + model[:obj], + discharge_rate[sc.name, su.name, t], + su.discharge_cost[t] * sc.probability, + ) + + # Net injection + add_to_expression!( + net_injection[sc.name, su.bus.name, t], + discharge_rate[sc.name, su.name, t], + 1.0, + ) + add_to_expression!( + net_injection[sc.name, su.bus.name, t], + charge_rate[sc.name, su.name, t], + -1.0, + ) + + # Simultaneous charging and discharging + if !su.simultaneous_charge_and_discharge[t] + # Initialize the model dictionary + eq_simultaneous_charge_and_discharge = + _init(model, :eq_simultaneous_charge_and_discharge) + # Constraints + eq_simultaneous_charge_and_discharge[sc.name, su.name, t] = + @constraint( + model, + is_charging[sc.name, su.name, t] + + is_discharging[sc.name, su.name, t] <= 1.0 + ) + end + + # Charge and discharge constraints + eq_min_charge_rate[sc.name, su.name, t] = @constraint( + model, + charge_rate[sc.name, su.name, t] >= + is_charging[sc.name, su.name, t] * su.min_charge_rate[t] + ) + eq_max_charge_rate[sc.name, su.name, t] = @constraint( + model, + charge_rate[sc.name, su.name, t] <= + is_charging[sc.name, su.name, t] * su.max_charge_rate[t] + ) + eq_min_discharge_rate[sc.name, su.name, t] = @constraint( + model, + discharge_rate[sc.name, su.name, t] >= + is_discharging[sc.name, su.name, t] * su.min_discharge_rate[t] + ) + eq_max_discharge_rate[sc.name, su.name, t] = @constraint( + model, + discharge_rate[sc.name, su.name, t] <= + is_discharging[sc.name, su.name, t] * su.max_discharge_rate[t] + ) + + # Storage energy transition constraint + prev_storage_level = + t == 1 ? su.initial_level : storage_level[sc.name, su.name, t-1] + eq_storage_transition[sc.name, su.name, t] = @constraint( + model, + storage_level[sc.name, su.name, t] == + (1 - su.loss_factor[t]) * prev_storage_level + + charge_rate[sc.name, su.name, t] * + time_step * + su.charge_efficiency[t] - + discharge_rate[sc.name, su.name, t] * time_step / + su.discharge_efficiency[t] + ) + + # Storage ending level constraint + if t == sc.time + eq_ending_level[sc.name, su.name] = @constraint( + model, + su.min_ending_level <= + storage_level[sc.name, su.name, t] <= + su.max_ending_level + ) + end + end + return +end diff --git a/src/solution/solution.jl b/src/solution/solution.jl index 3cb6f41..c3a9cdf 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -103,6 +103,30 @@ function solution(model::JuMP.Model)::OrderedDict ] for pu in sc.profiled_units ) end + if !isempty(sc.storage_units) + sol[sc.name]["Storage level (MWh)"] = + timeseries(model[:storage_level], sc.storage_units, sc = sc) + sol[sc.name]["Is charging"] = + timeseries(model[:is_charging], sc.storage_units, sc = sc) + sol[sc.name]["Storage charging rates (MW)"] = + timeseries(model[:charge_rate], sc.storage_units, sc = sc) + sol[sc.name]["Storage charging cost (\$)"] = OrderedDict( + su.name => [ + value(model[:charge_rate][sc.name, su.name, t]) * + su.charge_cost[t] for t in 1:instance.time + ] for su in sc.storage_units + ) + sol[sc.name]["Is discharging"] = + timeseries(model[:is_discharging], sc.storage_units, sc = sc) + sol[sc.name]["Storage discharging rates (MW)"] = + timeseries(model[:discharge_rate], sc.storage_units, sc = sc) + sol[sc.name]["Storage discharging cost (\$)"] = OrderedDict( + su.name => [ + value(model[:discharge_rate][sc.name, su.name, t]) * + su.discharge_cost[t] for t in 1:instance.time + ] for su in sc.storage_units + ) + end sol[sc.name]["Spinning reserve (MW)"] = OrderedDict( r.name => OrderedDict( g.name => [ diff --git a/src/transform/randomize/XavQiuAhm2021.jl b/src/transform/randomize/XavQiuAhm2021.jl index f58d9cc..8404388 100644 --- a/src/transform/randomize/XavQiuAhm2021.jl +++ b/src/transform/randomize/XavQiuAhm2021.jl @@ -137,6 +137,11 @@ function _randomize_costs( α = rand(rng, distribution) pu.cost *= α end + for su in sc.storage_units + α = rand(rng, distribution) + su.charge_cost *= α + su.discharge_cost *= α + end return end diff --git a/src/transform/slice.jl b/src/transform/slice.jl index 3fbbdaa..ef90082 100644 --- a/src/transform/slice.jl +++ b/src/transform/slice.jl @@ -56,6 +56,21 @@ function slice( ps.demand = ps.demand[range] ps.revenue = ps.revenue[range] end + for su in sc.storage_units + su.min_level = su.min_level[range] + su.max_level = su.max_level[range] + su.simultaneous_charge_and_discharge = + su.simultaneous_charge_and_discharge[range] + su.charge_cost = su.charge_cost[range] + su.discharge_cost = su.discharge_cost[range] + su.charge_efficiency = su.charge_efficiency[range] + su.discharge_efficiency = su.discharge_efficiency[range] + su.loss_factor = su.loss_factor[range] + su.min_charge_rate = su.min_charge_rate[range] + su.max_charge_rate = su.max_charge_rate[range] + su.min_discharge_rate = su.min_discharge_rate[range] + su.max_discharge_rate = su.max_discharge_rate[range] + end end return modified end diff --git a/src/validation/validate.jl b/src/validation/validate.jl index 863b675..1b60995 100644 --- a/src/validation/validate.jl +++ b/src/validation/validate.jl @@ -334,6 +334,195 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01) end end end + for su in sc.storage_units + storage_level = solution[sc.name]["Storage level (MWh)"][su.name] + charge_rate = + solution[sc.name]["Storage charging rates (MW)"][su.name] + discharge_rate = + solution[sc.name]["Storage discharging rates (MW)"][su.name] + actual_charge_cost = + solution[sc.name]["Storage charging cost (\$)"][su.name] + actual_discharge_cost = + solution[sc.name]["Storage discharging cost (\$)"][su.name] + is_charging = bin(solution[sc.name]["Is charging"][su.name]) + is_discharging = bin(solution[sc.name]["Is discharging"][su.name]) + # time in hours + time_step = sc.time_step / 60 + + for t in 1:instance.time + # Unit must store at least its minimum level + if storage_level[t] < su.min_level[t] - tol + @error @sprintf( + "Storage unit %s stores below its minimum level at time %d (%.2f < %.2f)", + su.name, + t, + storage_level[t], + su.min_level[t] + ) + err_count += 1 + end + # Unit must store at most its maximum level + if storage_level[t] > su.max_level[t] + tol + @error @sprintf( + "Storage unit %s stores above its maximum level at time %d (%.2f > %.2f)", + su.name, + t, + storage_level[t], + su.max_level[t] + ) + err_count += 1 + end + + if t == instance.time + # Unit must store at least its minimum level at last time period + if storage_level[t] < su.min_ending_level - tol + @error @sprintf( + "Storage unit %s stores below its minimum ending level (%.2f < %.2f)", + su.name, + storage_level[t], + su.min_ending_level + ) + err_count += 1 + end + # Unit must store at most its maximum level at last time period + if storage_level[t] > su.max_ending_level + tol + @error @sprintf( + "Storage unit %s stores above its maximum ending level (%.2f > %.2f)", + su.name, + storage_level[t], + su.max_ending_level + ) + err_count += 1 + end + end + + # Unit must follow the energy transition constraint + prev_level = t == 1 ? su.initial_level : storage_level[t-1] + current_level = + (1 - su.loss_factor[t]) * prev_level + + time_step * ( + charge_rate[t] * su.charge_efficiency[t] - + discharge_rate[t] / su.discharge_efficiency[t] + ) + if abs(storage_level[t] - current_level) > tol + @error @sprintf( + "Storage unit %s has unexpected level at time %d (%.2f should be %.2f)", + unit.name, + t, + storage_level[t], + current_level + ) + err_count += 1 + end + + # Unit cannot simultaneous charge and discharge if it is not allowed + if !su.simultaneous_charge_and_discharge[t] && + is_charging[t] && + is_discharging[t] + @error @sprintf( + "Storage unit %s is charging and discharging simultaneous at time %d", + su.name, + t + ) + err_count += 1 + end + + # Unit must charge at least its minimum rate + if is_charging[t] && + (charge_rate[t] < su.min_charge_rate[t] - tol) + @error @sprintf( + "Storage unit %s charges below its minimum limit at time %d (%.2f < %.2f)", + unit.name, + t, + charge_rate[t], + su.min_charge_rate[t] + ) + err_count += 1 + end + # Unit must charge at most its maximum rate + if is_charging[t] && + (charge_rate[t] > su.max_charge_rate[t] + tol) + @error @sprintf( + "Storage unit %s charges above its maximum limit at time %d (%.2f > %.2f)", + unit.name, + t, + charge_rate[t], + su.max_charge_rate[t] + ) + err_count += 1 + end + # Unit must have zero charge when it is not charging + if !is_charging[t] && (charge_rate[t] > tol) + @error @sprintf( + "Storage unit %s charges power at time %d while not charging (%.2f > 0)", + unit.name, + t, + charge_rate[t] + ) + err_count += 1 + end + + # Unit must discharge at least its minimum rate + if is_discharging[t] && + (discharge_rate[t] < su.min_discharge_rate[t] - tol) + @error @sprintf( + "Storage unit %s discharges below its minimum limit at time %d (%.2f < %.2f)", + unit.name, + t, + discharge_rate[t], + su.min_discharge_rate[t] + ) + err_count += 1 + end + # Unit must discharge at most its maximum rate + if is_discharging[t] && + (discharge_rate[t] > su.max_discharge_rate[t] + tol) + @error @sprintf( + "Storage unit %s discharges above its maximum limit at time %d (%.2f > %.2f)", + unit.name, + t, + discharge_rate[t], + su.max_discharge_rate[t] + ) + err_count += 1 + end + # Unit must have zero discharge when it is not charging + if !is_discharging[t] && (discharge_rate[t] > tol) + @error @sprintf( + "Storage unit %s discharges power at time %d while not discharging (%.2f > 0)", + unit.name, + t, + discharge_rate[t] + ) + err_count += 1 + end + + # Compute storage costs + charge_cost = su.charge_cost[t] * charge_rate[t] + discharge_cost = su.discharge_cost[t] * discharge_rate[t] + # Compare costs + if abs(actual_charge_cost[t] - charge_cost) > tol + @error @sprintf( + "Storage unit %s has unexpected charge cost at time %d (%.2f should be %.2f)", + unit.name, + t, + actual_charge_cost[t], + charge_cost + ) + err_count += 1 + end + if abs(actual_discharge_cost[t] - discharge_cost) > tol + @error @sprintf( + "Storage unit %s has unexpected discharge cost at time %d (%.2f should be %.2f)", + unit.name, + t, + actual_discharge_cost[t], + discharge_cost + ) + err_count += 1 + end + end + end end return err_count end @@ -346,6 +535,8 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) fixed_load = sum(b.load[t] for b in sc.buses) ps_load = 0 production = 0 + storage_charge = 0 + storage_discharge = 0 if length(sc.price_sensitive_loads) > 0 ps_load = sum( solution[sc.name]["Price-sensitive loads (MW)"][ps.name][t] @@ -364,23 +555,35 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) for pu in sc.profiled_units ) end + if length(sc.storage_units) > 0 + storage_charge += sum( + solution[sc.name]["Storage charging rates (MW)"][su.name][t] + for su in sc.storage_units + ) + storage_discharge += sum( + solution[sc.name]["Storage discharging rates (MW)"][su.name][t] + for su in sc.storage_units + ) + end if "Load curtail (MW)" in keys(solution) load_curtail = sum( solution[sc.name]["Load curtail (MW)"][b.name][t] for b in sc.buses ) end - balance = fixed_load - load_curtail - production + ps_load + balance = fixed_load - load_curtail - production + ps_load + storage_charge - storage_discharge # Verify that production equals demand if abs(balance) > tol @error @sprintf( - "Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f != 0)", + "Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f + %.2f - %.2f != 0)", t, fixed_load, ps_load, load_curtail, production, + storage_charge, + storage_discharge, ) err_count += 1 end diff --git a/test/fixtures/case14-storage.json.gz b/test/fixtures/case14-storage.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..6a5451a7fd11ce7419c13fe538d1fa5a0fb545de GIT binary patch literal 2284 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 GH~;`A162b6 literal 0 HcmV?d00001 diff --git a/test/src/instance/read_test.jl b/test/src/instance/read_test.jl index c9ae1fd..2ddffbc 100644 --- a/test/src/instance/read_test.jl +++ b/test/src/instance/read_test.jl @@ -139,20 +139,20 @@ function instance_read_test() 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.min_power == [60.0 for t in 1:4] - @test first_pu.max_power == [100.0 for t in 1:4] + 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" - 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.min_power == [0.0 for t in 1:4] - @test second_pu.max_power == [120.0 for t in 1:4] + 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 @@ -166,4 +166,64 @@ function instance_read_test() @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 end diff --git a/test/src/transform/randomize/XavQiuAhm2021_test.jl b/test/src/transform/randomize/XavQiuAhm2021_test.jl index 7819529..dfe397f 100644 --- a/test/src/transform/randomize/XavQiuAhm2021_test.jl +++ b/test/src/transform/randomize/XavQiuAhm2021_test.jl @@ -102,5 +102,28 @@ function transform_randomize_XavQiuAhm2021_test() test_approx(pu1.cost[1], 98.039) test_approx(pu2.cost[1], 48.385) end + + @testset "storage unit cost" begin + sc = UnitCommitment.read( + fixture("case14-storage.json.gz"), + ).scenarios[1] + # Check original costs + su1 = sc.storage_units[1] + su3 = sc.storage_units[3] + test_approx(su1.charge_cost[4], 2.0) + test_approx(su1.discharge_cost[1], 2.5) + test_approx(su3.charge_cost[2], 2.1) + test_approx(su3.discharge_cost[3], 1.2) + randomize!( + sc, + XavQiuAhm2021.Randomization(randomize_load_profile = false), + rng = MersenneTwister(42), + ) + # Check randomized costs + test_approx(su1.charge_cost[4], 1.961) + test_approx(su1.discharge_cost[1], 2.451) + test_approx(su3.charge_cost[2], 2.196) + test_approx(su3.discharge_cost[3], 1.255) + end end end diff --git a/test/src/transform/slice_test.jl b/test/src/transform/slice_test.jl index affe864..fdd86b4 100644 --- a/test/src/transform/slice_test.jl +++ b/test/src/transform/slice_test.jl @@ -65,4 +65,34 @@ function transform_slice_test() variable_names = true, ) end + + @testset "slice storage units" begin + instance = UnitCommitment.read(fixture("case14-storage.json.gz")) + modified = UnitCommitment.slice(instance, 2:4) + sc = modified.scenarios[1] + + # Should update all time-dependent fields + for su in sc.storage_units + @test length(su.min_level) == 3 + @test length(su.max_level) == 3 + @test length(su.simultaneous_charge_and_discharge) == 3 + @test length(su.charge_cost) == 3 + @test length(su.discharge_cost) == 3 + @test length(su.charge_efficiency) == 3 + @test length(su.discharge_efficiency) == 3 + @test length(su.loss_factor) == 3 + @test length(su.min_charge_rate) == 3 + @test length(su.max_charge_rate) == 3 + @test length(su.min_discharge_rate) == 3 + @test length(su.max_discharge_rate) == 3 + end + + # Should be able to build model without errors + optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) + model = UnitCommitment.build_model( + instance = modified, + optimizer = optimizer, + variable_names = true, + ) + end end