diff --git a/README.md b/README.md index bcc5563..3a5273b 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,12 @@ plot(scatter(x=ω/(2π*1e9), y=angle.(S)), Layout(xaxis_title="Frequency [GHz]", ``` ![](docs/Phase.png) + +## Using with ANSYS Q3D Extractor + +Plain text files containing RLGC parameters exported by ANSYS® Q3D Extractor® +software can be used to construct a `Circuit` object via `Circuit(file_path)`. +Currently only capacitance matrices are supported. + +ANSYS and Q3D Extractor are registered trademarks of ANSYS, Inc. or its +subsidiaries in the United States or other countries. diff --git a/src/AdmittanceModels.jl b/src/AdmittanceModels.jl index 30ada34..9ce94fb 100644 --- a/src/AdmittanceModels.jl +++ b/src/AdmittanceModels.jl @@ -6,5 +6,6 @@ include("circuit.jl") include("pso_model.jl") include("blackbox.jl") include("circuit_components.jl") +include("ansys.jl") end diff --git a/src/ansys.jl b/src/ansys.jl new file mode 100644 index 0000000..155c11e --- /dev/null +++ b/src/ansys.jl @@ -0,0 +1,206 @@ +#= +The functions here are intended to generate AdmittanceModels.Circuit objects +from ANSYS Q3D plain text output files containing RLGC parameters. + +Here's a small example of the expected Q3D file format, valid for ANSYS Electronics Desktop 2016: + +################################################################################################################ +DesignVariation:\$DummyParam1='1' \$DummyParam2='1mm' \$dummy_param3='100e19pF' \$dummy_param4='1.2eUnits' +Setup1:LastAdaptive +Problem Type:C +C Units:farad, G Units:sie +Reduce Matrix:Original +Frequency: 4E+09 Hz + +Capacitance Matrix + net_name_1 net_name_2 +net_name_1 1.1924E-13 -1.3563E-16 +net_name_2 -1.3563E-16 3.7607E-13 + +Conductance Matrix + net_name_1 net_name_2 +net_name_1 2.4489E-09 -2.3722E-12 +net_name_2 -2.3722E-12 7.7123E-09 + +Capacitance Matrix Coupling Coefficient + net_name_1 net_name_2 +net_name_1 1 0.00064047 0.00018631 +net_name_2 0.00064047 1 0.00010129 + +Conductance Matrix Coupling Coefficient + net_name_1 net_name_2 +net_name_1 1 0.00054586 0.00017696 +net_name_2 0.00054586 1 9.2987E-05 +################################################################################################################ + +Each data block like \"Capacitance Matrix\" must start with a line enumerating +the N net names. These are the column labels for the given matrix. The matrix +rows are specified directly below this line. Each of the N row lines start with +a net name to label the row, followed by N space-separated float values for the +matrix elements. We expect these to be strings `parse(Float64, _)` can handle. +=# + +using LinearAlgebra: diagind + +""" + parse_value_and_unit(s::AbstractString) + +Parse strings like "9.9e-9mm" => (9.9e-9, "mm"). +""" +function parse_value_and_unit(s::AbstractString) + # Three capture groups in the regex intended to match + # (±X.X)(e-X)(unit) + m = match(r"(\-?[\d|\.]+)(e-?[\d]+)?(\D*)", s) + exp_str = m[2] == nothing ? "" : m[2] + num_str = m[1] * exp_str + num = parse(Float64, num_str) + unit_str = String(m[3]) + (num, unit_str) +end + +const matrix_types = Dict( + :capacitance => "Capacitance Matrix", + :conductance => "Conductance Matrix", + # :dc_inductance => "DC Inductance Matrix", + # :dc_resistance => "DC Resistance Matrix", + # :ac_inductance => "AC Inductance Matrix", + # :ac_resistance => "AC Resistance Matrix", + # :dc_plus_ac_resistance => "DCPlusAC Resistance Matrix", +) + +const unit_names = Dict( + :capacitance => "C Units", + :conductance => "G Units" +) + +const units_to_multipliers = Dict( + "fF" => 1e-15, + "pF" => 1e-12, + "nF" => 1e-9, + "uF" => 1e-6, + "mF" => 1e-3, + "farad" => 1.0, + "mSie" => 1e-3, + "sie" => 1.0 +) + +""" + parse_q3d_txt(filepath, matrix_type::Symbol) + +Parses a plain text file generated by ANSYS Q3D containing RLGC simulation output. + +# Args +- `q3d_file`: the path to the text file. +- `matrix_type`: A `Symbol` indicating the type of data to read. Currently, + symbols in $(collect(keys(matrix_types))) are the only available choices, + and only the units in $(collect(keys(units_to_multipliers))) are handled. + +# Returns +- A tuple `(design_variation, net_names, matrix)`, where: + - `design_variation` is a `Dict` of design variables + - `net_names` is a `Vector` of net names (strings) + - `matrix` is the requested matrix +""" +function parse_q3d_txt(q3d_file, matrix_type::Symbol) + !haskey(matrix_types, matrix_type) && error("matrix type not implemented.") + + matrix_name = matrix_types[matrix_type] + unit_name = unit_names[matrix_type] + + # Windows uses \r\n for newlines, where Linux just uses \n. + # We work around that difference here by deleting all carriage return (\r) + # characters from the file. + linesep = "\n" + file_text = replace(read(q3d_file, String), "\r" => "") + file_chunks = split(file_text, linesep * linesep) + @assert length(file_chunks) > 1 "expected more than one block in Q3D plain text file." + + is_chunk(header) = chunk -> startswith(chunk, header) + function get_chunk(header) + # the chunk may be missing, guard against errors + idx = findfirst(is_chunk(header), file_chunks) + !isnothing(idx) && return file_chunks[idx] + return nothing + end + get_chunk_line(chunk, line_num) = split(chunk, linesep)[line_num] + + # The following parses lines like: + # DesignVariation:var1='12' var2='1e-05' var3='123um' + # into Dict("var1" => (12, ""), + # "var2" => (1e-5, ""), + # "var3" => (123, "um")) + design_variation_chunk = get_chunk("DesignVariation") + design_variation = if !isnothing(design_variation_chunk) + _, design_variation_data_str = split(get_chunk_line(design_variation_chunk, 1), ":") + design_strings = split(design_variation_data_str) + parse_design_kv((k, v)) = String(k) => parse_value_and_unit(strip(v, ['\''])) + Dict(parse_design_kv.(split.(design_strings, ['=']))) + else + Dict{String, Tuple{Float64, String}}() + end + + local unit_multiplier + for l in split(file_chunks[1], linesep) + if occursin("Units", l) + unit = Dict(split.(split(l, ", "), ':'))[unit_name] + !haskey(units_to_multipliers, unit) && error("unit $unit not implemented.") + unit_multiplier = units_to_multipliers[unit] + break + end + end + (@isdefined unit_multiplier) || error("units not given in Q3D plain text file.") + + # Don't forget to include the line separator here. + # We typically have matrix_name = "Capacitance Matrix" + # But, these files can have a heading like "Capacitance Matrix Coupling Coefficient" + matrix_chunk = get_chunk(matrix_name * linesep) + net_names = String.(split(get_chunk_line(matrix_chunk, 2))) + + get_matrix_row(row_idx) = parse.(Float64, + split(get_chunk_line(matrix_chunk, 2 + row_idx))[2:end]) .* unit_multiplier + matrix = reduce(hcat, get_matrix_row.(1:length(net_names))) + + (design_variation, net_names, matrix) +end + +""" + Circuit(q3d_file; matrix_types = [:capacitance]) + +Return an `AdmittanceModels.Circuit` from a plain text file generated by +ANSYS Q3D containing RLGC simulation output. + +# Args +- `q3d_file`: the path to the text file. +- `matrix_type`: A list of symbols indicating which type of matrix data to read. + Currently, symbols in $(collect(keys(matrix_types))) are the only available + choices, and only the units in $(collect(keys(units_to_multipliers))) are handled. +""" +function Circuit(q3d_file; matrix_types = [:capacitance]) + parse_dict = Dict(t => parse_q3d_txt(q3d_file, t) for t ∈ matrix_types) + + # Check that all matrix types are defined over the same set of nets + # `take_only` asserts the iterator it's passed has only one element + nets = take_only(collect(Set(map(t -> t[2], values(parse_dict))))) + matrix_dict = Dict(t => parse_dict[t][3] for t ∈ matrix_types) + + zero_mat() = zeros(length(nets), length(nets)) + + # The C and G matrices expected in AdmittanceModels.Circuit are the same + # as those parsed from Q3D, except with all diagonal entries set to 0 and + # all values made positive. + function prep_matrix(matrix_type) + x = get(matrix_dict, matrix_type, zero_mat()) + x[diagind(x)] .= 0. + abs.(x) + end + + k = zero_mat() + g = prep_matrix(:conductance) + c = prep_matrix(:capacitance) + return Circuit(k, g, c, nets) +end + +function take_only(xs) + @assert length(xs) == 1 "Expected $xs to have one element." + return xs[1] +end diff --git a/test/data/dummy_gc_1.txt b/test/data/dummy_gc_1.txt new file mode 100755 index 0000000..3b0dd68 --- /dev/null +++ b/test/data/dummy_gc_1.txt @@ -0,0 +1,48 @@ +DesignVariation:dummy1='-1.23' dummy2='9e-07mm' dummy3='-1.0e21pF' dummy4='1.2eunits' +Setup1:LastAdaptive +Problem Type:C +C Units:pF, G Units:mSie +Reduce Matrix:Original +Frequency: 1E+009 Hz + +Capacitance Matrix + net1 net2 net3 net4 +net1 0.011991 -0.0017236 -0.0035891 -0.00021927 +net2 -0.0017236 0.012106 -0.0045205 -0.00013048 +net3 -0.0035891 -0.0045205 0.04501 -0.00043332 +net4 -0.00021927 -0.00013048 -0.00043332 0.0037523 + +Capacitance Matrix Coupling Coefficient + net1 net2 net3 net4 +net1 1 0.14306 0.15449 0.032688 +net2 0.14306 1 0.19366 0.019359 +net3 0.15449 0.19366 1 0.033343 +net4 0.032688 0.019359 0.033343 1 + +Spice Capacitance Matrix + net1 net2 net3 net4 +net1 0.0064594 0.0017236 0.0035891 0.00021927 +net2 0.0017236 0.0057315 0.0045205 0.00013048 +net3 0.0035891 0.0045205 0.036467 0.00043332 +net4 0.00021927 0.00013048 0.00043332 0.0029693 + +Conductance Matrix + net1 net2 net3 net4 +net1 0 0 0 0 +net2 0 0 0 0 +net3 0 0 0 0 +net4 0 0 0 0 + +Conductance Matrix Coupling Coefficient + net1 net2 net3 net4 +net1 0 0 0 0 +net2 0 0 0 0 +net3 0 0 0 0 +net4 0 0 0 0 + +Spice Conductance Matrix + net1 net2 net3 net4 +net1 0 -0 -0 -0 +net2 -0 0 -0 -0 +net3 -0 -0 0 -0 +net4 -0 -0 -0 0 diff --git a/test/data/dummy_gc_2.txt b/test/data/dummy_gc_2.txt new file mode 100755 index 0000000..9fb3dea --- /dev/null +++ b/test/data/dummy_gc_2.txt @@ -0,0 +1,48 @@ +Setup1:LastAdaptive +Problem Type:C +C Units:pF, G Units:kSie +Reduce Matrix:Original +Frequency: 1E+009 Hz + +Capacitance Matrix + net1 net2 net3 net4 +net1 0.011991 -0.0017236 -0.0035891 -0.00021927 +net2 -0.0017236 0.012106 -0.0045205 -0.00013048 +net3 -0.0035891 -0.0045205 0.04501 -0.00043332 +net4 -0.00021927 -0.00013048 -0.00043332 0.0037523 + +Capacitance Matrix Coupling Coefficient + net1 net2 net3 net4 +net1 1 0.14306 0.15449 0.032688 +net2 0.14306 1 0.19366 0.019359 +net3 0.15449 0.19366 1 0.033343 +net4 0.032688 0.019359 0.033343 1 + +Spice Capacitance Matrix + net1 net2 net3 net4 +net1 0.0064594 0.0017236 0.0035891 0.00021927 +net2 0.0017236 0.0057315 0.0045205 0.00013048 +net3 0.0035891 0.0045205 0.036467 0.00043332 +net4 0.00021927 0.00013048 0.00043332 0.0029693 + +Conductance Matrix + net1 net2 net3 net4 +net1 0 0 0 0 +net2 0 0 0 0 +net3 0 0 0 0 +net4 0 0 0 0 + +Conductance Matrix Coupling Coefficient + net1 net2 net3 net4 +net1 0 0 0 0 +net2 0 0 0 0 +net3 0 0 0 0 +net4 0 0 0 0 + +Spice Conductance Matrix + net1 net2 net3 net4 +net1 0 -0 -0 -0 +net2 -0 0 -0 -0 +net3 -0 -0 0 -0 +net4 -0 -0 -0 0 + diff --git a/test/data/dummy_gc_3.txt b/test/data/dummy_gc_3.txt new file mode 100644 index 0000000..70d7f9a --- /dev/null +++ b/test/data/dummy_gc_3.txt @@ -0,0 +1,46 @@ +Setup1:LastAdaptive +Problem Type:C +Reduce Matrix:Original +Frequency: 1E+009 Hz + +Capacitance Matrix + net1 net2 net3 net4 +net1 0.011991 -0.0017236 -0.0035891 -0.00021927 +net2 -0.0017236 0.012106 -0.0045205 -0.00013048 +net3 -0.0035891 -0.0045205 0.04501 -0.00043332 +net4 -0.00021927 -0.00013048 -0.00043332 0.0037523 + +Capacitance Matrix Coupling Coefficient + net1 net2 net3 net4 +net1 1 0.14306 0.15449 0.032688 +net2 0.14306 1 0.19366 0.019359 +net3 0.15449 0.19366 1 0.033343 +net4 0.032688 0.019359 0.033343 1 + +Spice Capacitance Matrix + net1 net2 net3 net4 +net1 0.0064594 0.0017236 0.0035891 0.00021927 +net2 0.0017236 0.0057315 0.0045205 0.00013048 +net3 0.0035891 0.0045205 0.036467 0.00043332 +net4 0.00021927 0.00013048 0.00043332 0.0029693 + +Conductance Matrix + net1 net2 net3 net4 +net1 0 0 0 0 +net2 0 0 0 0 +net3 0 0 0 0 +net4 0 0 0 0 + +Conductance Matrix Coupling Coefficient + net1 net2 net3 net4 +net1 0 0 0 0 +net2 0 0 0 0 +net3 0 0 0 0 +net4 0 0 0 0 + +Spice Conductance Matrix + net1 net2 net3 net4 +net1 0 -0 -0 -0 +net2 -0 0 -0 -0 +net3 -0 -0 0 -0 +net4 -0 -0 -0 0 diff --git a/test/data/empty.txt b/test/data/empty.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/data/one_block.txt b/test/data/one_block.txt new file mode 100644 index 0000000..a311d1b --- /dev/null +++ b/test/data/one_block.txt @@ -0,0 +1,5 @@ +Setup1:LastAdaptive +Problem Type:C +C Units:pF, G Units:mSie +Reduce Matrix:Original +Frequency: 1E+009 Hz diff --git a/test/runtests.jl b/test/runtests.jl index 3f11999..1010338 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,5 +4,6 @@ include("test_circuit.jl") include("test_pso_model.jl") include("test_blackbox.jl") include("test_circuit_components.jl") +include("test_ansys.jl") include("../paper/radiative_loss.jl") include("../paper/hybridization.jl") diff --git a/test/test_ansys.jl b/test/test_ansys.jl new file mode 100644 index 0000000..4d12e2f --- /dev/null +++ b/test/test_ansys.jl @@ -0,0 +1,47 @@ +@testset "Q3D plain text parsing" begin + data(x) = joinpath(@__DIR__, "data", x) + + # file should satisfy some basic checks + @test_throws AssertionError Circuit(data("empty.txt")) + @test_throws AssertionError Circuit(data("one_block.txt")) + + # test expected output + d = data("dummy_gc_1.txt") + @test Circuit(d).vertices == ["net1", "net2", "net3", "net4"] + + design_variation, vertex_names, capacitance_matrix = + AdmittanceModels.parse_q3d_txt(d, :capacitance) + + # test negative val + no units + @test design_variation["dummy1"] == (-1.23, "") + + # test negative exponent + units + @test design_variation["dummy2"] == (9.0e-7, "mm") + + # test negative val, positive exponent + units + @test design_variation["dummy3"] == (-1.0e21, "pF") + + # test units that start with "e" (the parser should look for things like 'e09' or 'e-12') + @test design_variation["dummy4"] == (1.2, "eunits") + + # unit not implemented + @test_throws ErrorException try + Circuit(data("dummy_gc_2.txt"); matrix_types = [:conductance]) + catch e + buf = IOBuffer() + showerror(buf, e) + message = String(take!(buf)) + @test occursin("not implemented", message) + rethrow(e) + end + + @test_throws ErrorException try + Circuit(data("dummy_gc_3.txt"); matrix_types = [:conductance]) + catch e + buf = IOBuffer() + showerror(buf, e) + message = String(take!(buf)) + @test occursin("units not given", message) + rethrow(e) + end +end