Git Product home page Git Product logo

maximevh / equivalentcircuits.jl Goto Github PK

View Code? Open in Web Editor NEW
21.0 21.0 5.0 292 KB

A julia package to either fit the parameters of a specified equivalent electrical circuit to electrochemical impedance data, or to suggest a plausible circuit configuration for a given set of measurements (either through a comparison of circuits from the literature, or through an evolutionary algorithm approach).

License: MIT License

Julia 100.00%

equivalentcircuits.jl's People

Contributors

ma-sadeghi avatar maximevh avatar sindrezp avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

equivalentcircuits.jl's Issues

Paper citation

Greetings everyone,

I'm currently working on a paper regarding electrocatalysis and am using this package to fit an equivalent circuit for my electrochemical systems. I would like to reference this package in my paper. Could someone direct me to the repository's preferred citation format, or let me know if there's an official way to reference this tool?

Thank you everyone in advance.

Non-deterministic behavior

Hi, I am trying out EquivalentCircuits. I created a circuit of configuration [R1,C2]-[R3,C4]-R5 and created dummy (ideal) impedance values.

If I pass those to parameteroptimisation with the circuit string, it generates correct values.

If I use circuit_evaluation and pass in terminals = "RC", it sometimes gets close [C1,R2]-[C3,R4] (and the dropping of the R5 is excusable due to it's relatively small value in the original model) but if I repeat the execution it might fail with Algorithm did not converge. In all cases I get a series of Initial position cannot be on the boundary of the box. Moving elements to the interior. Element indices affected: [4] prints (although with varying indices).

I can share dummy values and test script if they would be helpful.

Also, is there a preferred forum for questions other than creating issues here? Thanks!

Warburg element

Awesome package. I was waiting for the Julia version of Impedance.py. Any plans for adding a Warburg element?

Happy to contribute as well!

Name of functions and variables

I have a proposal:

Please consider to use more detailed function and variable names.
One example is the term: Circuit
It is either a string (the string representation of the equivalent circuit)
or a mutable struct with three fields.
And it would be nice, if you could spent some time to explain the concept of karva
and the meaning of fitness of a Circuit.

P.S.:
I enjoy to play with your package and I hope you find the time to release the next
version.

How do I add a custom circuit with a CPE element to circuit_evolution() method?

Greetings everyone,

I understand we can use the file Circuitlibrary.csv as an argument for the circuit_evolution() function. Instead, I'm now trying to use a custom circuit to generate the upcoming circuit tree. Here's my attempt:

guess = Vector{Tuple{String, Vector{Float64}}}([
    ("R1-[C2,R3-[P4,R5]]", [10, 20, 30, 0.9, 10]),
])


@time circuit_evolution(
    measurements, frequencies,
    initial_population = guess,
    generations = 50, 
    population_size = 50,
    terminals = "RCP",
)

The error is: BoundsError: attempt to access 5-element Vector{Float64} at index [6]

How do I proceed? Thank you everyone in advance.

Environment: Julia version: v1.9.2

Steps to Reproduce:

Define the initial population as shown above.
Execute the circuit_evolution() function with the specified parameters.

Performance of impedance.py vs. EquivalentCircuits.jl

I compared Impedance.py through PyCall.jl to EquivalentCircuits.jl. Naively I thought that Julia would out-perform Python. This quick test showed a ~25X speed advantage for the Python module. Of course the Julia module does not require the initial guess for circuit components.

image

I did a second test with the example data included in the EquivalentCitcuits.jl Readme.md. The time difference is roughly the same.

image

However, if I enter nonsensical values for the initual guess for the Python module, the fit misses the real data completely.

So I suppose the trade-off here is convenience vs. speed?

Is it on the roadmap to include an optional initial guess for the component values?

Fail to find suitable parameters

If I use your sample impedance data, one suitable equivalent circuit can be defined as:

circuit_model = "[C1,[C2-[R3,C4],R5]]"
circuit_param   = (C1 = 2.322248710116646e-9, C2 = 7.146778669252158e-7, R3 = 8015.389370331851, C4 = 1.6325663887245989e-9, R5 = 5918.9481528813185)

But I do not know what the trick is to find this good optimization result.
The following fails regularly to optimize the parameters in an acceptable quality:

circuit_model = "[C1,[C2-[R3,C4],R5]]"
circuit_params_preset = EquivalentCircuits.parameteroptimisation(circuit_model, fn_data_full)

Do you have an advice?

Algorithm fails to find optimal parameters for a given equivalent circuit model

Hi Maxime,

today I discussed with a colleague data he has measured and fitted with Gamry-Fit-Software.
He proposed a suitable equivalent circuit model and told me that the Gamry software finds a
parameter set that is very well suited to fit the data.
I have put the data and a sample code in the sub-folder examples of my fork of your repository, see
example_iterative_optimization_with_static_equivalent_circuit_model
I hope you like it and it is useful for your work :-)

