From e24f22553b32b23b3899d3b4ece0334665190d18 Mon Sep 17 00:00:00 2001 From: Jun He Date: Sun, 26 Mar 2023 23:11:54 -0400 Subject: [PATCH] Created profiled units and tests --- src/UnitCommitment.jl | 3 + src/instance/migrate.jl | 12 ++ src/instance/read.jl | 173 +++++++++++++++----------- src/instance/structs.jl | 11 ++ src/model/build.jl | 3 + src/model/formulations/base/punit.jl | 27 ++++ test/fixtures/case14-profiled.json.gz | Bin 0 -> 1921 bytes test/fixtures/ucjl-0.4.json.gz | Bin 0 -> 1914 bytes test/instance/migrate_test.jl | 8 ++ test/instance/read_test.jl | 114 +++++++++++++++++ 10 files changed, 275 insertions(+), 76 deletions(-) create mode 100644 src/model/formulations/base/punit.jl create mode 100644 test/fixtures/case14-profiled.json.gz create mode 100644 test/fixtures/ucjl-0.4.json.gz diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 484e5be..a2886fc 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -4,6 +4,8 @@ module UnitCommitment +@info "PU" ##REMOVE ME + include("instance/structs.jl") include("model/formulations/base/structs.jl") include("solution/structs.jl") @@ -30,6 +32,7 @@ include("model/formulations/base/psload.jl") include("model/formulations/base/sensitivity.jl") include("model/formulations/base/system.jl") include("model/formulations/base/unit.jl") +include("model/formulations/base/punit.jl") include("model/formulations/CarArr2006/pwlcosts.jl") include("model/formulations/DamKucRajAta2016/ramp.jl") include("model/formulations/Gar1962/pwlcosts.jl") diff --git a/src/instance/migrate.jl b/src/instance/migrate.jl index becb912..317a309 100644 --- a/src/instance/migrate.jl +++ b/src/instance/migrate.jl @@ -17,6 +17,7 @@ function _migrate(json) end version = VersionNumber(version) version >= v"0.3" || _migrate_to_v03(json) + version >= v"0.4" || _migrate_to_v04(json) return end @@ -36,3 +37,14 @@ function _migrate_to_v03(json) end end end + +function _migrate_to_v04(json) + # Migrate thermal units + if json["Generators"] !== nothing + for (gen_name, gen) in json["Generators"] + if gen["Type"] === nothing + gen["Type"] = "Thermal" + end + end + end +end diff --git a/src/instance/read.jl b/src/instance/read.jl index 1586e63..98393e7 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -85,6 +85,7 @@ function _from_json(json; repair = true) lines = TransmissionLine[] loads = PriceSensitiveLoad[] reserves = Reserve[] + profiled_units = ProfiledUnit[] function scalar(x; default = nothing) x !== nothing || return default @@ -135,6 +136,7 @@ function _from_json(json; repair = true) length(buses), timeseries(dict["Load (MW)"]), Unit[], + ProfiledUnit[], PriceSensitiveLoad[], ) name_to_bus[bus_name] = bus @@ -161,90 +163,107 @@ function _from_json(json; repair = true) # Read units for (unit_name, dict) in json["Generators"] + # Read and validate unit type + unit_type = scalar(dict["Type"], default = nothing) + unit_type !== nothing || error("unit $unit_name has no type specified") bus = name_to_bus[dict["Bus"]] - # Read production cost curve - K = length(dict["Production cost curve (MW)"]) - curve_mw = hcat( - [timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]..., - ) - curve_cost = hcat( - [timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]..., - ) - min_power = curve_mw[:, 1] - max_power = curve_mw[:, K] - min_power_cost = curve_cost[:, 1] - segments = CostSegment[] - for k in 2:K - amount = curve_mw[:, k] - curve_mw[:, k-1] - cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount - replace!(cost, NaN => 0.0) - push!(segments, CostSegment(amount, cost)) - end - - # Read startup costs - startup_delays = scalar(dict["Startup delays (h)"], default = [1]) - startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0]) - startup_categories = StartupCategory[] - for k in 1:length(startup_delays) - push!( - startup_categories, - StartupCategory( - startup_delays[k] .* time_multiplier, - startup_costs[k], - ), + if lowercase(unit_type) === "thermal" + # Read production cost curve + K = length(dict["Production cost curve (MW)"]) + curve_mw = hcat( + [timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]..., ) - end + curve_cost = hcat( + [timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]..., + ) + min_power = curve_mw[:, 1] + max_power = curve_mw[:, K] + min_power_cost = curve_cost[:, 1] + segments = CostSegment[] + for k in 2:K + amount = curve_mw[:, k] - curve_mw[:, k-1] + cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount + replace!(cost, NaN => 0.0) + push!(segments, CostSegment(amount, cost)) + end - # Read reserve eligibility - unit_reserves = Reserve[] - if "Reserve eligibility" in keys(dict) - unit_reserves = - [name_to_reserve[n] for n in dict["Reserve eligibility"]] - end + # Read startup costs + startup_delays = scalar(dict["Startup delays (h)"], default = [1]) + startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0]) + startup_categories = StartupCategory[] + for k in 1:length(startup_delays) + push!( + startup_categories, + StartupCategory( + startup_delays[k] .* time_multiplier, + startup_costs[k], + ), + ) + end - # Read and validate initial conditions - initial_power = scalar(dict["Initial power (MW)"], default = nothing) - initial_status = scalar(dict["Initial status (h)"], default = nothing) - if initial_power === nothing - initial_status === nothing || - error("unit $unit_name has initial status but no initial power") - else - initial_status !== nothing || - error("unit $unit_name has initial power but no initial status") - initial_status != 0 || - error("unit $unit_name has invalid initial status") - if initial_status < 0 && initial_power > 1e-3 - error("unit $unit_name has invalid initial power") + # Read reserve eligibility + unit_reserves = Reserve[] + if "Reserve eligibility" in keys(dict) + unit_reserves = + [name_to_reserve[n] for n in dict["Reserve eligibility"]] end - initial_status *= time_multiplier - end - unit = Unit( - unit_name, - bus, - max_power, - min_power, - timeseries(dict["Must run?"], default = [false for t in 1:T]), - min_power_cost, - segments, - scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier, - scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier, - scalar(dict["Ramp up limit (MW)"], default = 1e6), - scalar(dict["Ramp down limit (MW)"], default = 1e6), - scalar(dict["Startup limit (MW)"], default = 1e6), - scalar(dict["Shutdown limit (MW)"], default = 1e6), - initial_status, - initial_power, - startup_categories, - unit_reserves, - ) - push!(bus.units, unit) - for r in unit_reserves - push!(r.units, unit) + # Read and validate initial conditions + initial_power = scalar(dict["Initial power (MW)"], default = nothing) + initial_status = scalar(dict["Initial status (h)"], default = nothing) + if initial_power === nothing + initial_status === nothing || + error("unit $unit_name has initial status but no initial power") + else + initial_status !== nothing || + error("unit $unit_name has initial power but no initial status") + initial_status != 0 || + error("unit $unit_name has invalid initial status") + if initial_status < 0 && initial_power > 1e-3 + error("unit $unit_name has invalid initial power") + end + initial_status *= time_multiplier + end + + unit = Unit( + unit_name, + bus, + max_power, + min_power, + timeseries(dict["Must run?"], default = [false for t in 1:T]), + min_power_cost, + segments, + scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier, + scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier, + scalar(dict["Ramp up limit (MW)"], default = 1e6), + scalar(dict["Ramp down limit (MW)"], default = 1e6), + scalar(dict["Startup limit (MW)"], default = 1e6), + scalar(dict["Shutdown limit (MW)"], default = 1e6), + initial_status, + initial_power, + startup_categories, + unit_reserves, + ) + push!(bus.units, unit) + for r in unit_reserves + push!(r.units, unit) + end + name_to_unit[unit_name] = unit + push!(units, unit) + elseif lowercase(unit_type) === "profiled" + bus = name_to_bus[dict["Bus"]] + pu = ProfiledUnit( + unit_name, + bus, + timeseries(dict["Maximum power (MW)"]), + timeseries(dict["Cost (\$/MW)"]) + ) + push!(bus.profiled_units, pu) + push!(profiled_units, pu) + else + error("unit $unit_name has an invalid type") end - name_to_unit[unit_name] = unit - push!(units, unit) end # Read transmission lines @@ -325,6 +344,8 @@ function _from_json(json; repair = true) time = T, units_by_name = Dict(g.name => g for g in units), units = units, + profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units), + profiled_units = profiled_units, ) if repair UnitCommitment.repair!(instance) diff --git a/src/instance/structs.jl b/src/instance/structs.jl index f07f694..3d74524 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -7,6 +7,7 @@ mutable struct Bus offset::Int load::Vector{Float64} units::Vector + profiled_units::Vector price_sensitive_loads::Vector end @@ -48,6 +49,13 @@ mutable struct Unit reserves::Vector{Reserve} end +mutable struct ProfiledUnit + name::String + bus::Bus + capacity::Vector{Float64} + cost::Vector{Float64} +end + mutable struct TransmissionLine name::String offset::Int @@ -90,11 +98,14 @@ Base.@kwdef mutable struct UnitCommitmentInstance time::Int units_by_name::Dict{AbstractString,Unit} units::Vector{Unit} + profiled_units_by_name::Dict{AbstractString,ProfiledUnit} + profiled_units::Vector{ProfiledUnit} end function Base.show(io::IO, instance::UnitCommitmentInstance) print(io, "UnitCommitmentInstance(") print(io, "$(length(instance.units)) units, ") + print(io, "$(length(instance.profiled_units)) profiled units, ") print(io, "$(length(instance.buses)) buses, ") print(io, "$(length(instance.lines)) lines, ") print(io, "$(length(instance.contingencies)) contingencies, ") diff --git a/src/model/build.jl b/src/model/build.jl index 8ee7235..090aad9 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -90,6 +90,9 @@ function build_model(; for ps in instance.price_sensitive_loads _add_price_sensitive_load!(model, ps) end + for pu in instance.profiled_units + _add_profiled_unit!(model, pu) + end _add_system_wide_eqs!(model) @objective(model, Min, model[:obj]) end diff --git a/src/model/formulations/base/punit.jl b/src/model/formulations/base/punit.jl new file mode 100644 index 0000000..81e86cc --- /dev/null +++ b/src/model/formulations/base/punit.jl @@ -0,0 +1,27 @@ +# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +function _add_profiled_unit!( + model::JuMP.Model, + pu::ProfiledUnit, +)::Nothing + punits = _init(model, :profiled_units) + net_injection = _init(model, :expr_net_injection) + for t in 1:model[:instance].time + # Decision variable + punits[pu.name, t] = + @variable(model, lower_bound = 0, upper_bound = pu.capacity[t]) + + # Objective function terms + add_to_expression!(model[:obj], punits[pu.name, t], pu.cost[t]) + + # Net injection + add_to_expression!( + net_injection[pu.bus.name, t], + punits[pu.name, t], + 1.0, + ) + end + return +end diff --git a/test/fixtures/case14-profiled.json.gz b/test/fixtures/case14-profiled.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c98d7d5e85add5d38e5aeeee8a922beff2643dff GIT binary patch literal 1921 zcmV-{2Y&b;iwFn>2O(qt17l%xWid1@aB^>EX>4U=E^2dcZUF6C$&TAb5ItwUf)IR| z0CuPMC6~Z)76ga`!($|eAP0_yGedwP1uk|bhW|Z9%_g#oVoA1~1c>Z|-CZiauC98u zQ14F?_$*$fb-K#htZo)xC+|#W~-bDh0!{s;i4` z*#g33Sv764?CQ5!a{BVO9?tIr+5RIqe;O|eY*C?$X0Y}L4b77YFd9W;u~jM2RnV{# z&{><-ZMRM?vLd}{?rsS|f~by{6zNg@!JFwbbIuudYJ;!Kjr`6B|5WB}o)$^drfqkB z#%Iw@Kgg}CcUgV!B9E#+Y`ON8oc2(8Tn|NTpk2HcG-5-&+fe&?C+dX8GRzOPQ;I1B zrc=Q%6^`Vi`V%;$ns7)x?|84%)jEL(6!|J|?{75tv9?>ei|SoDD|rujR_?lMNeW-) zWxncGfU@n``i`Y1gnEs;#L_=+#nQaENqTO-y6GRi5iYx;*eT!iM{96NJc2zbe#f@X znymNgtjMqO^SsF08}!;2wO9-<7bgNWO$6$*6M<6knzaZK%w&a+CITUZV$L88p=@9r zAp+%tV1^d32M|HLDdnHxkK>TV$5nktzG5?d#s2>*-r)SNU$OniuJ{vQ9p5Ca zW42gFz6m#d6ME_T+>Oj^0Xx}( z*Tcw0Q<-yQD)V!dUOZeQ!%umR^vc`ci!W9}6Df>DSV3y0trW=YK`JJLlNiNF7Pt?N z;40@xHNP9G5TIR2i?FKTmWjYsz_tVv3hReMU|M^jDPWO0VK-RgnBvAD)-#HEuO$N2 z2&RNr9@*h=7mAbYisZZ9Py_2z23EE+V;u&^Ghu>8YJX^YrP7iD6XEQc>sL%mC$Za? z%t~VsJg~#1M%_X%FIDgqs0gW%f)Sw{()Yr?rcs6|Yq6VB5UNH8Bvh4P)(P}G5HMO9 zMKO)wdtW1G4j2L(nQ?foF$ki-td$z6oB}gkXhA|cL#Z)R4U}Rz;#Gm4R8r;86w3S63YF_8wlEKG)4`RzULw{Xk@+aebUr*Od<469)w09&FvPVs51gIeHCotDiiZ~E6fNs*W1!k|FB z_^j&cWtN;zzO_l~tE_$4k7nktvvk>}C8#*9u2;<+HJKnBfBcN61!rBe%+_~bJL|tA zA)N;6Eq6{qeDHCeUi?z^1$S~;RPTm!4;4>v7T>QjKwU0x5P6Nr|L`ca&dRiCZw58t z6xS9=xb11rsCq>0DW&3i%bs%9p5TPiK+FWA6a$W@^F55s#vl9ExtGgSO5nXK~a>$u&Y9x08q1 znrkWMsZ9&$6l~x&80VEV4hN0YdwYU*G%n#mi17HKge6c2x5mO>5j5BM0g%FT&jSR~ z0Sus+_izP$n+MR^dfvOboiICAgN|Uw9sUq@H0#CfZY1B&?v@2 z3EcA;a~3x-WkL-(?<3IHs^EZEw1uLne@Q=qdp>DK^fAI64MV4rrZBUxLqF;1{mBE9 zn`zYZS#u6GjZ=i66sb^*dka;SlaOSUw0g?9=hJ2cYwRSPmVx}ZMZ;=q&VUD<2j&Lk zTud?HeA=8vP0RKGVmvxPLlXLA4$Z!mzRy{HBJYU#%o$trPzIFaS0Jc!a}O=MGEQ3f zGvy@n`7&m>1F0kgLQM$Ft%UawjY2~IHQ^<4K3~SD4s#QwJ)zSv{Ro;vStTJLm6BUO z>>GB+FRHT5%f2nl$Bpq4$)V3LFSBKvT|6|>$a{@~^j>3$uXqcqklC&dqNz(2}+7VJCY4;@U2=|b*2ac2(V8rbRJ78NS7M^5NufUKpTi Hf-wL9B*&aE literal 0 HcmV?d00001 diff --git a/test/fixtures/ucjl-0.4.json.gz b/test/fixtures/ucjl-0.4.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..0721e54a4a70e4d4c435d0a1f99daee4b6f10b2e GIT binary patch literal 1914 zcmV-=2Zi__iwFoA0U=}n19f9+Y%MS@G%jj$Z*BnXTFH*vMi4z`zJd^Zm;iRC_a&FW zaTWxK1H)q^had-zhBHHeA_XpXCWik#Ma?F%i(*N(oCJvMgWX*!zOJr%wNUR*68J1$ zrFFW>+N^FCUnlQRzF7PU|IDir{=b^cdXCa2e5f{5Piez+A{ z{Fs$lown7Tu7@vPiNXIjYu|Rg(BfQdJ>m8#H|wn5d2^lBtF#zzzpAT?ZrK9DWLY(B zvh3=&S#tXFw;s;#1KIu~IDZ;13T#oKi)OI)2Mx`W2{0N(W3g2!(N)l}6VO?k)@`>= zF0vxMY3^cb2me&&ZJriM)23~Af5vCgO+U!3 zt9My_?;?+?KWw@7m7Mlad0Y=gY@l7d7Bpf*z1vXxc_-?G#xl$gwNr{I1g2BLFcpsE zqxus#q?&L@J@0t0)73hG2Nd}#Z|`q3__4NIxr^#uIV*V&c~R)DhY z+4_#9Cxm*9yTsBzZ^hEQxJi0$zq;uky%8?EqSz_l^haxONj!o*DSpSc&YGa57G z^7FjN+Z*)S7qwUnFBc~QHBAKSvlD?*@tU;=5zJ(Tk0t^kgksJh454gb93cYbgJ6ah zum=!9yyTCVtmMSHI*>D5q-}~b5J{9XFgcVn5W$xnxJ2ER-@tZTcNv9J1dUQ6vCm8? z_TNA$dYV$~aVh1W;g92x#m7~BN4{b+eZ~I&E8gJzuV1nK$FBGjUmf2htz))WN4^O+ zeG_`>Ccs9Qd68X=_wN(T-~jPCU-Wi8ee%`AK90;!BQu`{ncR)cYymsjg4e^yMpKz{ zWGeG>m0mntBg0R5j`Ygg--|C+LK7*BL|8#;rmYmn>_I9fgOeD=NEWycj^HZiNHxD3 zst}-ENsF+m;FgKNRlv3c6AJ5xLtt8ap($XII$<|hO-B1JTQwCPHGh-bF$1`DqMrwa(dZp5m0u$lvnd?_fODD10m&{6I5j?QN zrAFOCFfUc`6sQQPk%AGS9Mbo~zNS%zDQmHtQV^;}2P9OLVAcurI}k8h8AUOT;Co*q zXAT$w8<}x3QVo=1IpS4{1;B}|JL4=itYtRn5J)ih zW^f=H0aSyOn#?<^(G743Y#AcO8xqR>$r}jTYcxg;lfZfiK?}%0L0QBhU`HCrO(d*) zbGLBJ1KT1O38!$u8VsvpApl#VJ5KRwtbSdOk zPrkKD>#MAN*pFuBud{U7rX{F2t*%$i9W|LC9Dn?brv+zSv&`0aUpwo+BO#pz>n(Rq zL45FWo?iS?^#ylwSybvNVx53 z&!~Dt?J1?=ddr@2)}G*m(m>1vqZ9;=_4omhNE{s?en~PsKr}l=4URWUm z2umx34mfBeB=-azbrU#fXVtu=1M|THQw9};4Khd^G*VdY37Y0A0&1@DygddD*^(<~ z3|9(rHFwtFsF6BrggTy`A7*O7#}SXBmK=(1rGvK4C}(lhNXa!q9k-K**qUo8=BZ5! z=oD<=HW=rXG!6%i)O&k^b~G;GL5T49p@bz+2)D+F5CQFnV|Z z5oM_f68C}NEY4|YHWzAlbdPO^I3Ba zHH}k*pcJW4jC%`Jm6MQUm9%=wx#!bn1Z(UhoR)$7xJAQiYtDcNod@Oy}?FjdfvycLYa=QL8^RJ z=gaI_la)>0=5Mp4sM3p3%3L=)e*2G)es?fJ zR3m`y-^DlUyeuL4k8wR;RbAO`{X78qv0YWSmuXSVPlj(Xo_sj@8^Kg->4Gr;03@82 ACjbBd literal 0 HcmV?d00001 diff --git a/test/instance/migrate_test.jl b/test/instance/migrate_test.jl index 2ea6d46..33889a1 100644 --- a/test/instance/migrate_test.jl +++ b/test/instance/migrate_test.jl @@ -16,3 +16,11 @@ end @test length(instance.buses) == 14 @test length(instance.lines) == 20 end + +@testset "read v0.4" begin + instance = UnitCommitment.read("$FIXTURES/ucjl-0.4.json.gz") + @test length(instance.units) == 6 + @test length(instance.buses) == 14 + @test length(instance.lines) == 20 + @test length(instance.profiled_units) == 2 +end diff --git a/test/instance/read_test.jl b/test/instance/read_test.jl index cc9e332..0ef0cc7 100644 --- a/test/instance/read_test.jl +++ b/test/instance/read_test.jl @@ -124,3 +124,117 @@ end @test unit.startup_categories[3].delay == 6 @test unit.initial_status == -200 end + +@testset "read_benchmark profiled-units" begin + instance = UnitCommitment.read("$FIXTURES/case14-profiled.json.gz") + @test length(instance.lines) == 20 + @test length(instance.buses) == 14 + @test length(instance.units) == 6 + @test length(instance.profiled_units) == 2 + @test length(instance.contingencies) == 19 + @test length(instance.price_sensitive_loads) == 1 + @test instance.time == 4 + + @test instance.lines[5].name == "l5" + @test instance.lines[5].source.name == "b2" + @test instance.lines[5].target.name == "b5" + @test instance.lines[5].reactance ≈ 0.17388 + @test instance.lines[5].susceptance ≈ 10.037550333 + @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].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].source.name == "b1" + @test instance.lines[1].target.name == "b2" + @test instance.lines[1].reactance ≈ 0.059170 + @test instance.lines[1].susceptance ≈ 29.496860773945 + @test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4] + @test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4] + @test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4] + + @test instance.buses[9].name == "b9" + @test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] + @test instance.buses_by_name["b9"].name == "b9" + + @test instance.reserves[1].name == "r1" + @test instance.reserves[1].type == "spinning" + @test instance.reserves[1].amount == [100.0, 100.0, 100.0, 100.0] + @test instance.reserves_by_name["r1"].name == "r1" + + unit = instance.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 instance.units_by_name["g1"].name == "g1" + + unit = instance.units[2] + @test unit.name == "g2" + @test unit.must_run == [false for t in 1:4] + @test length(unit.reserves) == 1 + + unit = instance.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 instance.contingencies[1].lines == [instance.lines[1]] + @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] + @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 instance.price_sensitive_loads_by_name["ps1"].name == "ps1" + + profiled_unit = instance.profiled_units[2] + @test profiled_unit.name == "g8" + @test profiled_unit.bus.name == "b5" + @test profiled_unit.cost == [50.0 for t in 1:4] + @test profiled_unit.capacity == [120.0 for t in 1:4] + @test instance.profiled_units_by_name["g8"].name == "g8" +end \ No newline at end of file