From 1e372018d29fa6cc2ea5a7026bc4b0e6920e2a52 Mon Sep 17 00:00:00 2001 From: LucasLessa1 Date: Mon, 25 Aug 2025 18:09:09 -0300 Subject: [PATCH 1/3] Add new export_data function to export the xml to atp --- Project.toml | 2 + src/ImportExport.jl | 5 +- src/ImportExport/atp.jl | 275 +++++++++++++++++++++++++++++++++++ test/cable_test.json | 310 ++++++++++++++++++++++++++++++++++++++++ test/export_xml.jl | 165 +++++++++++++++++++++ test/runtests.jl | 48 +++++++ 6 files changed, 804 insertions(+), 1 deletion(-) create mode 100644 src/ImportExport/atp.jl create mode 100644 test/cable_test.json create mode 100644 test/export_xml.jl diff --git a/Project.toml b/Project.toml index bcf85341..f211f429 100644 --- a/Project.toml +++ b/Project.toml @@ -33,6 +33,7 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [sources] GetDP = {rev = "main", url = "https://github.com/Electa-Git/GetDP.jl"} @@ -65,6 +66,7 @@ Reexport = "1.2.2" Serialization = "1.11.0" SpecialFunctions = "2.5.0" Statistics = "1.11.1" +TestItemRunner = "1.1.0" [extras] Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" diff --git a/src/ImportExport.jl b/src/ImportExport.jl index fa72e647..a604fbee 100644 --- a/src/ImportExport.jl +++ b/src/ImportExport.jl @@ -26,6 +26,8 @@ module ImportExport # Export public API export export_data +export export_ZY2XML +export read_atp_data export save export load! export get @@ -39,6 +41,7 @@ include("commondeps.jl") using Measurements using EzXML # For PSCAD export using Dates # For PSCAD export +using Printf # For ATP export using JSON3 using Serialization # For .jls format import Base: get @@ -54,6 +57,6 @@ include("ImportExport/deserialize.jl") include("ImportExport/cableslibrary.jl") include("ImportExport/materialslibrary.jl") include("ImportExport/pscad.jl") - +include("ImportExport/atp.jl") end # module ImportExport diff --git a/src/ImportExport/atp.jl b/src/ImportExport/atp.jl new file mode 100644 index 00000000..78de7e02 --- /dev/null +++ b/src/ImportExport/atp.jl @@ -0,0 +1,275 @@ + +""" +$(TYPEDSIGNATURES) + +Exports calculated [`LineParameters`](@ref) to an ATP-style XML file. + +This function takes all the system information, cables, ground parameters and frequency and assembles the XML to be used in ATPDraw + +# Arguments + +- `problem`: A [`LineParametersProblem`](@ref) object used to retrieve the frequency vector for the export. +- `file_name`: The path to the output XML file (default: "*_export.xml"). + +# Returns + +- The absolute path of the saved file. +""" +function export_data(::Val{:atp}, + problem::LineParametersProblem; + file_name::String="$(cable_system.system_id)_export.xml" +)::Union{String,Nothing} + + function _set_attributes!(element::EzXML.Node, attrs::Dict) + for (k, v) in attrs + element[k] = string(v) + end + end + # --- 1. Setup Constants and Variables --- + cable_system = problem.system + earth_props = problem.earth_props + file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) + freq = problem.frequencies[1] + num_phases = length(cable_system.cables) + + # Create XML Structure and LCC Component + doc = XMLDocument() + project = ElementNode("project") + setroot!(doc, project) + _set_attributes!(project, Dict("Application" => "ATPDraw", "Version" => "7.3", "VersionXML" => "1")) + header = addelement!(project, "header") + _set_attributes!(header, Dict("Timestep" => 1e-6, "Tmax" => 0.1, "XOPT" => 0, "COPT" => 0, "SysFreq" => freq, "TopLeftX" => 200, "TopLeftY" => 0)) + objects = addelement!(project, "objects") + variables = addelement!(project, "variables") + comp = addelement!(objects, "comp") + _set_attributes!(comp, Dict("Name" => "LCC", "Id" => "$(cable_system.system_id)_1", "Capangl" => 90, "CapPosX" => -10, "CapPosY" => -25, "Caption" => "")) + comp_content = addelement!(comp, "comp_content") + _set_attributes!(comp_content, Dict("PosX" => 280, "PosY" => 360, "NumPhases" => num_phases, "Icon" => "default", "SinglePhaseIcon" => "true")) + for side in ["IN", "OUT"]; y0 = -20; for k in 1:num_phases; y0 += 10; node = addelement!(comp_content, "node"); _set_attributes!(node, Dict("Name" => "$side$k", "Value" => "C$(k)$(side=="IN" ? "SND" : "RCV")", "UserNamed" => "true", "Kind" => k, "PosX" => side == "IN" ? -20 : 20, "PosY" => y0, "NamePosX" => 0, "NamePosY" => 0)); end; end + soil_rho = earth_props.layers[end].base_rho_g + for (name, value) in [("Length", cable_system.line_length), ("Freq", freq), ("Grnd resis", soil_rho)]; data_node = addelement!(comp_content, "data"); _set_attributes!(data_node, Dict("Name" => name, "Value" => value)); end + + # Populate the LCC Sub-structure with CORRECTLY Structured Cable Data + lcc_node = addelement!(comp, "LCC") + _set_attributes!(lcc_node, Dict("NumPhases" => num_phases, "IconLength" => "true", "LineCablePipe" => 2, "ModelType" => 1)) + cable_header = addelement!(lcc_node, "cable_header") + _set_attributes!(cable_header, Dict("InAirGrnd" => 1, "MatrixOutput" => "true", "ExtraCG"=>"$(num_phases)")) + + for (k, cable) in enumerate(cable_system.cables) + cable_node = addelement!(cable_header, "cable") + + num_components = length(cable.design_data.components) + outermost_radius = cable.design_data.components[end].insulator_group.radius_ext + + _set_attributes!(cable_node, Dict( + "NumCond" => num_components, + "Rout" => outermost_radius, + "PosX" => cable.horz, + "PosY" => cable.vert + )) + + for component in cable.design_data.components + conductor_node = addelement!(cable_node, "conductor") + + cond_group = component.conductor_group + ins_group = component.insulator_group + + rho_eq = calc_equivalent_rho(cond_group.resistance, cond_group.radius_ext, cond_group.radius_in) + mu_r_cond = calc_equivalent_mu(cond_group.gmr, cond_group.radius_ext, cond_group.radius_in) + mu_r_ins = ins_group.layers[1].material_props.mu_r + eps_eq = calc_equivalent_eps(ins_group.shunt_capacitance, ins_group.radius_in, ins_group.radius_ext) + + _set_attributes!(conductor_node, Dict( + "Rin" => cond_group.radius_in, + "Rout" => cond_group.radius_ext, + "rho" => rho_eq, + "muC" => mu_r_cond, + "muI" => mu_r_ins, + "epsI" => eps_eq, + "Cext" => ins_group.shunt_capacitance, + "Gext" => ins_group.shunt_conductance + )) + end + end + + # Finalize and Write to File + _set_attributes!(variables, Dict("NumSim" => 1, "IOPCVP" => 0, "UseParser" => "false")) + + try + open(file_name, "w") do fid + prettyprint(fid, doc) + end + @info "XML file saved to: $(_display_path(file_name))" + return file_name + catch e + @error "Failed to write XML file '$file_name'" exception=(e, catch_backtrace()) + return nothing + end +end + + +""" + read_atp_data(filepath::String, cable_system::LineCableSystem) + +Reads an ATP `.lis` output file, extracts the Ze and Zi matrices, and dynamically +reorders them to a grouped-by-phase format based on the provided `cable_system` +structure. It correctly handles systems with a variable number of components per cable. + +# Arguments +- `filepath`: The path to the `.lis` file. +- `cable_system`: The `LineCableSystem` object corresponding to the data in the file. + +# Returns +- `Array{T, 2}`: A 2D complex matrix representing the total reordered series + impedance `Z = Ze + Zi` for a single frequency. +- `nothing`: If the file cannot be found, parsed, or if the matrix dimensions in the + file do not match the provided `cable_system` structure. +""" +function read_atp_data(filepath::String, cable_system::LineCableSystem)::Union{Array{COMPLEXSCALAR, 2}, Nothing} + # --- Inner helper function to parse a matrix block from text lines --- + function parse_block(block_lines::Vector{String}) + data_lines = filter(line -> !isempty(strip(line)), block_lines) + if isempty(data_lines) return Matrix{ComplexF64}(undef, 0, 0) end + matrix_size = length(split(data_lines[1])) + real_parts = zeros(Float64, matrix_size, matrix_size) + imag_parts = zeros(Float64, matrix_size, matrix_size) + row_counter = 1 + for i in 1:2:length(data_lines) + if i + 1 > length(data_lines) break end + real_line, imag_line = data_lines[i], data_lines[i+1] + try + real_parts[row_counter, :] = [parse(Float64, s) for s in split(real_line)[1:matrix_size]] + imag_parts[row_counter, :] = [parse(Float64, s) for s in split(imag_line)[1:matrix_size]] + catch e; @error "Parsing failed" exception=(e, catch_backtrace()); return nothing end + row_counter += 1 + if row_counter > matrix_size break end + end + return real_parts + im * imag_parts + end + + # --- Main Function Logic --- + if !isfile(filepath) @error "File not found: $filepath"; return nothing end + lines = readlines(filepath) + ze_start_idx = findfirst(occursin.("Earth impedance [Ze]", lines)) + zi_start_idx = findfirst(occursin.("Conductor internal impedance [Zi]", lines)) + if isnothing(ze_start_idx) || isnothing(zi_start_idx) @error "Could not find Ze/Zi headers."; return nothing end + + Ze = parse_block(lines[ze_start_idx + 1 : zi_start_idx - 1]) + Zi = parse_block(lines[zi_start_idx + 1 : end]) + if isnothing(Ze) || isnothing(Zi) return nothing end + + # --- DYNAMICALLY GENERATE PERMUTATION INDICES (Numerical Method) --- + component_counts = [length(c.design_data.components) for c in cable_system.cables] + total_conductors = sum(component_counts) + num_phases = length(component_counts) + max_components = isempty(component_counts) ? 0 : maximum(component_counts) + + if size(Ze, 1) != total_conductors + @error "Matrix size from file ($(size(Ze,1))x$(size(Ze,1))) does not match total components in cable_system ($total_conductors)." + return nothing + end + + num_conductors_per_type = [sum(c >= i for c in component_counts) for i in 1:max_components] + type_offsets = cumsum([0; num_conductors_per_type[1:end-1]]) + + permutation_indices = Int[] + sizehint!(permutation_indices, total_conductors) + instance_counters = ones(Int, max_components) + for phase_idx in 1:num_phases + for comp_type_idx in 1:component_counts[phase_idx] + instance = instance_counters[comp_type_idx] + original_idx = type_offsets[comp_type_idx] + instance + push!(permutation_indices, original_idx) + instance_counters[comp_type_idx] += 1 + end + end + + Ze_reordered = Ze[permutation_indices, permutation_indices] + Zi_reordered = Zi[permutation_indices, permutation_indices] + + return Ze_reordered+Zi_reordered +end + + +""" +$(TYPEDSIGNATURES) + +Exports calculated [`LineParameters`](@ref) to an ATP-style XML file. + +This function takes the results of a simulation (Z and Y matrices) and writes them +into a structured XML format for use in other programs. + +# Arguments + +- `line_params`: A [`LineParameters`](@ref) object containing the calculated Z and Y matrices to be exported. +- `cable_system`: A [`LineCableSystem`](@ref) object used for metadata (e.g., the default filename). +- `problem`: A [`LineParametersProblem`](@ref) object used to retrieve the frequency vector for the export. +- `file_name`: The path to the output XML file (default: "*_export.xml"). + +# Returns + +- The absolute path of the saved file. +""" +function export_ZY2XML(line_params::LineParameters, + problem::LineParametersProblem; + file_name::String="$(problem.system.system_id)_ZY.xml", +)::Union{String,Nothing} + + cable_length = 1.0 + atp_format = "G+Bi" + freq_vec = problem.frequencies + file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) + + open(file_name, "w") do fid + num_phases = size(line_params.Z, 1) + y_fmt = (atp_format == "C") ? "C" : "G+Bi" + + @printf(fid, "\n", num_phases, cable_length, y_fmt) + + # --- Z Matrix Printing --- + for (k, freq_val) in enumerate(freq_vec) + @printf(fid, " \n", freq_val) + for i in 1:num_phases + row_str = join([@sprintf("%.16E%+.16Ei", real(line_params.Z[i, j, k]), imag(line_params.Z[i, j, k])) for j in 1:num_phases], ",") + println(fid, row_str) + end + @printf(fid, " \n") + end + + # --- Y Matrix Printing --- + if atp_format == "C" + freq1 = f[1] + @printf(fid, " \n", freq1) + for i in 1:num_phases + row_str = join([@sprintf("%.16E", imag(line_params.Y[i, j, 1]) / (2 * pi * freq1)) for j in 1:num_phases], ",") + println(fid, row_str) + end + @printf(fid, " \n") + else # Case for "G+Bi" + for (k, freq_val) in enumerate(freq_vec) + @printf(fid, " \n", freq_val) + for i in 1:num_phases + row_str = join([@sprintf("%.16E%+.16Ei", real(line_params.Y[i, j, k]), imag(line_params.Y[i, j, k])) for j in 1:num_phases], ",") + println(fid, row_str) + end + @printf(fid, " \n") + end + end + + # --- Footer --- + println(fid, "") + end + try + # Use pretty print option for debugging comparisons if needed + # open(filename, "w") do io; prettyprint(io, doc); end + if isfile(file_name) + @info "XML file saved to: $(_display_path(file_name))" + end + return file_name + catch e + @error "Failed to write XML file '$(_display_path(file_name))': $(e)" + isa(e, SystemError) && println("SystemError details: ", e.extrainfo) + return nothing + rethrow(e) # Rethrow to indicate failure clearly + end +end \ No newline at end of file diff --git a/test/cable_test.json b/test/cable_test.json new file mode 100644 index 00000000..0a75ce9a --- /dev/null +++ b/test/cable_test.json @@ -0,0 +1,310 @@ +{ + "__julia_type__": "LineCableModels.DataModel.CablesLibrary", + "data": { + "test_cable": { + "cable_id": "test_cable", + "nominal_data": { + "U": 30, + "screen_cross_section": 35, + "capacitance": 0.39, + "__julia_type__": "LineCableModels.DataModel.NominalData", + "U0": 18, + "conductor_cross_section": 1000, + "armor_cross_section": null, + "resistance": 0.0291, + "inductance": 0.3, + "designation_code": "NA2XS(FL)2Y" + }, + "__julia_type__": "LineCableModels.DataModel.CableDesign", + "components": [ + { + "insulator_group": { + "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", + "layers": [ + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 32.3, + "mu_r": 1, + "rho": 5300 + }, + "radius_in": 0.02115, + "__julia_type__": "LineCableModels.DataModel.Semicon", + "radius_ext": 0.02145, + "temperature": 20 + }, + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1000, + "mu_r": 1, + "rho": 1000 + }, + "radius_in": 0.02145, + "__julia_type__": "LineCableModels.DataModel.Semicon", + "radius_ext": 0.02205, + "temperature": 20 + }, + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 2.3, + "mu_r": 1, + "rho": 197000000000000 + }, + "radius_in": 0.02205, + "__julia_type__": "LineCableModels.DataModel.Insulator", + "radius_ext": 0.03005, + "temperature": 20 + }, + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1000, + "mu_r": 1, + "rho": 500 + }, + "radius_in": 0.03005, + "__julia_type__": "LineCableModels.DataModel.Semicon", + "radius_ext": 0.030350000000000002, + "temperature": 20 + }, + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 32.3, + "mu_r": 1, + "rho": 5300 + }, + "radius_in": 0.030350000000000002, + "__julia_type__": "LineCableModels.DataModel.Semicon", + "radius_ext": 0.030650000000000004, + "temperature": 20 + } + ] + }, + "__julia_type__": "LineCableModels.DataModel.CableComponent", + "id": "core", + "conductor_group": { + "__julia_type__": "LineCableModels.DataModel.ConductorGroup", + "layers": [ + { + "lay_ratio": 0, + "material_props": { + "T0": 20, + "alpha": 0.00429, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 1.000022, + "rho": 2.8264e-8 + }, + "lay_direction": 1, + "radius_in": 0, + "__julia_type__": "LineCableModels.DataModel.WireArray", + "radius_wire": 0.00235, + "num_wires": 1, + "temperature": 20 + }, + { + "lay_ratio": 15, + "material_props": { + "T0": 20, + "alpha": 0.00429, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 1.000022, + "rho": 2.8264e-8 + }, + "lay_direction": 1, + "radius_in": 0.00235, + "__julia_type__": "LineCableModels.DataModel.WireArray", + "radius_wire": 0.00235, + "num_wires": 6, + "temperature": 20 + }, + { + "lay_ratio": 13.5, + "material_props": { + "T0": 20, + "alpha": 0.00429, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 1.000022, + "rho": 2.8264e-8 + }, + "lay_direction": 1, + "radius_in": 0.007050000000000001, + "__julia_type__": "LineCableModels.DataModel.WireArray", + "radius_wire": 0.00235, + "num_wires": 12, + "temperature": 20 + }, + { + "lay_ratio": 12.5, + "material_props": { + "T0": 20, + "alpha": 0.00429, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 1.000022, + "rho": 2.8264e-8 + }, + "lay_direction": 1, + "radius_in": 0.01175, + "__julia_type__": "LineCableModels.DataModel.WireArray", + "radius_wire": 0.00235, + "num_wires": 18, + "temperature": 20 + }, + { + "lay_ratio": 11, + "material_props": { + "T0": 20, + "alpha": 0.00429, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 1.000022, + "rho": 2.8264e-8 + }, + "lay_direction": 1, + "radius_in": 0.01645, + "__julia_type__": "LineCableModels.DataModel.WireArray", + "radius_wire": 0.00235, + "num_wires": 24, + "temperature": 20 + } + ] + } + }, + { + "insulator_group": { + "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", + "layers": [ + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 32.3, + "mu_r": 1, + "rho": 5300 + }, + "radius_in": 0.031700000000000006, + "__julia_type__": "LineCableModels.DataModel.Semicon", + "radius_ext": 0.03200000000000001, + "temperature": 20 + } + ] + }, + "__julia_type__": "LineCableModels.DataModel.CableComponent", + "id": "sheath", + "conductor_group": { + "__julia_type__": "LineCableModels.DataModel.ConductorGroup", + "layers": [ + { + "lay_ratio": 10, + "material_props": { + "T0": 20, + "alpha": 0.00393, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 0.999994, + "rho": 1.7241e-8 + }, + "lay_direction": 1, + "radius_in": 0.030650000000000004, + "__julia_type__": "LineCableModels.DataModel.WireArray", + "radius_wire": 0.000475, + "num_wires": 49, + "temperature": 20 + }, + { + "lay_ratio": 10, + "material_props": { + "T0": 20, + "alpha": 0.00393, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 0.999994, + "rho": 1.7241e-8 + }, + "lay_direction": 1, + "radius_in": 0.0316, + "__julia_type__": "LineCableModels.DataModel.Strip", + "width": 0.01, + "radius_ext": 0.031700000000000006, + "temperature": 20 + } + ] + } + }, + { + "insulator_group": { + "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", + "layers": [ + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 2.3, + "mu_r": 1, + "rho": 197000000000000 + }, + "radius_in": 0.032150000000000005, + "__julia_type__": "LineCableModels.DataModel.Insulator", + "radius_ext": 0.032200000000000006, + "temperature": 20 + }, + { + "material_props": { + "T0": 20, + "alpha": 0, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 2.3, + "mu_r": 1, + "rho": 197000000000000 + }, + "radius_in": 0.032200000000000006, + "__julia_type__": "LineCableModels.DataModel.Insulator", + "radius_ext": 0.034600000000000006, + "temperature": 20 + } + ] + }, + "__julia_type__": "LineCableModels.DataModel.CableComponent", + "id": "jacket", + "conductor_group": { + "__julia_type__": "LineCableModels.DataModel.ConductorGroup", + "layers": [ + { + "material_props": { + "T0": 20, + "alpha": 0.00429, + "__julia_type__": "LineCableModels.Materials.Material", + "eps_r": 1, + "mu_r": 1.000022, + "rho": 2.8264e-8 + }, + "radius_in": 0.03200000000000001, + "__julia_type__": "LineCableModels.DataModel.Tubular", + "radius_ext": 0.032150000000000005, + "temperature": 20 + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/test/export_xml.jl b/test/export_xml.jl new file mode 100644 index 00000000..bab9315c --- /dev/null +++ b/test/export_xml.jl @@ -0,0 +1,165 @@ +# test/test_atp_export.jl + +using Test +using TestItemRunner +using LineCableModels + +# Add your setup snippets here if they are in this file +# @testsnippet defaults begin ... end +# @testsnippet cable_system_export begin ... end + +@testsnippet deps_export_atp begin + using EzXML +end + +@testitem "Export to ATPDraw LCC format" setup=[defaults, cable_system_export, deps_export_atp] begin + + # All variables from the setup snippets are available here (problem_atp, cable_system, etc.) + + # 1. ARRANGE & ACT: Run the export in a temporary directory + mktempdir() do tmpdir + output_file = joinpath(tmpdir, "atp_export_test.xml") + result_path = export_data(Val(:atp), problem_atp, file_name=output_file) + + # 2. ASSERT: Basic file checks (unchanged) + @test result_path == output_file + @test isfile(output_file) + @test filesize(output_file) > 500 + + # 3. ASSERT: General XML structure and LCC data + println(" Performing high-level XML structure checks...") + doc = readxml(output_file) + root_node = root(doc) + + @test nodename(root_node) == "project" + @test root_node["Application"] == "ATPDraw" + + # Find the main LCC component content node + comp_content_node = findfirst("/project/objects/comp/comp_content", root_node) + @test !isnothing(comp_content_node) + + # Verify general parameters like Length, Freq, and Ground Resistivity + println(" Verifying general LCC data (Length, Freq, Grnd resis)...") + @test parse(Float64, findfirst("data[@Name='Length']", comp_content_node)["Value"]) ≈ cable_system.line_length + @test parse(Float64, findfirst("data[@Name='Freq']", comp_content_node)["Value"]) ≈ problem_atp.frequencies[1] + @test parse(Float64, findfirst("data[@Name='Grnd resis']", comp_content_node)["Value"]) ≈ problem_atp.earth_props.layers[end].base_rho_g + + # 4. ASSERT: Detailed validation of ALL cables and conductors + println(" Verifying all cables and their conductors...") + lcc_node = findfirst("/project/objects/comp/LCC", root_node) + cable_header = findfirst("cable_header", lcc_node) + cable_nodes = findall("cable", cable_header) + + @test length(cable_nodes) == num_phases + + # Loop through each cable exported in the XML and compare it to the source + for (i, cable_node) in enumerate(cable_nodes) + println(" -> Checking Cable #$i...") + source_cable = cable_system.cables[i] + + # Verify position of EACH cable + @test parse(Float64, cable_node["PosX"]) ≈ source_cable.horz + @test parse(Float64, cable_node["PosY"]) ≈ source_cable.vert + + # Verify the number of conductor components inside this cable + num_components = length(source_cable.design_data.components) + @test parse(Int, cable_node["NumCond"]) == num_components + + conductor_nodes = findall("conductor", cable_node) + @test length(conductor_nodes) == num_components + + # Loop through each conductor component within the cable + for (j, conductor_node) in enumerate(conductor_nodes) + source_component = source_cable.design_data.components[j] + cond_group = source_component.conductor_group + ins_group = source_component.insulator_group + + # Pre-calculate the expected values using the same functions as the export + expected_rho = calc_equivalent_rho(cond_group.resistance, cond_group.radius_ext, cond_group.radius_in) + expected_muC = calc_equivalent_mu(cond_group.gmr, cond_group.radius_ext, cond_group.radius_in) + expected_epsI = calc_equivalent_eps(ins_group.shunt_capacitance, ins_group.radius_in, ins_group.radius_ext) + + # Assert that every attribute matches the expected value + @test parse(Float64, conductor_node["Rin"]) ≈ cond_group.radius_in + @test parse(Float64, conductor_node["Rout"]) ≈ cond_group.radius_ext + @test parse(Float64, conductor_node["rho"]) ≈ expected_rho + @test parse(Float64, conductor_node["muC"]) ≈ expected_muC + @test parse(Float64, conductor_node["muI"]) ≈ ins_group.layers[1].material_props.mu_r + @test parse(Float64, conductor_node["epsI"]) ≈ expected_epsI + @test parse(Float64, conductor_node["Cext"]) ≈ ins_group.shunt_capacitance + @test parse(Float64, conductor_node["Gext"]) ≈ ins_group.shunt_conductance + end + end + println(" All detailed checks passed!") + end +end + +# Run the test +# @run_package_tests filter = ti -> occursin("Export to ATPDraw LCC format", ti.name) + + +@testitem "Export to ATP format" setup=[defaults, cable_system_export, deps_export_atp] begin + + # The ACT and ASSERT parts of your test go here. + # All variables from the snippets are already defined. + + # 1. RUN THE TEST IN A TEMPORARY DIRECTORY + mktempdir() do tmpdir + output_file = joinpath(tmpdir, "atp_export_test.xml") + println(" Exporting ATP XML file to: ", output_file) + Z_matrix = randn(ComplexF64, num_phases, num_phases, length(freqs)) + Y_matrix = randn(ComplexF64, num_phases, num_phases, length(freqs)) + line_params = LineParameters(Z_matrix, Y_matrix) + + # Call the function we want to test + result_path = export_ZY2XML(line_params, problem_atp, file_name=output_file) + + # 2. BASIC FILE CHECKS + @test result_path == output_file + @test isfile(output_file) + @test filesize(output_file) > 100 + + xml_content = read(output_file, String) + @test occursin("", xml_content) + + # 3. XML STRUCTURE AND DATA VALIDATION + println(" Performing XML structure checks via XPath...") + xml_doc = readxml(output_file) + root_node = root(xml_doc) + + @test nodename(root_node) == "ZY" + @test parse(Int, root_node["NumPhases"]) == num_phases + + z_blocks = findall("//Z", root_node) + @test length(z_blocks) == length(freqs) + + # 4. DETAILED DATA VERIFICATION (for the first frequency) + println(" Verifying numerical data for first frequency...") + first_z_block = z_blocks[1] + @test parse(Float64, first_z_block["Freq"]) ≈ freqs[1] + + z_matrix_rows = split(strip(nodecontent(first_z_block)), '\n') + @test length(z_matrix_rows) == num_phases + + first_row_elements = split(z_matrix_rows[1], ',') + @test length(first_row_elements) == num_phases + number_pattern = r"(-?[\d\.]+E[+-]\d+)" + complex_pattern = Regex("$(number_pattern.pattern)([+-][\\d\\.]+E[+-]\\d+)i") + + match_result = match(complex_pattern, first_row_elements[1]) + + if !isnothing(match_result) + # The captures are now guaranteed to be valid Float64 strings + real_part = parse(Float64, match_result.captures[1]) + imag_part = parse(Float64, match_result.captures[2]) + parsed_z11 = complex(real_part, imag_part) + + expected_z11 = Z_matrix[1, 1, 1] + @test parsed_z11 ≈ expected_z11 rtol=1e-12 + end + end +end + + +@run_package_tests filter = ti -> occursin("Export to ATP format", ti.name) \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 6b306050..d8883307 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,4 +9,52 @@ using TestItemRunner using DataFrames end + + +@testsnippet cable_system_export begin + + cables_library = CablesLibrary() + cables_library = load!(cables_library, file_name=joinpath(@__DIR__, "./cable_test.json")) + + # Retrieve the reloaded design + cable_design = collect(values(cables_library.data))[1] + x0, y0 = 0.0, -1.0 + xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.035); + + # Initialize the `LineCableSystem` with the first cable (phase A): + cablepos = CablePosition(cable_design, xa, ya, + Dict("core" => 1, "sheath" => 0, "jacket" => 0)) + cable_system = LineCableSystem("test_cable_sys", 1000.0, cablepos) + + # Add remaining cables (phases B and C): + add!(cable_system, cable_design, xb, yb, + Dict("core" => 2, "sheath" => 0, "jacket" => 0)) + add!(cable_system, cable_design, xc, yc, + Dict("core" => 3, "sheath" => 0, "jacket" => 0)) + + freqs = sort(abs.(randn(3))) + earth_params_atp = EarthModel(freqs, 100.0, 10.0, 1.0) + num_phases = cable_system.num_phases + + # Create minimal mock objects for the other required arguments + problem_atp = LineParametersProblem( + cable_system, + temperature=20.0, # Operating temperature + earth_props=earth_params_atp, + frequencies=freqs, # Frequency for the analysis + ); + +end + +# @testitem "Cable System Export Setup" setup=[defaults, cable_system_export] begin +# # This is an actual test. +# # The code from the `defaults` and `cable_system_export` snippets has already run +# # before this line is executed. + +# # We can add a simple test to ensure a variable from the snippet was created. +# @test @isdefined problem_atp +# @test problem_atp isa LineParametersProblem +# end + +# @run_package_tests filter = ti -> occursin("Cable System Export Setup", ti.name) @run_package_tests \ No newline at end of file From d876136d839f5c2a6e7806312e18fdf3db13b70f Mon Sep 17 00:00:00 2001 From: LucasLessa1 Date: Mon, 25 Aug 2025 20:52:05 -0300 Subject: [PATCH 2/3] refactor(ImportExport): update export_data and read_data function signatures --- src/ImportExport.jl | 3 +-- src/ImportExport/atp.jl | 42 +++++++++++++++++++++++------------------ test/export_xml.jl | 4 ++-- test/runtests.jl | 2 +- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/ImportExport.jl b/src/ImportExport.jl index a604fbee..2e8b4856 100644 --- a/src/ImportExport.jl +++ b/src/ImportExport.jl @@ -26,8 +26,7 @@ module ImportExport # Export public API export export_data -export export_ZY2XML -export read_atp_data +export read_data export save export load! export get diff --git a/src/ImportExport/atp.jl b/src/ImportExport/atp.jl index 78de7e02..39f67d92 100644 --- a/src/ImportExport/atp.jl +++ b/src/ImportExport/atp.jl @@ -16,7 +16,9 @@ This function takes all the system information, cables, ground parameters and fr - The absolute path of the saved file. """ function export_data(::Val{:atp}, - problem::LineParametersProblem; + cable_system::LineCableSystem, + earth_props::EarthModel; + base_freq=f₀, file_name::String="$(cable_system.system_id)_export.xml" )::Union{String,Nothing} @@ -26,10 +28,7 @@ function export_data(::Val{:atp}, end end # --- 1. Setup Constants and Variables --- - cable_system = problem.system - earth_props = problem.earth_props file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - freq = problem.frequencies[1] num_phases = length(cable_system.cables) # Create XML Structure and LCC Component @@ -38,7 +37,7 @@ function export_data(::Val{:atp}, setroot!(doc, project) _set_attributes!(project, Dict("Application" => "ATPDraw", "Version" => "7.3", "VersionXML" => "1")) header = addelement!(project, "header") - _set_attributes!(header, Dict("Timestep" => 1e-6, "Tmax" => 0.1, "XOPT" => 0, "COPT" => 0, "SysFreq" => freq, "TopLeftX" => 200, "TopLeftY" => 0)) + _set_attributes!(header, Dict("Timestep" => 1e-6, "Tmax" => 0.1, "XOPT" => 0, "COPT" => 0, "SysFreq" => base_freq, "TopLeftX" => 200, "TopLeftY" => 0)) objects = addelement!(project, "objects") variables = addelement!(project, "variables") comp = addelement!(objects, "comp") @@ -47,7 +46,7 @@ function export_data(::Val{:atp}, _set_attributes!(comp_content, Dict("PosX" => 280, "PosY" => 360, "NumPhases" => num_phases, "Icon" => "default", "SinglePhaseIcon" => "true")) for side in ["IN", "OUT"]; y0 = -20; for k in 1:num_phases; y0 += 10; node = addelement!(comp_content, "node"); _set_attributes!(node, Dict("Name" => "$side$k", "Value" => "C$(k)$(side=="IN" ? "SND" : "RCV")", "UserNamed" => "true", "Kind" => k, "PosX" => side == "IN" ? -20 : 20, "PosY" => y0, "NamePosX" => 0, "NamePosY" => 0)); end; end soil_rho = earth_props.layers[end].base_rho_g - for (name, value) in [("Length", cable_system.line_length), ("Freq", freq), ("Grnd resis", soil_rho)]; data_node = addelement!(comp_content, "data"); _set_attributes!(data_node, Dict("Name" => name, "Value" => value)); end + for (name, value) in [("Length", cable_system.line_length), ("Freq", base_freq), ("Grnd resis", soil_rho)]; data_node = addelement!(comp_content, "data"); _set_attributes!(data_node, Dict("Name" => name, "Value" => value)); end # Populate the LCC Sub-structure with CORRECTLY Structured Cable Data lcc_node = addelement!(comp, "LCC") @@ -109,14 +108,14 @@ end """ - read_atp_data(filepath::String, cable_system::LineCableSystem) + read_atp_data(file_name::String, cable_system::LineCableSystem) Reads an ATP `.lis` output file, extracts the Ze and Zi matrices, and dynamically reorders them to a grouped-by-phase format based on the provided `cable_system` structure. It correctly handles systems with a variable number of components per cable. # Arguments -- `filepath`: The path to the `.lis` file. +- `file_name`: The path to the `.lis` file. - `cable_system`: The `LineCableSystem` object corresponding to the data in the file. # Returns @@ -125,7 +124,11 @@ structure. It correctly handles systems with a variable number of components per - `nothing`: If the file cannot be found, parsed, or if the matrix dimensions in the file do not match the provided `cable_system` structure. """ -function read_atp_data(filepath::String, cable_system::LineCableSystem)::Union{Array{COMPLEXSCALAR, 2}, Nothing} +function read_data(::Val{:atp}, + cable_system::LineCableSystem, + freq::AbstractFloat; + file_name::String="$(cable_system.system_id)_1.lis" + )::Union{Array{COMPLEXSCALAR, 2}, Nothing} # --- Inner helper function to parse a matrix block from text lines --- function parse_block(block_lines::Vector{String}) data_lines = filter(line -> !isempty(strip(line)), block_lines) @@ -148,8 +151,8 @@ function read_atp_data(filepath::String, cable_system::LineCableSystem)::Union{A end # --- Main Function Logic --- - if !isfile(filepath) @error "File not found: $filepath"; return nothing end - lines = readlines(filepath) + if !isfile(file_name) @error "File not found: $file_name"; return nothing end + lines = readlines(file_name) ze_start_idx = findfirst(occursin.("Earth impedance [Ze]", lines)) zi_start_idx = findfirst(occursin.("Conductor internal impedance [Zi]", lines)) if isnothing(ze_start_idx) || isnothing(zi_start_idx) @error "Could not find Ze/Zi headers."; return nothing end @@ -210,14 +213,17 @@ into a structured XML format for use in other programs. - The absolute path of the saved file. """ -function export_ZY2XML(line_params::LineParameters, - problem::LineParametersProblem; - file_name::String="$(problem.system.system_id)_ZY.xml", -)::Union{String,Nothing} +function export_data(::Val{:atp}, + line_params::LineParameters, + freq::Vector{BASE_FLOAT}; + file_name::String="ZY_export.xml", + cable_system::Union{LineCableSystem,Nothing}=nothing + )::Union{String,Nothing} + # Construct the file name with system_id if cable_system is provided + file_name = isnothing(cable_system) ? file_name : "$(cable_system.system_id)_ZY_export.xml" cable_length = 1.0 atp_format = "G+Bi" - freq_vec = problem.frequencies file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) open(file_name, "w") do fid @@ -227,7 +233,7 @@ function export_ZY2XML(line_params::LineParameters, @printf(fid, "\n", num_phases, cable_length, y_fmt) # --- Z Matrix Printing --- - for (k, freq_val) in enumerate(freq_vec) + for (k, freq_val) in enumerate(freq) @printf(fid, " \n", freq_val) for i in 1:num_phases row_str = join([@sprintf("%.16E%+.16Ei", real(line_params.Z[i, j, k]), imag(line_params.Z[i, j, k])) for j in 1:num_phases], ",") @@ -246,7 +252,7 @@ function export_ZY2XML(line_params::LineParameters, end @printf(fid, " \n") else # Case for "G+Bi" - for (k, freq_val) in enumerate(freq_vec) + for (k, freq_val) in enumerate(freq) @printf(fid, " \n", freq_val) for i in 1:num_phases row_str = join([@sprintf("%.16E%+.16Ei", real(line_params.Y[i, j, k]), imag(line_params.Y[i, j, k])) for j in 1:num_phases], ",") diff --git a/test/export_xml.jl b/test/export_xml.jl index bab9315c..772c4091 100644 --- a/test/export_xml.jl +++ b/test/export_xml.jl @@ -19,7 +19,7 @@ end # 1. ARRANGE & ACT: Run the export in a temporary directory mktempdir() do tmpdir output_file = joinpath(tmpdir, "atp_export_test.xml") - result_path = export_data(Val(:atp), problem_atp, file_name=output_file) + result_path = export_data(Val(:atp), cable_system, earth_props, file_name=output_file) # 2. ASSERT: Basic file checks (unchanged) @test result_path == output_file @@ -112,7 +112,7 @@ end line_params = LineParameters(Z_matrix, Y_matrix) # Call the function we want to test - result_path = export_ZY2XML(line_params, problem_atp, file_name=output_file) + result_path = export_data(Val(:atp), line_params, freqs; file_name=output_file) # 2. BASIC FILE CHECKS @test result_path == output_file diff --git a/test/runtests.jl b/test/runtests.jl index d8883307..3d598049 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,4 +57,4 @@ end # end # @run_package_tests filter = ti -> occursin("Cable System Export Setup", ti.name) -@run_package_tests \ No newline at end of file +# @run_package_tests \ No newline at end of file From b90ba277fc4dda6d3502c7dd3878ae0a08d04035 Mon Sep 17 00:00:00 2001 From: LucasLessa1 Date: Mon, 25 Aug 2025 22:49:42 -0300 Subject: [PATCH 3/3] adding atp.jl --- src/{ => importexport}/atp.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/{ => importexport}/atp.jl (98%) diff --git a/src/atp.jl b/src/importexport/atp.jl similarity index 98% rename from src/atp.jl rename to src/importexport/atp.jl index 39f67d92..5d08a0a2 100644 --- a/src/atp.jl +++ b/src/importexport/atp.jl @@ -269,11 +269,11 @@ function export_data(::Val{:atp}, # Use pretty print option for debugging comparisons if needed # open(filename, "w") do io; prettyprint(io, doc); end if isfile(file_name) - @info "XML file saved to: $(_display_path(file_name))" + @info "XML file saved to: $(file_name)" end return file_name catch e - @error "Failed to write XML file '$(_display_path(file_name))': $(e)" + @error "Failed to write XML file '$(file_name)': $(e)" isa(e, SystemError) && println("SystemError details: ", e.extrainfo) return nothing rethrow(e) # Rethrow to indicate failure clearly