The output parameters are too small and add user customized threshold?

Hi Maxime,

Hope you are doing well!

Would it be possible for you to add a user-customized threshold for the fitting process? Sometimes I found the default threshold 5e-4 couldn't raise any results for my EIS data. Thus, I think if we can customize that fitting threshold by using an additional argument of the evolution function, that would be great.

Another issue I noticed is the evolution function sometimes give me super small component value (such as 1e-18). Would it be possible to add an argument of the evolution function so that we can control the possible ranges of different types of components?

For the circuit_optimization function, I noticed you added some additional arguments. That's really fantastic. Would it be possible for us to fix (or set initial values of) only one (or several) circuit components instead of setting them all? I noticed the current arguments require the same length of the number of components.

For example: if I want to optimize the parameters for R1-P2-[R3,P4], with the current argument, I need to set x0 = [10,10,0.5,10,10,0.5]. I wonder if I can only pass something like R1_initial = 10 but don't set other components?

Thank you so much for your generous contribution to this awesome package. This is really FANTASTIC!!!

Best,
Runze

Use logging instead of `println`

It'd be great if all the printlns were replaced with proper logging. This is particularly useful when calling EquivalentCircuits.jl from other libraries, to avoid unwanted printing.

Proposal: Optional input parameter Low-Frequency resistance

Hi Maxime,

what about adding a punishment in the quality function for the difference between the measured low-frequency resistance and and the current result for the sum of the resistances?
Let's assume you have:
R1-L2-[R3,P4]-[R5,P6]

then the low frequency resistance should equal to the sum: R_low_frequency = R1+R3+R5

The low frequency should be similar to the resistance that can be calculated according to the
slope of the polarization curve.

And for this optional element in the quality function there should be also included a weighting factor.

What do you think?

Regards,

Stefan

Result `parameteroptimisation` is not type stable

julia> parameteroptimisation(circuit, [2im+1, 3+im, im-2], [0.1, 0.01, 0.001])
5-element Vector{Any}:
 1308.124526858658
    0.009999999998281602
    0.5515147589413701
    0.009999999999998893
    9.999999990871584e8

Also:

  • not sufficient documentation
  • unclear what the parameter vector is (how does this relate to the circuit?)

How to calculate quality of fit result?

I spent some time to figure out, how to compute a quality criteria by means of your functions, and I have the impression I do not understand the meaning of the function: circuitfitness().
Below my sample code, which includes my own approach to calculate a quality criteria. Does my code make sense?
What would be the correct way, if one would like to use your functions?

using EquivalentCircuits, PlotlyJS, Printf, RobustModels

