From 66c369e36f5007eb5437413c1868caa5e0f6f22b Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Mon, 20 Apr 2026 14:41:15 -0600 Subject: [PATCH] increase parser coverage --- test/Artifacts.toml | 14 +- test/Project.toml | 10 +- test/common.jl | 18 -- test/loading_utils.jl | 34 +++ test/rts_loading_utils.jl | 3 - test/runtests.jl | 17 +- test/test_common.jl | 31 +++ test/test_constructor_edges.jl | 120 +++++++++ test/test_enums.jl | 12 + test/test_power_system_table_data.jl | 370 ++------------------------- 10 files changed, 237 insertions(+), 392 deletions(-) delete mode 100644 test/common.jl create mode 100644 test/loading_utils.jl delete mode 100644 test/rts_loading_utils.jl create mode 100644 test/test_common.jl create mode 100644 test/test_constructor_edges.jl create mode 100644 test/test_enums.jl diff --git a/test/Artifacts.toml b/test/Artifacts.toml index e6a60ae..b5196f3 100644 --- a/test/Artifacts.toml +++ b/test/Artifacts.toml @@ -1,7 +1,15 @@ +[CaseData] +git-tree-sha1 = "edcb5940e84a802a86ad4f2223214d33121ac044" +lazy = true + + [[CaseData.download]] + url = "https://github.com/NREL-Sienna/PowerSystemsTestData/archive/refs/tags/4.0.2.tar.gz" + sha256 = "c844aba2ce37c1cc1bb4c496c809607f4ddb310cb76a5fdc266bf6a1345f2b51" + [rts] -git-tree-sha1 = "5098f357bad765bfefcff58f080818863ca776bd" +git-tree-sha1 = "c257b2c37b981f261fdc328b0fb9b96436a96537" lazy = true [[rts.download]] - url = "https://github.com/GridMod/RTS-GMLC/archive/refs/tags/v0.2.2.tar.gz" - sha256 = "f7a816f2390b96d44fa931c2790e2ec5ef81d0deb503c4c719b25ec1b585e2c2" + url = "https://github.com/GridMod/RTS-GMLC/archive/refs/tags/v0.2.3.tar.gz" + sha256 = "993429b8dc5f096e62479ca3b8551d52a53941322b452c825d12881cda915eae" diff --git a/test/Project.toml b/test/Project.toml index 0667f01..ac20f9b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,16 +1,12 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" LazyArtifacts = "4af54fe1-eca0-43a8-85a7-787d91b784e3" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" -PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e" -PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" PowerTableDataParser = "2b750c0e-0bff-11f1-9200-1befd75df6be" -SQLite = "0aa819cd-b072-5ff4-a722-6bc24af294d9" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] -julia = "^1.6" +julia = "^1.10" diff --git a/test/common.jl b/test/common.jl deleted file mode 100644 index 2bc8e24..0000000 --- a/test/common.jl +++ /dev/null @@ -1,18 +0,0 @@ -# copied from PowerSystems.jl/test/common.jl - -"""Return the Branch in the system that matches another by case-insensitive arc -names.""" -function get_branch(sys::System, other::Branch) - for branch in get_components(Branch, sys) - if lowercase(other.arc.from.name) == lowercase(branch.arc.from.name) && - lowercase(other.arc.to.name) == lowercase(branch.arc.to.name) - return branch - end - end - - error("Did not find branch with buses $(other.arc.from.name) ", "$(other.arc.to.name)") -end -function create_rts_system(time_series_resolution = Dates.Hour(1)) - data = PSY.PowerSystemTableData(RTS_GMLC_DIR, 100.0, DESCRIPTORS) - return System(data; time_series_resolution = time_series_resolution) -end diff --git a/test/loading_utils.jl b/test/loading_utils.jl new file mode 100644 index 0000000..78cd8f7 --- /dev/null +++ b/test/loading_utils.jl @@ -0,0 +1,34 @@ +import LazyArtifacts + +const RTS_SRC = joinpath( + LazyArtifacts.artifact"rts", + "RTS-GMLC-0.2.3", + "RTS_Data", + "SourceData", +) +const RTS_DESCRIPTORS = joinpath(@__DIR__, "data", "rts", "user_descriptors.yaml") + +const BUS118_SRC = joinpath( + LazyArtifacts.artifact"CaseData", + "PowerSystemsTestData-4.0.2", + "118-Bus", +) +const BUS118_DESCRIPTORS = joinpath(@__DIR__, "data", "118_bus", "user_descriptors.yaml") +const BUS118_GEN_MAPPING = joinpath(@__DIR__, "data", "118_bus", "generator_mapping.yaml") + +load_rts_data() = PDP.PowerSystemTableData(RTS_SRC, 100.0, RTS_DESCRIPTORS) + +# 118-Bus CSVs ship with PLEXOS-style filenames (Buses.csv / Lines.csv) but the +# parser keys off lowercase stems (bus / branch / gen). Stage them in a tmpdir. +function load_118_bus_data() + tmpdir = mktempdir() + cp(joinpath(BUS118_SRC, "Buses.csv"), joinpath(tmpdir, "bus.csv")) + cp(joinpath(BUS118_SRC, "Lines.csv"), joinpath(tmpdir, "branch.csv")) + cp(joinpath(BUS118_SRC, "gen.csv"), joinpath(tmpdir, "gen.csv")) + return PDP.PowerSystemTableData( + tmpdir, + 100.0, + BUS118_DESCRIPTORS; + generator_mapping_file = BUS118_GEN_MAPPING, + ) +end diff --git a/test/rts_loading_utils.jl b/test/rts_loading_utils.jl deleted file mode 100644 index 30d4892..0000000 --- a/test/rts_loading_utils.jl +++ /dev/null @@ -1,3 +0,0 @@ -import LazyArtifacts - -const RTS_DIR = joinpath(LazyArtifacts.artifact"rts", "RTS-GMLC-0.2.2") diff --git a/test/runtests.jl b/test/runtests.jl index baa436a..de52ba4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,17 +1,12 @@ using Test using Logging -using DataStructures -using Dates -using LinearAlgebra -using PowerSystems -using PowerSystemCaseBuilder using PowerTableDataParser import InfrastructureSystems +import DataFrames +import YAML const IS = InfrastructureSystems const PDP = PowerTableDataParser -const PSB = PowerSystemCaseBuilder -const PSY = PowerSystems import Aqua Aqua.test_unbound_args(PowerTableDataParser) @@ -20,13 +15,7 @@ Aqua.test_ambiguities(PowerTableDataParser) Aqua.test_stale_deps(PowerTableDataParser) Aqua.test_deps_compat(PowerTableDataParser) -const DATA_DIR = PSB.DATA_DIR -const BAD_DATA = joinpath(DATA_DIR, "bad_data_for_tests") -const RTS_GMLC_DIR = joinpath(DATA_DIR, "RTS_GMLC") -const DESCRIPTORS = joinpath(RTS_GMLC_DIR, "user_descriptors.yaml") - -include("common.jl") -include("rts_loading_utils.jl") +include("loading_utils.jl") LOG_FILE = "table-parser-test.log" LOG_LEVELS = Dict( diff --git a/test/test_common.jl b/test/test_common.jl new file mode 100644 index 0000000..a504418 --- /dev/null +++ b/test/test_common.jl @@ -0,0 +1,31 @@ +@testset "get_generator_mapping" begin + # happy path — package default + mappings = PDP.get_generator_mapping(PDP.GENERATOR_MAPPING_FILE_CDM) + @test mappings isa Dict{NamedTuple, String} + @test !isempty(mappings) + + # GenericBattery rename → EnergyReservoirStorage with a warning + tmp = tempname() * ".yaml" + write( + tmp, + """ + GenericBattery: + - {fuel: Battery, type: BA} + """, + ) + renamed = @test_logs (:warn,) PDP.get_generator_mapping(tmp) + @test renamed[(fuel = "Battery", unit_type = "BA")] == "EnergyReservoirStorage" + + # duplicate (fuel, type) across two generator entries → error + dup = tempname() * ".yaml" + write( + dup, + """ + ThermalStandard: + - {fuel: NG, type: ST} + HydroDispatch: + - {fuel: NG, type: ST} + """, + ) + @test_throws ErrorException PDP.get_generator_mapping(dup) +end diff --git a/test/test_constructor_edges.jl b/test/test_constructor_edges.jl new file mode 100644 index 0000000..a14c64a --- /dev/null +++ b/test/test_constructor_edges.jl @@ -0,0 +1,120 @@ +@testset "Directory constructor edge cases" begin + # Empty directory + empty_dir = mktempdir() + @test_throws ErrorException PDP.PowerSystemTableData( + empty_dir, + 100.0, + BUS118_DESCRIPTORS, + ) + + # Directory with only non-CSV / non-folder files + no_csv = mktempdir() + write(joinpath(no_csv, "README.md"), "nothing here") + @test_throws ErrorException PDP.PowerSystemTableData( + no_csv, + 100.0, + BUS118_DESCRIPTORS, + ) + + # Bad generator mapping path → rethrown (parser also @errors, expected) + staging = mktempdir() + cp(joinpath(BUS118_SRC, "Buses.csv"), joinpath(staging, "bus.csv")) + cp(joinpath(BUS118_SRC, "Lines.csv"), joinpath(staging, "branch.csv")) + cp(joinpath(BUS118_SRC, "gen.csv"), joinpath(staging, "gen.csv")) + @test_logs (:error,) match_mode = :any begin + @test_throws Exception PDP.PowerSystemTableData( + staging, + 100.0, + BUS118_DESCRIPTORS; + generator_mapping_file = joinpath(staging, "does_not_exist.yaml"), + ) + end +end + +@testset "Dict constructor: missing keys" begin + descriptors = Dict{Symbol, Vector}() + user_descriptors = Dict{Symbol, Vector}() + gen_mapping = Dict{NamedTuple, String}() + + # Missing 'bus' key + @test_throws IS.DataFormatError PDP.PowerSystemTableData( + Dict{String, Any}(), + mktempdir(), + user_descriptors, + descriptors, + gen_mapping, + ) + + # Missing 'base_power' → warns and falls back to DEFAULT_BASE_MVA + bus_df = DataFrames.DataFrame(; bus_id = [1]) + data = Dict{String, Any}("bus" => bus_df) + sys_data = @test_logs (:warn,) match_mode = :any PDP.PowerSystemTableData( + data, + mktempdir(), + user_descriptors, + descriptors, + gen_mapping, + ) + @test sys_data.base_power == PDP.DEFAULT_BASE_MVA + @test haskey(sys_data.category_to_df, :BUS) +end + +@testset "Timeseries metadata fallback" begin + descriptors = Dict{Symbol, Vector}() + user_descriptors = Dict{Symbol, Vector}() + gen_mapping = Dict{NamedTuple, String}() + bus_df = DataFrames.DataFrame(; bus_id = [1]) + data() = Dict{String, Any}("bus" => bus_df, "base_power" => 100.0) + + # .json present + d_json = mktempdir() + write(joinpath(d_json, "timeseries_pointers.json"), "{}") + sd_json = PDP.PowerSystemTableData( + data(), + d_json, + user_descriptors, + descriptors, + gen_mapping, + ) + @test endswith(sd_json.timeseries_metadata_file, ".json") + + # .csv present (no .json) + d_csv = mktempdir() + write(joinpath(d_csv, "timeseries_pointers.csv"), "") + sd_csv = PDP.PowerSystemTableData( + data(), + d_csv, + user_descriptors, + descriptors, + gen_mapping, + ) + @test endswith(sd_csv.timeseries_metadata_file, ".csv") + + # Neither present + d_none = mktempdir() + sd_none = PDP.PowerSystemTableData( + data(), + d_none, + user_descriptors, + descriptors, + gen_mapping, + ) + @test sd_none.timeseries_metadata_file === nothing +end + +@testset "_read_config_file: reserves rename + Symbol uppercasing" begin + tmp = tempname() * ".yaml" + write( + tmp, + """ + bus: + - {custom_name: Number, name: bus_id} + reserves: + - {custom_name: R, name: name} + """, + ) + cfg = PDP._read_config_file(tmp) + @test haskey(cfg, :BUS) + @test haskey(cfg, :RESERVE) + @test !haskey(cfg, :RESERVES) +end diff --git a/test/test_enums.jl b/test/test_enums.jl new file mode 100644 index 0000000..f91d48f --- /dev/null +++ b/test/test_enums.jl @@ -0,0 +1,12 @@ +@testset "get_enum_value" begin + @test PDP.get_enum_value(PDP.InputCategory, "bus") == PDP.InputCategory.BUS + # case-insensitive + @test PDP.get_enum_value(PDP.InputCategory, "BUS") == PDP.InputCategory.BUS + @test PDP.get_enum_value(PDP.InputCategory, "Branch") == PDP.InputCategory.BRANCH + + # invalid enum type — anything not in PDP.ENUM_MAPPINGS + @test_throws ArgumentError PDP.get_enum_value(Int, "bus") + + # invalid value within a known enum + @test_throws ArgumentError PDP.get_enum_value(PDP.InputCategory, "not_a_category") +end diff --git a/test/test_power_system_table_data.jl b/test/test_power_system_table_data.jl index 6e612f9..b7f7fd9 100644 --- a/test/test_power_system_table_data.jl +++ b/test/test_power_system_table_data.jl @@ -1,350 +1,26 @@ -import PowerSystems: LazyDictFromIterator - -@testset "PowerSystemTableData parsing" begin - resolutions = ( - (resolution = Dates.Minute(5), len = 288), - (resolution = Dates.Minute(60), len = 24), - ) - - for (resolution, len) in resolutions - sys = create_rts_system(resolution) - for time_series in get_time_series_multiple(sys) - @test length(time_series) == len - end - end -end - -@testset "PowerSystemTableData parsing invalid directory" begin - @test_throws ErrorException PDP.PowerSystemTableData(DATA_DIR, 100.0, DESCRIPTORS) -end - -@testset "Consistency between PowerSystemTableData and standardfiles" begin - # This signature is used to capture expected error logs from parsing matpower - consistency_test = - () -> begin - mpsys = PSY.System(joinpath(BAD_DATA, "RTS_GMLC_original.m")) - cdmsys = PSB.build_system( - PSB.PSITestSystems, - "test_RTS_GMLC_sys"; - force_build = true, - ) - mp_iter = get_components(HydroGen, mpsys) - mp_generators = LazyDictFromIterator(String, HydroGen, mp_iter, get_name) - for cdmgen in get_components(HydroGen, cdmsys) - mpgen = get(mp_generators, uppercase(get_name(cdmgen))) - if isnothing(mpgen) - error("did not find $cdmgen") - end - @test cdmgen.available == mpgen.available - @test lowercase(cdmgen.bus.name) == lowercase(mpgen.bus.name) - gen_dat = ( - structname = nothing, - fields = ( - :active_power, - :reactive_power, - :rating, - :active_power_limits, - :reactive_power_limits, - :ramp_limits, - ), - ) - function check_fields(chk_dat) - for field in chk_dat.fields - n = get(chk_dat, :structname, nothing) - (cdmd, mpd) = - if isnothing(n) - (cdmgen, mpgen) - else - (getfield(cdmgen, n), getfield(mpgen, n)) - end - cdmgen_val = getfield(cdmd, field) - mpgen_val = getfield(mpd, field) - if isnothing(cdmgen_val) || isnothing(mpgen_val) - @warn "Skip value with nothing" repr(cdmgen_val) repr(mpgen_val) - continue - end - @test cdmgen_val == mpgen_val - end - end - check_fields(gen_dat) - end - - mp_iter = get_components(ThermalGen, mpsys) - mp_generators = LazyDictFromIterator(String, ThermalGen, mp_iter, get_name) - for cdmgen in get_components(ThermalGen, cdmsys) - if isnothing(cdmgen) - # Skips generators parsed from Matpower as SynchCondensers in PSY5 - # The fields are different so those aren't valiated in this loop - continue - end - mpgen = get(mp_generators, uppercase(get_name(cdmgen))) - @test cdmgen.available == mpgen.available - @test lowercase(cdmgen.bus.name) == lowercase(mpgen.bus.name) - for field in (:active_power_limits, :reactive_power_limits, :ramp_limits) - cdmgen_val = getfield(cdmgen, field) - mpgen_val = getfield(mpgen, field) - if isnothing(cdmgen_val) || isnothing(mpgen_val) - @warn "Skip value with nothing" repr(cdmgen_val) repr(mpgen_val) - continue - end - @test cdmgen_val == mpgen_val - end - - mpgen_cost = get_operation_cost(mpgen) - # Currently true; this is likely to change in the future and then we'd have to change the test - @assert get_variable(mpgen_cost) isa - CostCurve{InputOutputCurve{PiecewiseLinearData}} - mp_points = get_points( - get_function_data(get_value_curve( - get_variable(mpgen_cost))), - ) - if length(mp_points) == 4 - cdm_op_cost = get_operation_cost(cdmgen) - @test get_fixed(cdm_op_cost) == 0.0 - fuel_curve = get_variable(cdm_op_cost) - fuel_cost = get_fuel_cost(fuel_curve) - mp_fixed = get_fixed(mpgen_cost) - io_curve = InputOutputCurve(get_value_curve(fuel_curve)) - cdm_points = get_points(io_curve) - @test all( - isapprox.( - [p.y * fuel_cost for p in cdm_points], - [p.y + mp_fixed for p in mp_points], - atol = 0.1), - ) - @test all( - isapprox.( - [p.x for p in cdm_points], - [p.x * get_base_power(mpgen) for p in mp_points], - atol = 0.1), - ) - end - end - - mp_iter = get_components(RenewableGen, mpsys) - mp_generators = - LazyDictFromIterator(String, RenewableGen, mp_iter, get_name) - for cdmgen in get_components(RenewableGen, cdmsys) - mpgen = get(mp_generators, uppercase(get_name(cdmgen))) - # Disabled since data is inconsisten between sources - #@test cdmgen.available == mpgen.available - @test lowercase(cdmgen.bus.name) == lowercase(mpgen.bus.name) - for field in (:rating, :power_factor) - cdmgen_val = getfield(cdmgen, field) - mpgen_val = getfield(mpgen, field) - if isnothing(cdmgen_val) || isnothing(mpgen_val) - @warn "Skip value with nothing" repr(cdmgen_val) repr(mpgen_val) - continue - end - @test cdmgen_val == mpgen_val - end - #@test compare_values_without_uuids(cdmgen.operation_cost, mpgen.operation_cost) - end - - cdm_ac_branches = collect(get_components(ACBranch, cdmsys)) - @test get_rating(cdm_ac_branches[2]) == - get_rating(get_branch(mpsys, cdm_ac_branches[2])) - @test get_rating(cdm_ac_branches[6]) == - get_rating(get_branch(mpsys, cdm_ac_branches[6])) - @test get_rating(cdm_ac_branches[120]) == - get_rating(get_branch(mpsys, cdm_ac_branches[120])) - - cdm_dc_branches = - collect(get_components(TwoTerminalGenericHVDCLine, cdmsys)) - @test get_active_power_limits_from(cdm_dc_branches[1]) == - get_active_power_limits_from(get_branch(mpsys, cdm_dc_branches[1])) - end - @test_logs (:error,) match_mode = :any min_level = Logging.Error consistency_test() -end - -@testset "Test reserve direction" begin - @test PSY.get_reserve_direction("Up") == ReserveUp - @test PSY.get_reserve_direction("Down") == ReserveDown - @test PSY.get_reserve_direction("up") == ReserveUp - @test PSY.get_reserve_direction("down") == ReserveDown - - for invalid in ("right", "left") - @test_throws PSY.DataFormatError PSY.get_reserve_direction(invalid) - end -end - -@testset "Test consistency between variable cost and heat rate parsing" begin - fivebus_dir = joinpath(DATA_DIR, "5-Bus") - rawsys_hr = PSY.PowerSystemTableData( - fivebus_dir, - 100.0, - joinpath(fivebus_dir, "user_descriptors_var_cost.yaml"); - generator_mapping_file = joinpath(fivebus_dir, "generator_mapping.yaml"), - ) - rawsys = PSY.PowerSystemTableData( - fivebus_dir, - 100.0, - joinpath(fivebus_dir, "user_descriptors_var_cost.yaml"); - generator_mapping_file = joinpath(fivebus_dir, "generator_mapping.yaml"), - ) - sys_hr = PSY.System(rawsys_hr) - sys = PSY.System(rawsys) - - g_hr = get_components(ThermalStandard, sys_hr) - g = get_components(ThermalStandard, sys) - @test get_variable.(get_operation_cost.(g)) == get_variable.(get_operation_cost.(g)) -end -@testset "Test create_poly_cost function" begin - cost_colnames = ["heat_rate_a0", "heat_rate_a1", "heat_rate_a2"] - - # Coefficients for a CC using natural gas - a2 = -0.000531607 - a1 = 0.060554675 - a0 = 8.951100118 - - # First test that return quadratic if all coefficients are provided. - # We convert the coefficients to string to mimic parsing from csv - example_generator = ( - name = "test-gen", - heat_rate_a0 = string(a0), - heat_rate_a1 = string(a1), - heat_rate_a2 = string(a2), - ) - cost_curve, fixed_cost = PSY.create_poly_cost(example_generator, cost_colnames) - @assert cost_curve isa QuadraticCurve - @assert isapprox(get_quadratic_term(cost_curve), a2, atol = 0.01) - @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) - @assert isapprox(get_constant_term(cost_curve), a0, atol = 0.01) - - # Test return linear with both proportional and constant term - example_generator = ( - name = "test-gen", - heat_rate_a0 = string(a0), - heat_rate_a1 = string(a1), - heat_rate_a2 = nothing, - ) - cost_curve, fixed_cost = PSY.create_poly_cost(example_generator, cost_colnames) - @assert cost_curve isa LinearCurve - @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) - @assert isapprox(get_constant_term(cost_curve), a0, atol = 0.01) - - # Test return linear with just proportional term - example_generator = ( - name = "test-gen", - heat_rate_a0 = nothing, - heat_rate_a1 = string(a1), - heat_rate_a2 = nothing, - ) - cost_curve, fixed_cost = PSY.create_poly_cost(example_generator, cost_colnames) - @assert cost_curve isa LinearCurve - @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) - - # Test raises error if a2 is passed but other coefficients are nothing - example_generator = ( - name = "test-gen", - heat_rate_a0 = nothing, - heat_rate_a1 = nothing, - heat_rate_a2 = string(a2), - ) - @test_throws IS.DataFormatError PSY.create_poly_cost(example_generator, cost_colnames) - example_generator = ( - name = "test-gen", - heat_rate_a0 = nothing, - heat_rate_a1 = string(a1), - heat_rate_a2 = string(a2), - ) - @test_throws IS.DataFormatError PSY.create_poly_cost(example_generator, cost_colnames) - example_generator = ( - name = "test-gen", - heat_rate_a0 = string(a0), - heat_rate_a1 = nothing, - heat_rate_a2 = string(a2), - ) - @test_throws IS.DataFormatError PSY.create_poly_cost(example_generator, cost_colnames) - - # Test that it works with zero proportional and constant term - example_generator = ( - name = "test-gen", - heat_rate_a0 = string(0.0), - heat_rate_a1 = string(0.0), - heat_rate_a2 = string(a2), - ) - cost_curve, fixed_cost = PSY.create_poly_cost(example_generator, cost_colnames) - @assert cost_curve isa QuadraticCurve - @assert isapprox(get_quadratic_term(cost_curve), a2, atol = 0.01) - @assert isapprox(get_proportional_term(cost_curve), 0.0, atol = 0.01) - @assert isapprox(get_constant_term(cost_curve), 0.0, atol = 0.01) - - # Test that create_poly_cost works with numeric values (not just strings) - # Some CSV parsers return numeric types directly instead of strings - example_generator = ( - name = "test-gen", - heat_rate_a0 = a0, # Float64 - heat_rate_a1 = a1, # Float64 - heat_rate_a2 = a2, # Float64 - ) - cost_curve, fixed_cost = PSY.create_poly_cost(example_generator, cost_colnames) - @assert cost_curve isa QuadraticCurve - @assert isapprox(get_quadratic_term(cost_curve), a2, atol = 0.01) - @assert isapprox(get_proportional_term(cost_curve), a1, atol = 0.01) - @assert isapprox(get_constant_term(cost_curve), a0, atol = 0.01) - - # Test with Int64 values (another common numeric type from CSV parsers) - example_generator = ( - name = "test-gen", - heat_rate_a0 = Int64(9), - heat_rate_a1 = Int64(0), - heat_rate_a2 = Int64(0), - ) - cost_curve, fixed_cost = PSY.create_poly_cost(example_generator, cost_colnames) - @assert cost_curve isa QuadraticCurve - @assert isapprox(get_quadratic_term(cost_curve), 0.0, atol = 0.01) - @assert isapprox(get_proportional_term(cost_curve), 0.0, atol = 0.01) - @assert isapprox(get_constant_term(cost_curve), 9.0, atol = 0.01) -end - -@testset "Test parsing with ThermalMultiStart generators" begin - # Test that ThermalMultiStart generators parse correctly with multi-start costs - # This exercises the multi-start cost fallback logic in make_thermal_generator_multistart - rawsys = PSY.PowerSystemTableData( - RTS_GMLC_DIR, - 100.0, - DESCRIPTORS; - generator_mapping_file = joinpath( - RTS_GMLC_DIR, - "generator_mapping_multi_start.yaml", - ), - ) - sys = PSY.System(rawsys; time_series_resolution = Dates.Hour(1)) - - # Verify ThermalMultiStart generators were created - ms_gens = collect(get_components(ThermalMultiStart, sys)) - @test length(ms_gens) > 0 - - # Check that startup costs were parsed correctly - for gen in ms_gens - op_cost = get_operation_cost(gen) - startup_costs = get_start_up(op_cost) - # Startup costs should be non-negative - @test startup_costs.hot >= 0.0 - @test startup_costs.warm >= 0.0 - @test startup_costs.cold >= 0.0 +@testset "PowerSystemTableData from RTS-GMLC" begin + sys_data = load_rts_data() + @test sys_data isa PDP.PowerSystemTableData + @test sys_data.base_power == 100.0 + for key in (:BUS, :BRANCH, :GENERATOR, :RESERVE, :DC_BRANCH, :STORAGE) + @test haskey(sys_data.category_to_df, key) end -end -@testset "Test Reservoirs and Turbines" begin - cdmsys = PSB.build_system( - PSB.PSITestSystems, - "test_RTS_GMLC_sys"; - force_build = true, - ) - @test !isempty(get_components(HydroTurbine, cdmsys)) - for turbine in get_components(HydroTurbine, cdmsys) - reservoir = get_connected_head_reservoirs(cdmsys, turbine) - @test !isempty(reservoir) - reservoir = get_connected_tail_reservoirs(cdmsys, turbine) - @test isempty(reservoir) - end - - @test !isempty(get_components(HydroReservoir, cdmsys)) - - for reservoir in get_components(HydroReservoir, cdmsys) - turbines = get_downstream_turbines(reservoir) - @test !isempty(turbines) - @test isempty(get_upstream_turbines(reservoir)) + @test sys_data.timeseries_metadata_file !== nothing + @test endswith(sys_data.timeseries_metadata_file, ".json") || + endswith(sys_data.timeseries_metadata_file, ".csv") + @test !isempty(sys_data.generator_mapping) + @test !isempty(sys_data.user_descriptors) + @test !isempty(sys_data.descriptors) +end + +@testset "PowerSystemTableData from 118-Bus" begin + sys_data = load_118_bus_data() + @test sys_data isa PDP.PowerSystemTableData + @test sys_data.base_power == 100.0 + for key in (:BUS, :BRANCH, :GENERATOR) + @test haskey(sys_data.category_to_df, key) end + @test DataFrames.nrow(sys_data.category_to_df[:BUS]) == 118 + @test DataFrames.nrow(sys_data.category_to_df[:BRANCH]) == 186 + @test sys_data.timeseries_metadata_file === nothing end