diff --git a/Project.toml b/Project.toml index e3e2d32..8a9606b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "PowerFlowFileParser" uuid = "bed98974-b02e-5e2f-9ee0-a103f5c450dd" -version = "0.1.0" +version = "0.2.0" authors = ["Sienna Team"] [deps] @@ -8,7 +8,6 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -PowerFlowData = "dd99e9e3-7471-40fc-b48d-a10501125371" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" @@ -17,7 +16,6 @@ DataStructures = "0.19.3" DocStringExtensions = "~0.8, ~0.9" InfrastructureSystems = "^3.2" LinearAlgebra = "1" -PowerFlowData = "1.6.0" Unicode = "1" YAML = "0.4.16" julia = "^1.10" diff --git a/docs/src/explanation/arch_design.md b/docs/src/explanation/arch_design.md index 8cba4f1..c8c9543 100644 --- a/docs/src/explanation/arch_design.md +++ b/docs/src/explanation/arch_design.md @@ -2,12 +2,9 @@ PowerFlowFileParser.jl follows a clear separation of concerns: -**Parse, Don't Transform**: The library's primary responsibility is converting text files into simple, well-structured intermediate representations (dictionaries or typed structs). Conversion to domain-specific types like PowerSystems.jl components is handled downstream. +**Parse, Don't Transform**: The library's primary responsibility is converting text files into simple, well-structured PowerModels dictionaries. Conversion to domain-specific types like PowerSystems.jl components is handled downstream. -**Two Parsing Pathways**: - - 1. **PowerModels-based**: Produces standardized dictionaries via `parse_file()` or `PowerModelsData` - 2. **PowerFlowData-based**: Produces typed structs via `PowerFlowDataNetwork` +**Parsing Pathway**: MATPOWER and PSS/E files are parsed into standardized PowerModels dictionaries via `parse_file()` or `PowerModelsData`. This architecture keeps the parser lightweight, testable, and reusable across multiple downstream packages. diff --git a/docs/src/explanation/data_structs.md b/docs/src/explanation/data_structs.md index 9f69053..0a25ff0 100644 --- a/docs/src/explanation/data_structs.md +++ b/docs/src/explanation/data_structs.md @@ -9,15 +9,6 @@ pm_data = PowerModelsData("case30.m") buses = pm_data.data["bus"] ``` -## PowerFlowDataNetwork - -Container wrapping PowerFlowData.Network typed structs. Access parsed components via `.data.buses`, `.data.generators`, etc. - -```julia -pfd_data = PowerFlowDataNetwork("network.raw") -buses = pfd_data.data.buses -``` - ## Main Functions ### `parse_file(file; kwargs...)` diff --git a/docs/src/tutorials/quickstart.md b/docs/src/tutorials/quickstart.md index f5c97de..d69d6a4 100644 --- a/docs/src/tutorials/quickstart.md +++ b/docs/src/tutorials/quickstart.md @@ -16,16 +16,6 @@ pm_data = PowerModelsData("case30.m") buses = pm_data.data["bus"] ``` -## Alternative Parser - -Use PowerFlowData.jl parser for PSS/E files: - -```julia -pfd_data = PowerFlowDataNetwork("network.raw") -buses = pfd_data.data.buses -generators = pfd_data.data.generators -``` - ## Advanced Options Control validation and data corrections: diff --git a/src/PowerFlowFileParser.jl b/src/PowerFlowFileParser.jl index 1999ba8..4c73eca 100644 --- a/src/PowerFlowFileParser.jl +++ b/src/PowerFlowFileParser.jl @@ -6,13 +6,11 @@ module PowerFlowFileParser # Exports export PowerModelsData -export PowerFlowDataNetwork export parse_file ################################################################################# # Imports -import PowerFlowData import LinearAlgebra import DataStructures: SortedDict import Unicode: normalize @@ -29,7 +27,6 @@ import InfrastructureSystems: # Includes include("definitions.jl") -include("powerflowdata_data.jl") include("power_models_data.jl") include("im_io.jl") include("pm_io.jl") diff --git a/src/pm_io/psse.jl b/src/pm_io/psse.jl index 8377273..4253721 100644 --- a/src/pm_io/psse.jl +++ b/src/pm_io/psse.jl @@ -171,8 +171,9 @@ function _psse2pm_branch!(pm_data::Dict, pti_data::Dict, import_all::Bool) end if first(branch["CKT"]) != '@' && first(branch["CKT"]) != '*' sub_data = Dict{String, Any}() - sub_data["f_bus"] = pop!(branch, "I") - sub_data["t_bus"] = pop!(branch, "J") + # a negative terminal bus number marks the metered end; the bus is its magnitude + sub_data["f_bus"] = abs(pop!(branch, "I")) + sub_data["t_bus"] = abs(pop!(branch, "J")) bus_from = pm_data["bus"][sub_data["f_bus"]] sub_data["base_voltage_from"] = bus_from["base_kv"] bus_to = pm_data["bus"][sub_data["t_bus"]] @@ -195,7 +196,7 @@ function _psse2pm_branch!(pm_data::Dict, pti_data::Dict, import_all::Bool) "LEN" => pop!(branch, "LEN"), ) - if pm_data["source_version"] ∈ ("32", "33") + if pm_data["source_version"] ∈ ("30", "32", "33") sub_data["rate_a"] = pop!(branch, "RATEA") sub_data["rate_b"] = pop!(branch, "RATEB") sub_data["rate_c"] = pop!(branch, "RATEC") @@ -257,6 +258,10 @@ function branch_isolated_bus_modifications!(pm_data::Dict, branch_data::Dict) bus_data = pm_data["bus"] from_bus_no = branch_data["f_bus"] to_bus_no = branch_data["t_bus"] + if !haskey(bus_data, from_bus_no) || !haskey(bus_data, to_bus_no) + @warn "Branch between buses $(from_bus_no) and $(to_bus_no) references a bus not present in the BUS data. Skipping isolated-bus check." + return + end from_bus = bus_data[from_bus_no] to_bus = bus_data[to_bus_no] @@ -421,7 +426,7 @@ function _psse2pm_generator!(pm_data::Dict, pti_data::Dict, import_all::Bool) "NREG" => pop!(gen, "NREG"), "BASLOD" => pop!(gen, "BASLOD"), ) - elseif pm_data["source_version"] ∈ ("32", "33") + elseif pm_data["source_version"] ∈ ("30", "32", "33") sub_data["ext"] = Dict{String, Any}( "IREG" => pop!(gen, "IREG"), "WPF" => pop!(gen, "WPF"), @@ -557,6 +562,22 @@ function _psse2pm_bus!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["source_id"] = ["bus", "$(bus["I"])"] sub_data["index"] = pop!(bus, "I") + gl = pop!(bus, "GL", 0.0) + bl = pop!(bus, "BL", 0.0) + if gl != 0.0 || bl != 0.0 + if !haskey(pm_data, "shunt") + pm_data["shunt"] = [] + end + shunt_data = Dict{String, Any}() + shunt_data["shunt_bus"] = sub_data["bus_i"] + shunt_data["gs"] = gl + shunt_data["bs"] = bl + shunt_data["status"] = sub_data["bus_status"] + shunt_data["source_id"] = ["bus", "$(sub_data["bus_i"])"] + shunt_data["index"] = length(pm_data["shunt"]) + 1 + push!(pm_data["shunt"], shunt_data) + end + if import_all _import_remaining_keys!(sub_data, bus) end @@ -597,7 +618,7 @@ function _psse2pm_load!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["interruptible"] = pop!(load, "INTRPT") sub_data["ext"] = Dict{String, Any}() - if pm_data["source_version"] ∈ ("32", "33") + if pm_data["source_version"] ∈ ("30", "32", "33") sub_data["ext"]["LOADTYPE"] = "" elseif pm_data["source_version"] == "35" sub_data["ext"]["LOADTYPE"] = pop!(load, "LOADTYPE", "") @@ -633,7 +654,10 @@ specifications. function _psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool) @info "Parsing PSS(R)E Fixed & Switched Shunt data into a PowerModels Dict..." - pm_data["shunt"] = [] + # bus records may have already contributed shunt entries + if !haskey(pm_data, "shunt") + pm_data["shunt"] = [] + end if haskey(pti_data, "FIXED SHUNT") for shunt in pti_data["FIXED SHUNT"] sub_data = Dict{String, Any}() @@ -718,7 +742,7 @@ function _psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["initial_status"][1:length(sub_data["step_number"])] sub_data["ext"]["NREG"] = pop!(switched_shunt, "NREG") - elseif pm_data["source_version"] ∈ ("32", "33") + elseif pm_data["source_version"] ∈ ("30", "32", "33") sub_data["ext"]["SWREM"] = switched_shunt["SWREM"] sub_data["initial_status"] = ones(Int, length(sub_data["y_increment"])) else @@ -977,7 +1001,7 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) "MAG2" => transformer["MAG2"], ) - if pm_data["source_version"] ∈ ("32", "33") + if pm_data["source_version"] ∈ ("30", "32", "33") sub_data["rate_a"] = pop!(transformer, "RATA1") sub_data["rate_b"] = pop!(transformer, "RATB1") sub_data["rate_c"] = pop!(transformer, "RATC1") @@ -1350,7 +1374,7 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["r_tertiary"] = Zr_t sub_data["x_tertiary"] = Zx_t - if pm_data["source_version"] ∈ ("32", "33") + if pm_data["source_version"] ∈ ("30", "32", "33") sub_data["rating_primary"] = min( transformer["RATA1"], @@ -1519,7 +1543,7 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) for prefix in TRANSFORMER3W_PARAMETER_NAMES for i in 1:length(WINDING_NAMES) key = "$prefix$i" - if pm_data["source_version"] ∈ ("32", "33") + if pm_data["source_version"] ∈ ("30", "32", "33") sub_data["ext"][key] = transformer[key] else continue @@ -1592,9 +1616,18 @@ function _psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["transfer_setpoint"] = dcline["SETVL"] - sub_data["name"] = strip(dcline["NAME"], ['"', '\'']) + sub_data["name"] = if pm_data["source_version"] == "30" + string(dcline["I"]) + else + strip(dcline["NAME"], ['"', '\'']) + end sub_data["f_bus"] = dcline["IPR"] sub_data["t_bus"] = dcline["IPI"] + if !haskey(pm_data["bus"], sub_data["f_bus"]) || + !haskey(pm_data["bus"], sub_data["t_bus"]) + @warn "Two-terminal DC line between buses $(sub_data["f_bus"]) and $(sub_data["t_bus"]) references a bus not present in the BUS data. Skipping." + continue + end if pm_data["has_isolated_type_buses"] push!(pm_data["connected_buses"], sub_data["f_bus"]) push!(pm_data["connected_buses"], sub_data["t_bus"]) @@ -1704,7 +1737,11 @@ function _psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["inverter_capacitor_reactance"] = dcline["XCAPI"] / ZbaseI sub_data["r"] = dcline["RDC"] / ZbaseR - if pm_data["source_version"] ∈ ("32", "33") + if pm_data["source_version"] == "30" + sub_data["ext"] = Dict{String, Any}( + "psse_name" => dcline["I"], + ) + elseif pm_data["source_version"] ∈ ("32", "33") sub_data["ext"] = Dict{String, Any}( "psse_name" => dcline["NAME"], ) @@ -1721,7 +1758,11 @@ function _psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) "two-terminal dc", sub_data["f_bus"], sub_data["t_bus"], - pop!(dcline, "NAME"), + if pm_data["source_version"] == "30" + pop!(dcline, "I") + else + pop!(dcline, "NAME") + end, ] sub_data["index"] = length(pm_data["dcline"]) + 1 @@ -1874,7 +1915,11 @@ function _psse2pm_facts!(pm_data::Dict, pti_data::Dict, import_all::Bool) ) sub_data = Dict{String, Any}() - sub_data["name"] = strip(facts["NAME"], ['"', '\'']) + sub_data["name"] = if pm_data["source_version"] == "30" + string(facts["N"]) + else + strip(facts["NAME"], ['"', '\'']) + end sub_data["control_mode"] = facts["MODE"] sub_data["bus"] = facts["I"] # Sending end bus number @@ -1901,7 +1946,7 @@ function _psse2pm_facts!(pm_data::Dict, pti_data::Dict, import_all::Bool) if pm_data["source_version"] == "35" sub_data["ext"]["NREG"] = facts["NREG"] sub_data["ext"]["MNAME"] = facts["MNAME"] - elseif pm_data["source_version"] ∈ ("32", "33") + elseif pm_data["source_version"] ∈ ("30", "32", "33") sub_data["ext"] = Dict{String, Any}( "J" => facts["J"], ) @@ -1980,6 +2025,7 @@ function _psse2pm_switch_breaker!(pm_data::Dict, pti_data::Dict, import_all::Boo @info "Parsing PSS(R)E Switches & Breakers data into a PowerModels Dict..." pm_data["breaker"] = [] pm_data["switch"] = [] + pm_data["other"] = [] mapping = Dict('@' => ("breaker", 1), '*' => ("switch", 0)) mapping_v35 = Dict(2 => "breaker", 3 => "switch") @@ -2025,7 +2071,7 @@ function _psse2pm_switch_breaker!(pm_data::Dict, pti_data::Dict, import_all::Boo ) if import_all - _import_remaining_keys!(sub_data, branch) + _import_remaining_keys!(sub_data, switching_device) end branch_isolated_bus_modifications!(pm_data, sub_data) diff --git a/src/pm_io/pti.jl b/src/pm_io/pti.jl index 81f5a49..757d34c 100644 --- a/src/pm_io/pti.jl +++ b/src/pm_io/pti.jl @@ -37,6 +37,33 @@ const _pti_sections_v35 = vcat( "SUBSTATION DATA", ) +""" +Section order for PSS(R)E v29/v30 raw files. v30 has no FIXED SHUNT section, and +places SWITCHED SHUNT immediately after VOLTAGE SOURCE CONVERTER rather than near +the end of the file as in v33/v35. +""" +const _pti_sections_v30 = [ + "CASE IDENTIFICATION", + "BUS", + "LOAD", + "GENERATOR", + "BRANCH", + "TRANSFORMER", + "AREA INTERCHANGE", + "TWO-TERMINAL DC", + "VOLTAGE SOURCE CONVERTER", + "SWITCHED SHUNT", + "IMPEDANCE CORRECTION", + "MULTI-TERMINAL DC", + "MULTI-SECTION LINE", + "ZONE", + "INTER-AREA TRANSFER", + "OWNER", + "FACTS CONTROL DEVICE", + "GNE DEVICE", + "INDUCTION MACHINE", +] + const _transaction_dtypes = [ ("IC", Int64), ("SBASE", Float64), @@ -129,6 +156,14 @@ const _bus_dtypes = [ const _bus_dtypes_v35 = _bus_dtypes +const _bus_dtypes_v30 = vcat( + _bus_dtypes[1:4], + [("GL", Float64), ("BL", Float64)], + _bus_dtypes[5:6], + _bus_dtypes[8:9], + [_bus_dtypes[7]], +) + const _load_dtypes = [ ("I", Int64), ("ID", String), @@ -156,6 +191,8 @@ const _load_dtypes_v35 = vcat( ], ) +const _load_dtypes_v30 = _load_dtypes[1:13] + const _fixed_shunt_dtypes = [ ("I", Int64), ("ID", String), @@ -205,6 +242,8 @@ const _generator_dtypes_v35 = vcat( _generator_dtypes[19:end], ) +const _generator_dtypes_v30 = _generator_dtypes[1:26] + const _branch_dtypes = [ ("I", Int64), ("J", Int64), @@ -252,6 +291,8 @@ const _branch_dtypes_v35 = vcat( _branch_dtypes[10:end], ) +const _branch_dtypes_v30 = vcat(_branch_dtypes[1:14], _branch_dtypes[16:end]) + const _switching_dtypes_v35 = [ ("I", Int64), ("J", Int64), @@ -302,6 +343,8 @@ const _transformer_dtypes = [ const _transformer_dtypes_v35 = _transformer_dtypes +const _transformer_dtypes_v30 = _transformer_dtypes[1:20] + const _transformer_3_1_dtypes = [ ("R1-2", Float64), ("X1-2", Float64), @@ -318,6 +361,8 @@ const _transformer_3_1_dtypes = [ const _transformer_3_1_dtypes_v35 = _transformer_3_1_dtypes +const _transformer_3_1_dtypes_v30 = _transformer_3_1_dtypes + const _transformer_3_2_dtypes = [ ("WINDV1", Float64), ("NOMV1", Float64), @@ -359,6 +404,8 @@ const _transformer_3_2_dtypes_v35 = vcat( _transformer_3_2_dtypes[9:end], ) +const _transformer_3_2_dtypes_v30 = _transformer_3_2_dtypes[1:16] + const _transformer_3_3_dtypes = [ ("WINDV2", Float64), ("NOMV2", Float64), @@ -400,6 +447,8 @@ const _transformer_3_3_dtypes_v35 = vcat( _transformer_3_3_dtypes[9:end], ) +const _transformer_3_3_dtypes_v30 = _transformer_3_3_dtypes[1:16] + const _transformer_3_4_dtypes = [ ("WINDV3", Float64), ("NOMV3", Float64), @@ -441,11 +490,15 @@ const _transformer_3_4_dtypes_v35 = vcat( _transformer_3_4_dtypes[9:end], ) +const _transformer_3_4_dtypes_v30 = _transformer_3_4_dtypes[1:16] + const _transformer_2_1_dtypes = [("R1-2", Float64), ("X1-2", Float64), ("SBASE1-2", Float64)] const _transformer_2_1_dtypes_v35 = _transformer_2_1_dtypes +const _transformer_2_1_dtypes_v30 = _transformer_2_1_dtypes + const _transformer_2_2_dtypes = [ ("WINDV1", Float64), ("NOMV1", Float64), @@ -487,10 +540,14 @@ const _transformer_2_2_dtypes_v35 = vcat( _transformer_2_2_dtypes[9:end], ) +const _transformer_2_2_dtypes_v30 = _transformer_2_2_dtypes[1:16] + const _transformer_2_3_dtypes = [("WINDV2", Float64), ("NOMV2", Float64)] const _transformer_2_3_dtypes_v35 = _transformer_2_3_dtypes +const _transformer_2_3_dtypes_v30 = _transformer_2_3_dtypes + const _area_interchange_dtypes = [("I", Int64), ("ISW", Int64), ("PDES", Float64), ("PTOL", Float64), ("ARNAME", String)] @@ -553,6 +610,8 @@ const _two_terminal_line_dtypes_v35 = vcat( _two_terminal_line_dtypes[43:end], ) +const _two_terminal_line_dtypes_v30 = vcat([("I", Int64)], _two_terminal_line_dtypes[2:end]) + const _vsc_line_dtypes = [ ("NAME", String), ("MDC", Int64), @@ -644,6 +703,8 @@ const _multi_term_main_dtypes = [ const _multi_term_main_dtypes_v35 = _multi_term_main_dtypes +const _multi_term_main_dtypes_v30 = vcat([("I", Int64)], _multi_term_main_dtypes[2:end]) + const _multi_term_nconv_dtypes = [ ("IB", Int64), ("N", Int64), @@ -689,6 +750,9 @@ const _multi_term_ndcln_dtypes = [ const _multi_term_ndcln_dtypes_v35 = _multi_term_ndcln_dtypes +const _multi_term_ndcln_dtypes_v30 = + vcat(_multi_term_ndcln_dtypes[1:3], _multi_term_ndcln_dtypes[5:end]) + const _multi_section_dtypes = [ ("I", Int64), ("J", Int64), @@ -707,6 +771,9 @@ const _multi_section_dtypes = [ const _multi_section_dtypes_v35 = _multi_section_dtypes +const _multi_section_dtypes_v30 = + vcat(_multi_section_dtypes[1:3], _multi_section_dtypes[5:end]) + const _zone_dtypes = [("I", Int64), ("ZONAME", String)] const _zone_dtypes_v35 = _zone_dtypes @@ -753,6 +820,8 @@ const _FACTS_dtypes_v35 = vcat( ], ) +const _FACTS_dtypes_v30 = vcat([("N", Int64)], _FACTS_dtypes[2:19]) + const _switched_shunt_dtypes = [ ("I", Int64), ("MODSW", Int64), @@ -802,6 +871,9 @@ const _switched_shunt_dtypes_v35 = vcat( ], ) +const _switched_shunt_dtypes_v30 = + vcat(_switched_shunt_dtypes[1:2], _switched_shunt_dtypes[5:end]) + # TODO: Account for multiple lines in GNE Device entries const _gne_device_dtypes = [ ("NAME", String), @@ -942,6 +1014,30 @@ const _pti_dtypes_v35 = Dict{String, Array}( "SUBSTATION DATA" => _substation_dtypes_v35, ) +const _pti_dtypes_v30 = merge( + filter(p -> p.first != "FIXED SHUNT", _pti_dtypes), + Dict{String, Array}( + "BUS" => _bus_dtypes_v30, + "LOAD" => _load_dtypes_v30, + "GENERATOR" => _generator_dtypes_v30, + "BRANCH" => _branch_dtypes_v30, + "TRANSFORMER" => _transformer_dtypes_v30, + "TRANSFORMER TWO-WINDING LINE 1" => _transformer_2_1_dtypes_v30, + "TRANSFORMER TWO-WINDING LINE 2" => _transformer_2_2_dtypes_v30, + "TRANSFORMER TWO-WINDING LINE 3" => _transformer_2_3_dtypes_v30, + "TRANSFORMER THREE-WINDING LINE 1" => _transformer_3_1_dtypes_v30, + "TRANSFORMER THREE-WINDING LINE 2" => _transformer_3_2_dtypes_v30, + "TRANSFORMER THREE-WINDING LINE 3" => _transformer_3_3_dtypes_v30, + "TRANSFORMER THREE-WINDING LINE 4" => _transformer_3_4_dtypes_v30, + "TWO-TERMINAL DC" => _two_terminal_line_dtypes_v30, + "MULTI-TERMINAL DC" => _multi_term_main_dtypes_v30, + "MULTI-SECTION LINE" => _multi_section_dtypes_v30, + "FACTS CONTROL DEVICE" => _FACTS_dtypes_v30, + "SWITCHED SHUNT" => _switched_shunt_dtypes_v30, + "MULTI-TERMINAL DC NDCLN" => _multi_term_ndcln_dtypes_v30, + ), +) + const _default_case_identification = Dict( "IC" => 0, "SBASE" => 100.0, @@ -960,6 +1056,9 @@ const _default_case_identification_v35 = Dict( "BASFRQ" => 60, ) +const _default_case_identification_v30 = + merge(_default_case_identification, Dict("REV" => 30)) + const _default_bus = Dict( "BASKV" => 0.0, "IDE" => 1, @@ -1463,6 +1562,13 @@ const _default_substation_data_v35 = Dict( "LATI" => 0.0, "LONG" => 0.0, "SGR" => 0.1, + # Node data sub-record defaults (NI and I have no default allowed). + "NODES" => Dict( + "NAME" => "", + "STATUS" => 1, + "VM" => 1.0, + "VA" => 0.0, + ), ) const _pti_defaults = Dict( @@ -1513,6 +1619,19 @@ const _pti_defaults_v35 = Dict( "SUBSTATION DATA" => _default_substation_data_v35, ) +const _default_generator_v30 = merge(_default_generator, Dict("WMOD" => 0, "WPF" => 1.0)) + +const _default_load_v30 = merge(_default_load, Dict("INTRPT" => 0)) + +const _pti_defaults_v30 = merge( + filter(p -> p.first != "FIXED SHUNT", _pti_defaults), + Dict{String, Dict{String}}( + "CASE IDENTIFICATION" => _default_case_identification_v30, + "GENERATOR" => _default_generator_v30, + "LOAD" => _default_load_v30, + ), +) + function _correct_nothing_values!(data::Dict) if !haskey(data, "BUS") return @@ -1748,7 +1867,64 @@ function _parse_line_element!( end const _comment_split = r"(?!\B[\'][^\']*)[\/](?![^\']*[\']\B)" -const _split_string = r",(?=(?:[^']*'[^']*')*[^']*$)" + +""" + _split_fields(line) + +Splits a free-format PTI line into fields. The separator is a comma with +optional surrounding whitespace, or a run of whitespace, evaluated outside +quoted regions. Both quote styles are protected, and consecutive commas mark +skipped (empty) fields. +""" +function _split_fields(line::AbstractString) + fields = String[] + buf = IOBuffer() + in_single = false + in_double = false + i = firstindex(line) + last = lastindex(line) + while i <= last + c = line[i] + if in_single + print(buf, c) + c == '\'' && (in_single = false) + i = nextind(line, i) + elseif in_double + print(buf, c) + c == '"' && (in_double = false) + i = nextind(line, i) + elseif c == '\'' + in_single = true + print(buf, c) + i = nextind(line, i) + elseif c == '"' + in_double = true + print(buf, c) + i = nextind(line, i) + elseif c == ',' || isspace(c) + push!(fields, String(take!(buf))) + comma_count = 0 + while i <= last + cc = line[i] + if cc == ',' + comma_count += 1 + elseif isspace(cc) + else + break + end + i = nextind(line, i) + end + for _ in 2:comma_count + push!(fields, "") + end + else + print(buf, c) + i = nextind(line, i) + end + end + push!(fields, String(take!(buf))) + return fields +end """ _get_line_elements(line) @@ -1771,7 +1947,7 @@ function _get_line_elements(line::AbstractString) line = strip(line_comment[1]) comment = length(line_comment) > 1 ? strip(line_comment[2]) : "" - elements = split(line, _split_string) + elements = _split_fields(line) return (elements, comment) end @@ -1910,6 +2086,21 @@ function process_substation_data!( end end +""" +Determine the PSS(R)E revision number of a raw file from its CASE IDENTIFICATION +header line (the REV field), falling back to 30 when the field is absent or +unparseable. v35 files are identified separately via the `@!` comment convention. +""" +function _resolve_pti_version(data_lines, is_v35) + is_v35 && return 35 + header = findfirst(l -> !startswith(strip(l), "@!") && !isempty(strip(l)), data_lines) + header === nothing && return 30 + fields, _ = _get_line_elements(data_lines[header]) + length(fields) < 3 && return 30 + rev = tryparse(Int, strip(fields[3])) + return rev === nothing ? 30 : rev +end + """ _parse_pti_data(data_string, sections) @@ -1918,24 +2109,31 @@ Internal function. Parse a PTI raw file into a `Dict`, given the file (typically given by default by `get_pti_sections()`. """ function _parse_pti_data(data_io::IO) - sections = deepcopy(_pti_sections) - sections_v35 = deepcopy(_pti_sections_v35) data_lines = readlines(data_io) skip_lines = 0 skip_sublines = 0 subsection = "" - is_v35 = false + is_v35 = any(startswith.(data_lines, "@!")) + version = _resolve_pti_version(data_lines, is_v35) + + if version ∉ (29, 30, 32, 33, 35) + throw(IS.DataFormatError("Unsupported PSS(R)E raw version: $version")) + end + + active_sections = if version == 35 + deepcopy(_pti_sections_v35) + elseif version in (29, 30) + deepcopy(_pti_sections_v30) + else + deepcopy(_pti_sections) + end pti_data = Dict{String, Array{Dict}}() - section = popfirst!(sections) - section_v35 = popfirst!(sections_v35) + section = popfirst!(active_sections) + section_v35 = section section_data = Dict{String, Any}() - if any(startswith.(data_lines, "@!")) - is_v35 = true - end - header_line_start = is_v35 ? 2 : 1 # Start in second line due to @! # Dynamically handle the start of BUS DATA section # In v35 files, BUS DATA starts in different lines due to the fields GENERAL,GAUSS,NEWTON,ADJUST,TYSL,SOLVER,RATING @@ -1973,7 +2171,13 @@ function _parse_pti_data(data_io::IO) 4 # Start for all v33 files end - current_dtypes = is_v35 ? _pti_dtypes_v35 : _pti_dtypes + current_dtypes = if version == 35 + _pti_dtypes_v35 + elseif version in (29, 30) + _pti_dtypes_v30 + else + _pti_dtypes + end line_index = 1 while line_index <= length(data_lines) @@ -1986,7 +2190,8 @@ function _parse_pti_data(data_io::IO) (elements, comment) = _get_line_elements(line) - first_element = strip(elements[1]) + # a section terminator may be written as a bare or quoted zero (or Q) + first_element = strip(strip(elements[1]), ['\'', '"']) if is_v35 && (line_index == 3 || line_index == 4) && section_v35 == "CASE IDENTIFICATION" @@ -2013,7 +2218,7 @@ function _parse_pti_data(data_io::IO) elseif line_index > (is_v35 ? bus_data_start - 1 : 3) && length(elements) != 0 && first_element == "0" if line_index == bus_data_start - section = is_v35 ? popfirst!(sections_v35) : popfirst!(sections) + section = popfirst!(active_sections) end if length(elements) > 1 @@ -2025,7 +2230,7 @@ function _parse_pti_data(data_io::IO) IS.LOG_GROUP_PARSING end - current_sections = is_v35 ? sections_v35 : sections + current_sections = active_sections if !isempty(current_sections) section = popfirst!(current_sections) end @@ -2034,7 +2239,7 @@ function _parse_pti_data(data_io::IO) continue else if line_index == bus_data_start - section = is_v35 ? popfirst!(sections_v35) : popfirst!(sections) + section = popfirst!(active_sections) section_data = Dict{String, Any}() end @@ -2167,9 +2372,9 @@ function _parse_pti_data(data_io::IO) _parse_line_element!(section_data, elements, section, current_dtypes) catch message throw( - @error( - "Parsing failed at line $line_index: $(sprint(showerror, message))" - ) + DataFormatError( + "Parsing failed at line $line_index: $(sprint(showerror, message))", + ), ) end line_index += 1 @@ -2185,7 +2390,7 @@ function _parse_pti_data(data_io::IO) ) catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2241,7 +2446,7 @@ function _parse_pti_data(data_io::IO) ) catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2288,7 +2493,7 @@ function _parse_pti_data(data_io::IO) end catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2313,7 +2518,7 @@ function _parse_pti_data(data_io::IO) ) catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2359,7 +2564,7 @@ function _parse_pti_data(data_io::IO) _parse_line_element!(section_data, elements, section, current_dtypes) catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2372,7 +2577,7 @@ function _parse_pti_data(data_io::IO) _parse_line_element!(section_data, elements, section, current_dtypes) catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2391,7 +2596,7 @@ function _parse_pti_data(data_io::IO) ) catch message throw( - @error( + DataFormatError( "Parsing failed at line $line_index: $(sprint(showerror, message))", ), ) @@ -2632,12 +2837,44 @@ end Internal function. Populates empty fields with PSS(R)E PTI v33 default values """ function _populate_defaults!(data::Dict) - for section in _pti_sections + rev = get(data["CASE IDENTIFICATION"][1], "REV", 30) + version = rev == "" ? 30 : rev + sections = if version == 35 + _pti_sections_v35 + elseif version in (29, 30) + _pti_sections_v30 + else + _pti_sections + end + defaults = if version == 35 + _pti_defaults_v35 + elseif version in (29, 30) + _pti_defaults_v30 + else + _pti_defaults + end + + for section in sections if haskey(data, section) - component_defaults = _pti_defaults[section] + component_defaults = defaults[section] for component in data[section] + if version in (29, 30) && + section in ("GENERATOR", "LOAD", "BUS", "SWITCHED SHUNT") + for (field, default_value) in component_defaults + if !haskey(component, field) + component[field] = default_value + end + end + end for (field, field_value) in component if isa(field_value, Array) + if !haskey(component_defaults, field) + @warn( + "'$field' in '$section' has no default value", + maxlog = 1, + ) + continue + end sub_component_defaults = component_defaults[field] for sub_component in field_value for (sub_field, sub_field_value) in sub_component diff --git a/src/powerflowdata_data.jl b/src/powerflowdata_data.jl deleted file mode 100644 index b88455b..0000000 --- a/src/powerflowdata_data.jl +++ /dev/null @@ -1,33 +0,0 @@ -""" -Container for data parsed by PowerFlowData. - -# Fields -- `data::PowerFlowData.Network`: Network structure containing the parsed power system data - -# Example -```julia -pfd_data = PowerFlowDataNetwork("system.raw") -# Access the network data -buses = pfd_data.data.buses -generators = pfd_data.data.generators -``` -""" -struct PowerFlowDataNetwork - data::PowerFlowData.Network -end - -""" -Constructs PowerFlowDataNetwork from a raw file. -Currently Supports PSSE data files v30, v32 and v33 - -# Arguments -- `file::Union{String, IO}`: Path to the PSSE raw file or IO stream to parse - -# Example -```julia -pfd_data = PowerFlowDataNetwork("system.raw") -``` -""" -function PowerFlowDataNetwork(file::Union{String, IO}; kwargs...) - return PowerFlowDataNetwork(PowerFlowData.parse_network(file)) -end diff --git a/test/Project.toml b/test/Project.toml index 117e147..e7e0d59 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,6 +2,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +PowerFlowFileParser = "bed98974-b02e-5e2f-9ee0-a103f5c450dd" PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/fixtures/v30_bus_shunt.raw b/test/fixtures/v30_bus_shunt.raw new file mode 100644 index 0000000..f9fcf2a --- /dev/null +++ b/test/fixtures/v30_bus_shunt.raw @@ -0,0 +1,60 @@ +0,100.0,30 / PSS(tm)E-30 RAW created +MODEL VERSION +SOME OTHER COMMENTS +111,'STBC ',69.00,1, 10.00, 5.00,227, 1,1.09814, -8.327, 1 /* [STBC 1 ] */ +112,'D2JK ', 69.00,1, 0.00, 0.00,117, 1,1.04896, -7.247, 2 /* [D2JK 5 ] */ +113,'7HJG# ', 69.00,3, 0.00, 0.00,127, 1,1.02894, -4.139, 2 /* [D2JK 6 ] */ +0 / end of bus cards +111,'G1',1,227, 1, -0.004, -0.000, -0.003, -0.000, 0.000, -0.000, 1 /* [STBC G1 ] */ +113,' G2',1,227, 1, 0.345, 0.024, 0.711, 0.076, -0.028, 0.003, 2 /* [D2JK LG1 ] */ +0 / end of load cards +111,'ST', 0.00, 0.00, 110.000, 0.000, 1.01375,334153, 161.0, 0.0, 1.0, 0.0, 0.0, 1.0,1, 200.0, 90.00, 0.00, 1,1.00 /* [STBC ST ] */ +112,'PV ', 3.10, 91.00, 91.000, -31.000, 1.14634, 0, 175.0, 0.0, 1.0, 0.0, 0.0, 1.0,1, 300.0, 91.00, 0.00, 2,1.00 /* [D2JK G1 ] */ +113,'1', 313.01, 82.29, 147.000, -72.000, 1.12330, 0, 380.0, 0.0, 1.0, 0.0, 0.0, 1.0,1, 55.1, 229.00, 0.00, 2,1.00 /* [D2JK 1 ] */ +0 / end of unit cards +111, 112,3 , 0.001870, 0.004420, 0.00227, 322, 322, 322, 0.0, 0.0, 0.0, 0.0, 1, 0.000, 1,1.00 /* [STBC 3 ] */ +111, 113,B , 0.029380, 0.045470, 0.00094, 67, 67, 67, 0.0, 0.0, 0.0, 0.0, 1, 0.000, 1,1.00 /* [D2JK B ] */ +112, 113,C , 0.024915, 0.019260, 0.01019, 546, 546, 546, 0.0, 0.0, 0.0, 0.0, 1, 0.000, 1,1.00 /* [D2JK C ] */ +0 / end of branch cards +112,113, 0,'G1', 1, 1, 1, 0.000000, 0.000000, 2,' XF10 ', 1, 26,1.00 + 0.032470, 0.065600,200.0 + 1.0000, 350.00, 0.000, 310, 310, 310, 0, 0, 1.0000, 1.0000, 0.0000, 0.0000, 2 , 0,0.0000,0.0000 /* [ATBFL XF10 ] */ + 1.0000, 169.00 +113,111, 112,'Z1', 1, 1, 1, 0.000000, 0.000000, 2,' AB1 _20 ', 1, 20,1.00 + 0.039570, 0.066600,200.0, 100.00, 0.000690, 0.078400, 100.00, 0.000590, 0.059100, 100.00,1.01893 + 1.0000, 350.00, 0.000, 310, 310, 310, 0, 0, 1.0000, 1.0000, 0.0000, 0.0000, 2 , 0,0.0000,0.0000 /* [AFBFZ VF10 ] */ + 1.0000, 169.00, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33 , 0,0.00000,0.00000 +13.8000, 13.800, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33 , 0,0.00000,0.00000 +0 / end of transformer adjustment cards +227,456, 2121.7211, 6.0000,'ABC1 ' +117,456, 2121.7211, 6.0000,'ABC2 ' +127,456, 2121.7211, 6.0000,'ABC3 ' +0 / end of area interchange cards + 11,1, 8.7531, 1600.60, 500.00, 400.00, 1.2468, 0.00000,'I', 0.00, 20, 1.00000 + 112, 2,33.00, 5.50, 0.0001, 3.0000, 500.0,0.33330,1.06275,1.50000,0.51000,0.00625, 0, 0, 0,'11', 0.0000 + 2222, 2,21.00,10.50, 0.0000, 3.0440, 250.0,0.97777,1.00000,1.40000,0.70000,0.00625, 0, 152, 3021,'TX', 2.0000 +0 / end of 2-terminal DC line cards +'line 1 ', 1, 0.0000, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0 + 1117, 1, 1, 150.000,1.01200,1118.0000,1.6400, 0.00, 210.00, 1300.19,0.5000, 100.00,-100.00,1117,100.00 + 114, 2, 1, -20.000,1.02100,1119.0000,1.6400, 0.00, 210.00, 1300.19,0.5000, 100.00,-100.00, 114,100.00 +0 / end of Voltage Source Converter cards +113,0, 1.123, 1.123, 0, 100.0,' ' , 26.00, 1, 26.00 /* [GILLY 113 ] */ +0 / end of shunt cards + 1, -40.000, 1.849, -30.000, 1.402, -20.000, 1.196, -10.000, 1.045, 0.000, 1.000, 10.000, 1.045, 20.000, 1.161, 30.000, 1.366, 40.000, 1.741 +0 / end of transformer impedance correction table cards +0 / end of multi-terminal DC line cards + 1, 2, '&1', 3 +114,-112, '&2', 4, 5 +0 / end of multi-section line cards +117,'ABC ' /*[ABC ] */ +127,'CDEF' /*[CDEF] */ +227,' CDEG ' /*[ CDEG] */ +0 / end of zone cards + 1, 2,'A', 10.00 + 1, 2,'B', -20.00 +0 / end of area transaction cards +1,' ABC ' +2,' CDE ' +0 / end of owner cards + 1, 113, 0,1, 0.000, 0.000,1.11111, 50.000, 100.000,0.97777,1.13333,1.00000, 0.000, 0.05555, 100.00, 1, 0.00000, 0.00000,0 +0 / end of FACTS device cards diff --git a/test/fixtures/v31_header.raw b/test/fixtures/v31_header.raw new file mode 100644 index 0000000..203f37c --- /dev/null +++ b/test/fixtures/v31_header.raw @@ -0,0 +1,60 @@ +0,100.0,31 / PSS(tm)E-30 RAW created +MODEL VERSION +SOME OTHER COMMENTS +111,'STBC ',69.00,1, 0.00, 0.00,227, 1,1.09814, -8.327, 1 /* [STBC 1 ] */ +112,'D2JK ', 69.00,1, 0.00, 0.00,117, 1,1.04896, -7.247, 2 /* [D2JK 5 ] */ +113,'7HJG# ', 69.00,3, 0.00, 0.00,127, 1,1.02894, -4.139, 2 /* [D2JK 6 ] */ +0 / end of bus cards +111,'G1',1,227, 1, -0.004, -0.000, -0.003, -0.000, 0.000, -0.000, 1 /* [STBC G1 ] */ +113,' G2',1,227, 1, 0.345, 0.024, 0.711, 0.076, -0.028, 0.003, 2 /* [D2JK LG1 ] */ +0 / end of load cards +111,'ST', 0.00, 0.00, 110.000, 0.000, 1.01375,334153, 161.0, 0.0, 1.0, 0.0, 0.0, 1.0,1, 200.0, 90.00, 0.00, 1,1.00 /* [STBC ST ] */ +112,'PV ', 3.10, 91.00, 91.000, -31.000, 1.14634, 0, 175.0, 0.0, 1.0, 0.0, 0.0, 1.0,1, 300.0, 91.00, 0.00, 2,1.00 /* [D2JK G1 ] */ +113,'1', 313.01, 82.29, 147.000, -72.000, 1.12330, 0, 380.0, 0.0, 1.0, 0.0, 0.0, 1.0,1, 55.1, 229.00, 0.00, 2,1.00 /* [D2JK 1 ] */ +0 / end of unit cards +111, 112,3 , 0.001870, 0.004420, 0.00227, 322, 322, 322, 0.0, 0.0, 0.0, 0.0, 1, 0.000, 1,1.00 /* [STBC 3 ] */ +111, 113,B , 0.029380, 0.045470, 0.00094, 67, 67, 67, 0.0, 0.0, 0.0, 0.0, 1, 0.000, 1,1.00 /* [D2JK B ] */ +112, 113,C , 0.024915, 0.019260, 0.01019, 546, 546, 546, 0.0, 0.0, 0.0, 0.0, 1, 0.000, 1,1.00 /* [D2JK C ] */ +0 / end of branch cards +112,113, 0,'G1', 1, 1, 1, 0.000000, 0.000000, 2,' XF10 ', 1, 26,1.00 + 0.032470, 0.065600,200.0 + 1.0000, 350.00, 0.000, 310, 310, 310, 0, 0, 1.0000, 1.0000, 0.0000, 0.0000, 2 , 0,0.0000,0.0000 /* [ATBFL XF10 ] */ + 1.0000, 169.00 +113,111, 112,'Z1', 1, 1, 1, 0.000000, 0.000000, 2,' AB1 _20 ', 1, 20,1.00 + 0.039570, 0.066600,200.0, 100.00, 0.000690, 0.078400, 100.00, 0.000590, 0.059100, 100.00,1.01893 + 1.0000, 350.00, 0.000, 310, 310, 310, 0, 0, 1.0000, 1.0000, 0.0000, 0.0000, 2 , 0,0.0000,0.0000 /* [AFBFZ VF10 ] */ + 1.0000, 169.00, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33 , 0,0.00000,0.00000 +13.8000, 13.800, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33 , 0,0.00000,0.00000 +0 / end of transformer adjustment cards +227,456, 2121.7211, 6.0000,'ABC1 ' +117,456, 2121.7211, 6.0000,'ABC2 ' +127,456, 2121.7211, 6.0000,'ABC3 ' +0 / end of area interchange cards + 11,1, 8.7531, 1600.60, 500.00, 400.00, 1.2468, 0.00000,'I', 0.00, 20, 1.00000 + 112, 2,33.00, 5.50, 0.0001, 3.0000, 500.0,0.33330,1.06275,1.50000,0.51000,0.00625, 0, 0, 0,'11', 0.0000 + 2222, 2,21.00,10.50, 0.0000, 3.0440, 250.0,0.97777,1.00000,1.40000,0.70000,0.00625, 0, 152, 3021,'TX', 2.0000 +0 / end of 2-terminal DC line cards +'line 1 ', 1, 0.0000, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0 + 1117, 1, 1, 150.000,1.01200,1118.0000,1.6400, 0.00, 210.00, 1300.19,0.5000, 100.00,-100.00,1117,100.00 + 114, 2, 1, -20.000,1.02100,1119.0000,1.6400, 0.00, 210.00, 1300.19,0.5000, 100.00,-100.00, 114,100.00 +0 / end of Voltage Source Converter cards +113,0, 1.123, 1.123, 0, 100.0,' ' , 26.00, 1, 26.00 /* [GILLY 113 ] */ +0 / end of shunt cards + 1, -40.000, 1.849, -30.000, 1.402, -20.000, 1.196, -10.000, 1.045, 0.000, 1.000, 10.000, 1.045, 20.000, 1.161, 30.000, 1.366, 40.000, 1.741 +0 / end of transformer impedance correction table cards +0 / end of multi-terminal DC line cards + 1, 2, '&1', 3 +114,-112, '&2', 4, 5 +0 / end of multi-section line cards +117,'ABC ' /*[ABC ] */ +127,'CDEF' /*[CDEF] */ +227,' CDEG ' /*[ CDEG] */ +0 / end of zone cards + 1, 2,'A', 10.00 + 1, 2,'B', -20.00 +0 / end of area transaction cards +1,' ABC ' +2,' CDE ' +0 / end of owner cards + 1, 113, 0,1, 0.000, 0.000,1.11111, 50.000, 100.000,0.97777,1.13333,1.00000, 0.000, 0.05555, 100.00, 1, 0.00000, 0.00000,0 +0 / end of FACTS device cards diff --git a/test/test_parse_v30.jl b/test/test_parse_v30.jl new file mode 100644 index 0000000..16fdfb5 --- /dev/null +++ b/test/test_parse_v30.jl @@ -0,0 +1,116 @@ +@testset "PSSE v30 parsing" begin + pm = PowerModelsData(joinpath(PSSE_RAW_DIR, "synthetic_data_v30.raw")) + + @test pm.data["source_version"] == "30" + @test pm.data["baseMVA"] == 100.0 + + @test haskey(pm.data["bus"], 111) + @test haskey(pm.data["bus"], 112) + @test haskey(pm.data["bus"], 113) + @test pm.data["bus"][111]["base_kv"] == 69.0 + @test isapprox(pm.data["bus"][111]["vm"], 1.09814; atol = 1e-5) + + @test length(pm.data["gen"]) == 3 + @test length(pm.data["load"]) == 2 + # 3 lines + 1 two-winding transformer are all stored under "branch" + @test length(pm.data["branch"]) == 4 + @test length(pm.data["3w_transformer"]) == 1 + + # branch 111-112 series impedance from the raw file + b = first( + v for v in values(pm.data["branch"]) + if Set((v["f_bus"], v["t_bus"])) == Set((111, 112)) && !v["transformer"] + ) + @test isapprox(b["br_r"], 0.001870; atol = 1e-6) + @test isapprox(b["br_x"], 0.004420; atol = 1e-6) + + for (_, g) in pm.data["gen"] + @test haskey(g, "ext") + end +end + +@testset "PSSE v30 bus shunt" begin + pm = PowerModelsData(joinpath(@__DIR__, "fixtures", "v30_bus_shunt.raw")) + shunts = [s for (_, s) in pm.data["shunt"] if s["shunt_bus"] == 111] + @test length(shunts) == 1 + # buses 112 and 113 carry zero GL/BL and must not produce shunts + @test length(pm.data["shunt"]) == 1 + # gs/bs come out in system per-unit (raw GL=10.0, BL=5.0 at baseMVA=100.0), + # matching the FIXED SHUNT conversion applied by make_per_unit! + @test isapprox(shunts[1]["gs"], 0.1; atol = 1e-6) + @test isapprox(shunts[1]["bs"], 0.05; atol = 1e-6) +end + +@testset "Unsupported PSSE version errors clearly" begin + @test_throws IS.DataFormatError PowerModelsData( + joinpath(@__DIR__, "fixtures", "v31_header.raw"), + ) +end + +@testset "v30 multi-terminal DC NDCLN layout" begin + fields = first.(PowerFlowFileParser._pti_dtypes_v30["MULTI-TERMINAL DC NDCLN"]) + @test fields == ["IDC", "JDC", "DCCKT", "RDC", "LDC"] + @test !("MET" in fields) +end + +@testset "v30 real system component counts" begin + expected = Dict( + "11BUS_KUNDUR_30.raw" => + (bus = 11, load = 2, gen = 4, line = 8, xf2 = 4, xf3 = 0), + "RTS_30.raw" => + (bus = 73, load = 51, gen = 160, line = 105, xf2 = 15, xf3 = 0), + ) + for (file, e) in expected + pm = PowerModelsData(joinpath(PSSE_RAW_DIR, file)).data + @test pm["source_version"] == "30" + @test length(pm["bus"]) == e.bus + @test length(pm["load"]) == e.load + @test length(pm["gen"]) == e.gen + @test count(v -> !v["transformer"], values(pm["branch"])) == e.line + @test count(v -> v["transformer"], values(pm["branch"])) == e.xf2 + @test length(get(pm, "3w_transformer", Dict())) == e.xf3 + end +end + +@testset "free-format field tokenizer" begin + sf = PowerFlowFileParser._split_fields + # blank-delimited + @test sf("0 100.00") == ["0", "100.00"] + # comma-delimited with padding absorbed + @test sf("0, 100.00, 33") == ["0", "100.00", "33"] + # blank-delimited with a single-quoted name containing interior blanks + @test sf("1 'ADK ' 138.00") == ["1", "'ADK '", "138.00"] + # double-quoted name with interior blanks must not be split + @test sf("\"LINE 1\",1,20.0") == ["\"LINE 1\"", "1", "20.0"] + # consecutive commas mark a skipped field + @test sf("1002,, 345.0") == ["1002", "", "345.0"] +end + +@testset "quoted-zero section terminator" begin + p(s) = PowerFlowFileParser._parse_pti_data(IOBuffer(s)) + header = "0, 100.0, 33 / t\ncomment1\ncomment2\n" + bus = "1,'BUS1',138.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n0 / end bus\n" + fixed_shunt = "0 / end fixed shunt\n" + gen = "1,'1',0.0,0.0,100.0,-100.0,1.0\n0 / end gen\n" + # an empty section terminated by a quoted zero must behave like a bare zero + bare = p(header * bus * "0 / end load\n" * fixed_shunt * gen) + quoted = p(header * bus * "'0' / end load\n" * fixed_shunt * gen) + @test haskey(quoted, "GENERATOR") + @test quoted["GENERATOR"][1]["I"] == 1 + @test quoted["GENERATOR"] == bare["GENERATOR"] + @test get(quoted, "LOAD", []) == get(bare, "LOAD", []) +end + +@testset "version detection over delimiter styles" begin + rv = PowerFlowFileParser._resolve_pti_version + # REV must be read from both comma- and whitespace-delimited headers + for delim in (", ", " ") + @test rv(["0$(delim)100.0$(delim)33 / c", "c1", "c2"], false) == 33 + @test rv(["0$(delim)100.0$(delim)32 / c", "c1", "c2"], false) == 32 + end + # a missing REV field defaults to 30 in either style + @test rv(["0, 100.00 / c", "c1", "c2"], false) == 30 + @test rv(["0 100.00 / c", "c1", "c2"], false) == 30 + # a v35 file is identified by its @! marker regardless of header delimiter + @test rv(["@!...", "0 100.0 33"], true) == 35 +end