# --- sample impedance data:
impedance_values_ = ComplexF64[5919.90084073586 - 15.794826681804063im, 5919.575521325405 - 32.677443741883025im, 5918.183674897797 - 67.57666460870544im, 
5912.242152808868 - 139.49441081175667im, 5887.119965779756 - 285.73699600024963im, 5785.038233646888 - 566.878749499386im, 5428.935296370544 - 997.1881947423717im, 
4640.2144606930815 - 1257.8277219098052im, 3871.8361085209845 - 978.9656717819273im, 3537.682636142598 - 564.9627167404748im, 3442.9419240480606 - 315.3996363823805im, 
3418.140460121871 - 219.68986957046025im, 3405.513645508888 - 242.57272592660013im, 3373.904450003017 - 396.0671717029891im, 3249.673719526995 - 742.0301829777005im, 
2808.423185495856 - 1305.924162464249im, 1779.4087896271944 - 1698.9660879948128im, 701.9588433822435 - 1361.4674351816855im, 208.28978681589047 - 777.6453690080142im, 
65.93273498232111 - 392.50667235657im]

# --- corresponding frequencies:
frequency_values = [0.1, 0.20691380811147891, 0.42813323987193935, 0.8858667904100828, 1.8329807108324356, 3.792690190732248, 7.847599703514613, 16.237767391887218, 
33.59818286283781, 69.51927961775601, 143.84498882876616, 297.63514416313194, 615.8482110660267, 1274.2749857031336, 2636.650898730358, 5455.5947811685255, 11288.378916846883, 
23357.214690901215, 48329.30238571752, 100000.0]

# --- generate frequency vector with n_elements with the same range as given in the measurement:
n_elements = 100
frequ_vec = exp10.(LinRange(log10(frequency_values[1]), log10(frequency_values[end]), n_elements))


# --- examples of suitable equivalent circuits
nr_best_circuits = 4
if nr_best_circuits == 3
    circuit_model_preset = "[C1,[C2-[R3,C4],R5]]"
    circuit_params_preset   = (C1 = 2.322248710116646e-9, C2 = 7.146778669252158e-7, R3 = 8015.389370331851, C4 = 1.6325663887245989e-9, R5 = 5918.9481528813185)
elseif  nr_best_circuits == 2
    circuit_model_preset = "[C1,R2-[R3,C4]]"
    circuit_params_preset   = (C1 = 0.025036871360843482, R2 = 396.73873944116787, R3 = 2178.061127814435, C4 = 1.1589755057609664e-5)
elseif  nr_best_circuits == 3
    circuit_model_preset = "R1-[C2,R3-[C4,R5]]"
    circuit_params_preset   = (R1 = 20.012355936798915, C2 = 4.000335253194046e-9, R3 = 3400.1181419153604, C4 = 4.0010158529883644e-6, R5 = 2499.9496058123705)
    # circuit_par_preset   = (R1 = 18.133936476549355, C2 = 3.967856543272228e-9, R3 = 3401.2083814207344, C4 = 3.9972681328104986e-6, R5 = 2500.4785300668846)        
elseif  nr_best_circuits == 4
    circuit_model_preset = "[[C1,R2],[C3,R4]-R5]"
    circuit_params_preset   = (C1 = 3.95177182904296e-9, R2 = 12300.52241056916, C3 = 2.075152378369779e-6, R4 = 6687.854083227787, R5 = 4720.304069423031)
else
    error(string("Choise nr_best_circuits = ", nr_best_circuits,"does not exist!"))
end

# --- simulate impedance based on suitable preset of a circuit model and its corresponding parameter-set: 
circfunc_preset = EquivalentCircuits.circuitfunction(circuit_model_preset)
impedance_preset = EquivalentCircuits.simulateimpedance_noiseless(circfunc_preset, circuit_params_preset, frequ_vec)

# **************************************************************************************************************************
# --- function "circuitevolution()" to find a suitable equivalent circuit model and its parameters:
# **************************************************************************************************************************
# # Arguments
# - `generations::Integer=10`: the maximum number of iterations of the evolutionary algorithm.
# - `population_size::Integer=30`: the number of individuals in the population during each iteration.
# - `terminals::String="RCLP"`: the circuit components that are to be included in the circuit identification.
# - `head::Integer=8`: a hyperparameter than controls the maximum considered complexity of the circuits.
# - `cutoff::Float64=0.8`: a hyperparameter that controls the circuit complexity by removing redundant components.
# - `initial_population::Array{Circuit,1}=nothing`:the option to provide an initial population of circuits
#   (obtained by using the loadpopulation function) with which the algorithm starts.
# -------------------------------------------------------------------------------------------------------------------------
terminals_      = "RC"
head_           = 6
equiv_circ_evo  = EquivalentCircuits.circuitevolution(impedance_values_, frequency_values, terminals=terminals_, generations=100, population_size=20, head=head_)
# --- simulate Impedance:
circfunc_evo    = EquivalentCircuits.circuitfunction(equiv_circ_evo.Circuit)
impedance_evo   = EquivalentCircuits.simulateimpedance_noiseless(circfunc_evo, equiv_circ_evo.Parameters, frequ_vec)
# --- Calc quality as the mean of the distances between measured and simulated impedance:
impedance_evo_data_pts = EquivalentCircuits.simulateimpedance_noiseless(circfunc_evo, equiv_circ_evo.Parameters, frequency_values)
Q_              = RobustModels.mean(abs.(impedance_values_ - impedance_evo_data_pts))
println("Q: ", Q_, ", Circuit Model: ", equiv_circ_evo.Circuit)
println(equiv_circ_evo.Parameters)
# --- calc fitness:
karva_str       = EquivalentCircuits.generatekarva(head_, terminals_)
karva_parameters = EquivalentCircuits.karva_parameters(karva_str)
circuit_struct  = EquivalentCircuits.Circuit(karva_str, karva_parameters, nothing)
circuit_parameters, circuit_struct.fitness, param_inds = EquivalentCircuits.circuitfitness(circuit_struct, impedance_values_, frequency_values)
println(string("fitness: ", circuit_struct.fitness))

# --- plot measured against simulated impedance:
function plot_nyquist_comp_preset_evo()
    s_pts_info = Vector{String}(undef, 0)
    for i_ndx in eachindex(frequency_values)
        push!(s_pts_info, @sprintf("#:%i, f=%.2fHz", i_ndx, frequency_values[i_ndx]))
    end
    # ---
    trace_impedance = PlotlyJS.scatter(; x= real(impedance_values_), y=  imag(impedance_values_), name = "measured impedance", text = s_pts_info, mode = "markers")
    trace_simulated_preset  = PlotlyJS.scatter(; x= real(impedance_preset), y= imag(impedance_preset), name = string("preset: ", circuit_model_preset))
    trace_simulated_auto    = PlotlyJS.scatter(; x= real(impedance_evo),    y= imag(impedance_evo),    name = string("evolution: ",   equiv_circ_evo.Circuit))
    # ---
    plt_layout = PlotlyJS.Layout(;
        title_text          = "Sample <-> Preset <-> Evolution",
        xaxis_title_text    = "z<sub>Real</sub>",
        xaxis_dtick         = 1000,
        yaxis_title_text    = "z<sub>Imag</sub>",
        yaxis_dtick         = 1000,
        # --- Fixed Ratio Axes Configuration
        yaxis_scaleanchor   = "x",
        yaxis_scaleratio    = 1,
    )
    return PlotlyJS.Plot([trace_impedance, trace_simulated_preset, trace_simulated_auto], plt_layout)
end

plot_nyquist_comp_preset_evo()

Generate ECM from a given initial circuit guess?

Hi, Thank you for making this wonderful package, it really helped me a lot with EIS analysis.

I noticed that this package allows us to start autonomous ECM generation from a given circuit library file. I'm curious about if there's any possibility that we can assign one or two plausible circuits as the initial guess, and let the program generate ECM based on these circuits so that the final solutions will be close to the initial guess but have some variance. For example, adding a new input parameter into the function circuit_evolution. (circuit_evolution(... , ... , ... , initial_population = 'R1-[C2,L3]')

I was wondering if you'd like to consider adding that to the program. That could be very helpful! If so, I would be very grateful for that.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.