impedance-extend 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. impedance_extend-1.0.0/LICENSE +21 -0
  2. impedance_extend-1.0.0/PKG-INFO +120 -0
  3. impedance_extend-1.0.0/README.md +89 -0
  4. impedance_extend-1.0.0/impedance_extend/__init__.py +1 -0
  5. impedance_extend-1.0.0/impedance_extend/models/__init__.py +0 -0
  6. impedance_extend-1.0.0/impedance_extend/models/circuits/__init__.py +2 -0
  7. impedance_extend-1.0.0/impedance_extend/models/circuits/circuits.py +440 -0
  8. impedance_extend-1.0.0/impedance_extend/models/circuits/elements.py +449 -0
  9. impedance_extend-1.0.0/impedance_extend/models/circuits/fitting.py +839 -0
  10. impedance_extend-1.0.0/impedance_extend/preprocessing.py +508 -0
  11. impedance_extend-1.0.0/impedance_extend/tests/__init__.py +4 -0
  12. impedance_extend-1.0.0/impedance_extend/tests/test_circuit_elements.py +218 -0
  13. impedance_extend-1.0.0/impedance_extend/tests/test_circuits.py +221 -0
  14. impedance_extend-1.0.0/impedance_extend/tests/test_fitting.py +390 -0
  15. impedance_extend-1.0.0/impedance_extend/tests/test_model_io.py +33 -0
  16. impedance_extend-1.0.0/impedance_extend/tests/test_preprocessing.py +518 -0
  17. impedance_extend-1.0.0/impedance_extend/tests/test_validation.py +237 -0
  18. impedance_extend-1.0.0/impedance_extend/tests/test_visualization.py +77 -0
  19. impedance_extend-1.0.0/impedance_extend/validation.py +310 -0
  20. impedance_extend-1.0.0/impedance_extend/visualization.py +332 -0
  21. impedance_extend-1.0.0/impedance_extend.egg-info/PKG-INFO +120 -0
  22. impedance_extend-1.0.0/impedance_extend.egg-info/SOURCES.txt +25 -0
  23. impedance_extend-1.0.0/impedance_extend.egg-info/dependency_links.txt +1 -0
  24. impedance_extend-1.0.0/impedance_extend.egg-info/requires.txt +7 -0
  25. impedance_extend-1.0.0/impedance_extend.egg-info/top_level.txt +1 -0
  26. impedance_extend-1.0.0/setup.cfg +4 -0
  27. impedance_extend-1.0.0/setup.py +28 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 impedance.py developers.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: impedance_extend
3
+ Version: 1.0.0
4
+ Summary: A package for analyzing electrochemical impedance data (with support for GA, PSO and least_squares or combinations thereof)
5
+ Home-page: https://impedancepy.readthedocs.io/en/latest/
6
+ Author: impedance_extend.py developers
7
+ Author-email: prof.krishna.v@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: ~=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: altair>=3.0
15
+ Requires-Dist: matplotlib>=3.5
16
+ Requires-Dist: numpy>=1.22.4
17
+ Requires-Dist: scipy>=1.0
18
+ Requires-Dist: pandas
19
+ Requires-Dist: pygad>=3.6.0
20
+ Requires-Dist: pyswarms>=1.3
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ [![DOI]] ![GitHub release](https://img.shields.io/github/release/k-vijayaraghavan/impedance_extend.py)
33
+
34
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/impedance_extend?style=flat-square) [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)
35
+
36
+ [![Build Status](https://github.com/k-vijayaraghavan/impedance_extend.py/actions/workflows/ci.yml/badge.svg)](https://github.com/k-vijayaraghavan/impedance_extend.py/actions) [![Documentation Status](https://readthedocs.org/projects/impedancepy/badge/?version=latest&kill_cache=1)](https://impedancepy.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/ECSHackWeek/impedance.py/badge.svg?branch=master&kill_cache=1)](https://coveralls.io/github/ECSHackWeek/impedance.py?branch=master)
37
+
38
+ impedance_extend.py
39
+ ------------
40
+
41
+ `impedance_extend.py` is a Python package for making electrochemical impedance spectroscopy (EIS) analysis reproducible and easy-to-use. It extends [impedance.py](https://github.com/ECSHackWeek/impedance.py) by adding least_squares, GA (using pygad), and PSO (using pyswarms) as additional optimization methods. `impedance_extend.py` additionally supports sequential optimization (such as running GA/PSO followed by least_squares), and adding soft-constraints (such as ensure R1 < R2 or R1*C1 < 1 etc.).
42
+
43
+ Aiming to create a consistent, [scikit-learn-like API](https://arxiv.org/abs/1309.0238) for impedance analysis, impedance.py contains modules for data preprocessing, validation, model fitting, and visualization.
44
+
45
+ For a little more in-depth discussion of the package background and capabilities, check out our [Journal of Open Source Software paper](https://joss.theoj.org/papers/10.21105/joss.02349).
46
+
47
+ If you have a feature request or find a bug, please [file an issue](https://github.com/k-vijayaraghavan/impedance_extend.py/issues) or, better yet, make the code improvements and [submit a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)! The goal is to build an open-source tool that the entire impedance community can improve and use!
48
+
49
+ ### Installation
50
+
51
+ The easiest way to install impedance_extend.py is from [PyPI](https://pypi.org/project/impedance_extend/) using pip.
52
+
53
+ ```bash
54
+ pip install impedance_extend
55
+ ```
56
+
57
+ See [Getting started with impedance.py](https://impedancepy.readthedocs.io/en/latest/getting-started.html) for instructions on getting started from scratch.
58
+
59
+ #### Dependencies
60
+
61
+ impedance.py requires:
62
+
63
+ - Python (>=3.10)
64
+ - SciPy (>=1.0)
65
+ - NumPy (>=1.22.4)
66
+ - Matplotlib (>=3.5)
67
+ - Altair (>=3.0)
68
+ - Pandas
69
+ - pygad>=3.6.0
70
+ - pyswarms>=1.3
71
+
72
+ Several example notebooks are provided in the `docs/source/examples/` directory. Opening these will require Jupyter notebook or Jupyter lab.
73
+
74
+ #### Examples and Documentation
75
+
76
+ Several examples can be found in the `docs/source/examples/` directory (the [Fitting impedance spectra notebook](https://impedancepy.readthedocs.io/en/latest/examples/fitting_example.html) is a great place to start) and the documentation can be found at [impedancepy.readthedocs.io](https://impedancepy.readthedocs.io/en/latest/).
77
+
78
+ ## Citing impedance.py
79
+
80
+ [![DOI](https://joss.theoj.org/papers/10.21105/joss.02349/status.svg)](https://doi.org/10.21105/joss.02349)
81
+
82
+ If you use impedance.py in published work, please consider citing https://joss.theoj.org/papers/10.21105/joss.02349 as
83
+
84
+ ```bash
85
+ @article{Murbach2020,
86
+ doi = {10.21105/joss.02349},
87
+ url = {https://doi.org/10.21105/joss.02349},
88
+ year = {2020},
89
+ publisher = {The Open Journal},
90
+ volume = {5},
91
+ number = {52},
92
+ pages = {2349},
93
+ author = {Matthew D. Murbach and Brian Gerwe and Neal Dawson-Elli and Lok-kun Tsui},
94
+ title = {impedance.py: A Python package for electrochemical impedance analysis},
95
+ journal = {Journal of Open Source Software}
96
+ }
97
+ ```
98
+
99
+ ## Contributors ✨
100
+
101
+ This project was adapted from a fork of [impedance.py] (https://github.com/k-vijayaraghavan/impedance_extend.py). [Impedance.py] (https://github.com/k-vijayaraghavan/impedance_extend.py) was started at the [2018 Electrochemical Society (ECS) Hack Week in Seattle](https://www.electrochem.org/233/hack-week) and has benefited from a community of users and contributors since. Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
102
+
103
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
104
+ <!-- prettier-ignore-start -->
105
+ <!-- markdownlint-disable -->
106
+ <table>
107
+ <tbody>
108
+ <tr>
109
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/k-vijayaraghavan"><img src="https://avatars.githubusercontent.com/u/62916263?v=4" width="100px;" alt="Krishna Vijayaraghavan"/><br /><sub><b>Krishna Vijayaraghavan</b></sub></a><br /><a href="https://github.com/k-vijayaraghavan/impedance_extend.py/commits?author=k-vijayaraghavan" title="Code">💻</a></td>
110
+ </tr>
111
+ <tr>
112
+ </tbody>
113
+ </table>
114
+
115
+ <!-- markdownlint-restore -->
116
+ <!-- prettier-ignore-end -->
117
+
118
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
119
+
120
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -0,0 +1,89 @@
1
+ [![DOI]] ![GitHub release](https://img.shields.io/github/release/k-vijayaraghavan/impedance_extend.py)
2
+
3
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/impedance_extend?style=flat-square) [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)
4
+
5
+ [![Build Status](https://github.com/k-vijayaraghavan/impedance_extend.py/actions/workflows/ci.yml/badge.svg)](https://github.com/k-vijayaraghavan/impedance_extend.py/actions) [![Documentation Status](https://readthedocs.org/projects/impedancepy/badge/?version=latest&kill_cache=1)](https://impedancepy.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/ECSHackWeek/impedance.py/badge.svg?branch=master&kill_cache=1)](https://coveralls.io/github/ECSHackWeek/impedance.py?branch=master)
6
+
7
+ impedance_extend.py
8
+ ------------
9
+
10
+ `impedance_extend.py` is a Python package for making electrochemical impedance spectroscopy (EIS) analysis reproducible and easy-to-use. It extends [impedance.py](https://github.com/ECSHackWeek/impedance.py) by adding least_squares, GA (using pygad), and PSO (using pyswarms) as additional optimization methods. `impedance_extend.py` additionally supports sequential optimization (such as running GA/PSO followed by least_squares), and adding soft-constraints (such as ensure R1 < R2 or R1*C1 < 1 etc.).
11
+
12
+ Aiming to create a consistent, [scikit-learn-like API](https://arxiv.org/abs/1309.0238) for impedance analysis, impedance.py contains modules for data preprocessing, validation, model fitting, and visualization.
13
+
14
+ For a little more in-depth discussion of the package background and capabilities, check out our [Journal of Open Source Software paper](https://joss.theoj.org/papers/10.21105/joss.02349).
15
+
16
+ If you have a feature request or find a bug, please [file an issue](https://github.com/k-vijayaraghavan/impedance_extend.py/issues) or, better yet, make the code improvements and [submit a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)! The goal is to build an open-source tool that the entire impedance community can improve and use!
17
+
18
+ ### Installation
19
+
20
+ The easiest way to install impedance_extend.py is from [PyPI](https://pypi.org/project/impedance_extend/) using pip.
21
+
22
+ ```bash
23
+ pip install impedance_extend
24
+ ```
25
+
26
+ See [Getting started with impedance.py](https://impedancepy.readthedocs.io/en/latest/getting-started.html) for instructions on getting started from scratch.
27
+
28
+ #### Dependencies
29
+
30
+ impedance.py requires:
31
+
32
+ - Python (>=3.10)
33
+ - SciPy (>=1.0)
34
+ - NumPy (>=1.22.4)
35
+ - Matplotlib (>=3.5)
36
+ - Altair (>=3.0)
37
+ - Pandas
38
+ - pygad>=3.6.0
39
+ - pyswarms>=1.3
40
+
41
+ Several example notebooks are provided in the `docs/source/examples/` directory. Opening these will require Jupyter notebook or Jupyter lab.
42
+
43
+ #### Examples and Documentation
44
+
45
+ Several examples can be found in the `docs/source/examples/` directory (the [Fitting impedance spectra notebook](https://impedancepy.readthedocs.io/en/latest/examples/fitting_example.html) is a great place to start) and the documentation can be found at [impedancepy.readthedocs.io](https://impedancepy.readthedocs.io/en/latest/).
46
+
47
+ ## Citing impedance.py
48
+
49
+ [![DOI](https://joss.theoj.org/papers/10.21105/joss.02349/status.svg)](https://doi.org/10.21105/joss.02349)
50
+
51
+ If you use impedance.py in published work, please consider citing https://joss.theoj.org/papers/10.21105/joss.02349 as
52
+
53
+ ```bash
54
+ @article{Murbach2020,
55
+ doi = {10.21105/joss.02349},
56
+ url = {https://doi.org/10.21105/joss.02349},
57
+ year = {2020},
58
+ publisher = {The Open Journal},
59
+ volume = {5},
60
+ number = {52},
61
+ pages = {2349},
62
+ author = {Matthew D. Murbach and Brian Gerwe and Neal Dawson-Elli and Lok-kun Tsui},
63
+ title = {impedance.py: A Python package for electrochemical impedance analysis},
64
+ journal = {Journal of Open Source Software}
65
+ }
66
+ ```
67
+
68
+ ## Contributors ✨
69
+
70
+ This project was adapted from a fork of [impedance.py] (https://github.com/k-vijayaraghavan/impedance_extend.py). [Impedance.py] (https://github.com/k-vijayaraghavan/impedance_extend.py) was started at the [2018 Electrochemical Society (ECS) Hack Week in Seattle](https://www.electrochem.org/233/hack-week) and has benefited from a community of users and contributors since. Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
71
+
72
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
73
+ <!-- prettier-ignore-start -->
74
+ <!-- markdownlint-disable -->
75
+ <table>
76
+ <tbody>
77
+ <tr>
78
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/k-vijayaraghavan"><img src="https://avatars.githubusercontent.com/u/62916263?v=4" width="100px;" alt="Krishna Vijayaraghavan"/><br /><sub><b>Krishna Vijayaraghavan</b></sub></a><br /><a href="https://github.com/k-vijayaraghavan/impedance_extend.py/commits?author=k-vijayaraghavan" title="Code">💻</a></td>
79
+ </tr>
80
+ <tr>
81
+ </tbody>
82
+ </table>
83
+
84
+ <!-- markdownlint-restore -->
85
+ <!-- prettier-ignore-end -->
86
+
87
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
88
+
89
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,2 @@
1
+ # move the circuits file up a namespace for readability
2
+ from .circuits import * # noqa: F401, F403
@@ -0,0 +1,440 @@
1
+ from .fitting import circuit_fit, buildCircuit
2
+ from .fitting import calculateCircuitLength, check_and_eval
3
+ from impedance_extend.visualization import plot_altair, plot_bode, \
4
+ plot_nyquist
5
+ from .elements import circuit_elements, get_element_from_name
6
+
7
+ import json
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import warnings
11
+
12
+
13
+ class BaseCircuit:
14
+ """ Base class for equivalent circuit models """
15
+ def __init__(self, initial_guess=[], constants=None, name=None):
16
+ """ Base constructor for any equivalent circuit model
17
+
18
+ Parameters
19
+ ----------
20
+ initial_guess: numpy array
21
+ Initial guess of the circuit values
22
+
23
+ constants : dict, optional
24
+ Parameters and values to hold constant during fitting
25
+ (e.g. {"R0": 0.1})
26
+
27
+ name : str, optional
28
+ Name for the circuit
29
+ """
30
+
31
+ # if supplied, check that initial_guess is valid and store
32
+ initial_guess = [x for x in initial_guess if x is not None]
33
+ for i in initial_guess:
34
+ if not isinstance(i, (float, int, np.int32, np.float64)):
35
+ raise TypeError(f'value {i} in initial_guess is not a number')
36
+
37
+ # initalize class attributes
38
+ self.initial_guess = initial_guess
39
+ if constants is not None:
40
+ self.constants = constants
41
+ else:
42
+ self.constants = {}
43
+ self.name = name
44
+
45
+ # initialize fit parameters and confidence intervals
46
+ self.parameters_ = None
47
+ self.conf_ = None
48
+
49
+ def __eq__(self, other):
50
+ if self.__class__ == other.__class__:
51
+ matches = []
52
+ for key, value in self.__dict__.items():
53
+ if isinstance(value, np.ndarray):
54
+ matches.append((value == other.__dict__[key]).all())
55
+ else:
56
+ matches.append(value == other.__dict__[key])
57
+ return np.array(matches).all()
58
+ else:
59
+ raise TypeError('Comparing object is not of the same type.')
60
+
61
+ def fit(self, frequencies, impedance, bounds=None,
62
+ weight_by_modulus=False, **kwargs):
63
+ """ Fit the circuit model
64
+
65
+ Parameters
66
+ ----------
67
+ frequencies: numpy array
68
+ Frequencies
69
+
70
+ impedance: numpy array of dtype 'complex128'
71
+ Impedance values to fit
72
+
73
+ bounds: 2-tuple of array_like, optional
74
+ Lower and upper bounds on parameters. Defaults to bounds on all
75
+ parameters of 0 and np.inf, except the CPE alpha
76
+ which has an upper bound of 1
77
+
78
+ weight_by_modulus : bool, optional
79
+ Uses the modulus of each data (|Z|) as the weighting factor.
80
+ Standard weighting scheme when experimental variances are
81
+ unavailable. Only applicable when global_opt = False
82
+
83
+ kwargs :
84
+ Keyword arguments passed to
85
+ impedance_extend.models.circuits.fitting.circuit_fit,
86
+ and subsequently to scipy.optimize.curve_fit
87
+ or scipy.optimize.basinhopping
88
+
89
+ Returns
90
+ -------
91
+ self: returns an instance of self
92
+
93
+ """
94
+ frequencies = np.array(frequencies, dtype=float)
95
+ impedance = np.array(impedance, dtype=complex)
96
+
97
+ if len(frequencies) != len(impedance):
98
+ raise TypeError('length of frequencies and impedance do not match')
99
+
100
+ if self.initial_guess != []:
101
+ parameters, conf = circuit_fit(frequencies, impedance,
102
+ self.circuit, self.initial_guess,
103
+ constants=self.constants,
104
+ bounds=bounds,
105
+ weight_by_modulus=weight_by_modulus,
106
+ **kwargs)
107
+ self.parameters_ = parameters
108
+ if conf is not None:
109
+ self.conf_ = conf
110
+ else:
111
+ raise ValueError('No initial guess supplied')
112
+
113
+ return self
114
+
115
+ def _is_fit(self):
116
+ """ check if model has been fit (parameters_ is not None) """
117
+ if self.parameters_ is not None:
118
+ return True
119
+ else:
120
+ return False
121
+
122
+ def predict(self, frequencies, use_initial=False):
123
+ """ Predict impedance using an equivalent circuit model
124
+
125
+ Parameters
126
+ ----------
127
+ frequencies: array-like of numeric type
128
+ use_initial: boolean
129
+ If true and the model was previously fit use the initial
130
+ parameters instead
131
+
132
+ Returns
133
+ -------
134
+ impedance: ndarray of dtype 'complex128'
135
+ Predicted impedance at each frequency
136
+ """
137
+ frequencies = np.array(frequencies, dtype=float)
138
+ buildCircuit_text = buildCircuit(
139
+ self.circuit, constants=self.constants, eval_string='', index=0)[0]
140
+ builtCircuit = eval('lambda frequencies,parameters : ' +
141
+ buildCircuit_text, circuit_elements)
142
+ if self._is_fit() and not use_initial:
143
+ return builtCircuit(frequencies, self.parameters_)
144
+ else:
145
+ warnings.warn("Simulating circuit based on initial parameters")
146
+ return builtCircuit(frequencies, self.initial_guess)
147
+
148
+ def get_param_names(self):
149
+ """ Converts circuit string to names and units """
150
+
151
+ # parse the element names from the circuit string
152
+ names = self.circuit.replace('p', '').replace('(', '').replace(')', '')
153
+ names = names.replace(',', '-').replace(' ', '').split('-')
154
+
155
+ full_names, all_units = [], []
156
+ for name in names:
157
+ elem = get_element_from_name(name)
158
+ num_params = check_and_eval(elem).num_params
159
+ units = check_and_eval(elem).units
160
+ if num_params > 1:
161
+ for j in range(num_params):
162
+ full_name = '{}_{}'.format(name, j)
163
+ if full_name not in self.constants.keys():
164
+ full_names.append(full_name)
165
+ all_units.append(units[j])
166
+ else:
167
+ if name not in self.constants.keys():
168
+ full_names.append(name)
169
+ all_units.append(units[0])
170
+
171
+ return full_names, all_units
172
+
173
+ def __str__(self):
174
+ """ Defines the pretty printing of the circuit"""
175
+
176
+ to_print = '\n'
177
+ if self.name is not None:
178
+ to_print += 'Name: {}\n'.format(self.name)
179
+ to_print += 'Circuit string: {}\n'.format(self.circuit)
180
+ to_print += "Fit: {}\n".format(self._is_fit())
181
+
182
+ if len(self.constants) > 0:
183
+ to_print += '\nConstants:\n'
184
+ for name, value in self.constants.items():
185
+ elem = get_element_from_name(name)
186
+ units = check_and_eval(elem).units
187
+ if '_' in name and len(units) > 1:
188
+ unit = units[int(name.split('_')[-1])]
189
+ else:
190
+ unit = units[0]
191
+ to_print += ' {:>5} = {:.2e} [{}]\n'.format(name, value, unit)
192
+
193
+ names, units = self.get_param_names()
194
+ to_print += '\nInitial guesses:\n'
195
+ for name, unit, param in zip(names, units, self.initial_guess):
196
+ to_print += ' {:>5} = {:.2e} [{}]\n'.format(name, param, unit)
197
+ if self._is_fit():
198
+ params, confs = self.parameters_, self.conf_
199
+ to_print += '\nFit parameters:\n'
200
+ for name, unit, param, conf in zip(names, units, params, confs):
201
+ to_print += ' {:>5} = {:.2e}'.format(name, param)
202
+ to_print += ' (+/- {:.2e}) [{}]\n'.format(conf, unit)
203
+
204
+ return to_print
205
+
206
+ def plot(self, ax=None, f_data=None, Z_data=None, kind='altair', **kwargs):
207
+ """ visualizes the model and optional data as a nyquist,
208
+ bode, or altair (interactive) plots
209
+
210
+ Parameters
211
+ ----------
212
+ ax: matplotlib.axes
213
+ axes to plot on
214
+ f_data: np.array of type float
215
+ Frequencies of input data (for Bode plots)
216
+ Z_data: np.array of type complex
217
+ Impedance data to plot
218
+ kind: {'altair', 'nyquist', 'bode'}
219
+ type of plot to visualize
220
+
221
+ Other Parameters
222
+ ----------------
223
+ **kwargs : optional
224
+ If kind is 'nyquist' or 'bode', used to specify additional
225
+ `matplotlib.pyplot.Line2D` properties like linewidth,
226
+ line color, marker color, and labels.
227
+ If kind is 'altair', used to specify nyquist height as `size`
228
+
229
+ Returns
230
+ -------
231
+ ax: matplotlib.axes
232
+ axes of the created nyquist plot
233
+ """
234
+
235
+ if kind == 'nyquist':
236
+ if ax is None:
237
+ _, ax = plt.subplots(figsize=(5, 5))
238
+
239
+ if Z_data is not None:
240
+ ax = plot_nyquist(Z_data, ls='', marker='s', ax=ax, **kwargs)
241
+
242
+ if self._is_fit():
243
+ if f_data is not None:
244
+ f_pred = f_data
245
+ else:
246
+ f_pred = np.logspace(5, -3)
247
+
248
+ Z_fit = self.predict(f_pred)
249
+ ax = plot_nyquist(Z_fit, ls='-', marker='', ax=ax, **kwargs)
250
+ return ax
251
+ elif kind == 'bode':
252
+ if ax is None:
253
+ _, ax = plt.subplots(nrows=2, figsize=(5, 5))
254
+
255
+ if f_data is not None:
256
+ f_pred = f_data
257
+ else:
258
+ f_pred = np.logspace(5, -3)
259
+
260
+ if Z_data is not None:
261
+ if f_data is None:
262
+ raise ValueError('f_data must be specified if' +
263
+ ' Z_data for a Bode plot')
264
+ ax = plot_bode(f_data, Z_data, ls='', marker='s',
265
+ axes=ax, **kwargs)
266
+
267
+ if self._is_fit():
268
+ Z_fit = self.predict(f_pred)
269
+ ax = plot_bode(f_pred, Z_fit, ls='-', marker='',
270
+ axes=ax, **kwargs)
271
+ return ax
272
+ elif kind == 'altair':
273
+ plot_dict = {}
274
+
275
+ if Z_data is not None and f_data is not None:
276
+ plot_dict['data'] = {'f': f_data, 'Z': Z_data}
277
+
278
+ if self._is_fit():
279
+ if f_data is not None:
280
+ f_pred = f_data
281
+ else:
282
+ f_pred = np.logspace(5, -3)
283
+
284
+ Z_fit = self.predict(f_pred)
285
+ if self.name is not None:
286
+ name = self.name
287
+ else:
288
+ name = 'fit'
289
+ plot_dict[name] = {'f': f_pred, 'Z': Z_fit, 'fmt': '-'}
290
+
291
+ chart = plot_altair(plot_dict, **kwargs)
292
+ return chart
293
+ else:
294
+ raise ValueError("Kind must be one of 'altair'," +
295
+ f"'nyquist', or 'bode' (received {kind})")
296
+
297
+ def save(self, filepath):
298
+ """ Exports a model to JSON
299
+
300
+ Parameters
301
+ ----------
302
+ filepath: str
303
+ Destination for exporting model object
304
+ """
305
+
306
+ model_string = self.circuit
307
+ model_name = self.name
308
+
309
+ initial_guess = self.initial_guess
310
+
311
+ if self._is_fit():
312
+ parameters_ = list(self.parameters_)
313
+ model_conf_ = list(self.conf_)
314
+
315
+ data_dict = {"Name": model_name,
316
+ "Circuit String": model_string,
317
+ "Initial Guess": initial_guess,
318
+ "Constants": self.constants,
319
+ "Fit": True,
320
+ "Parameters": parameters_,
321
+ "Confidence": model_conf_,
322
+ }
323
+ else:
324
+ data_dict = {"Name": model_name,
325
+ "Circuit String": model_string,
326
+ "Initial Guess": initial_guess,
327
+ "Constants": self.constants,
328
+ "Fit": False}
329
+
330
+ with open(filepath, 'w') as f:
331
+ json.dump(data_dict, f)
332
+
333
+ def load(self, filepath, fitted_as_initial=False):
334
+ """ Imports a model from JSON
335
+
336
+ Parameters
337
+ ----------
338
+ filepath: str
339
+ filepath to JSON file to load model from
340
+
341
+ fitted_as_initial: bool
342
+ If true, loads the model's fitted parameters
343
+ as initial guesses
344
+
345
+ Otherwise, loads the model's initial and
346
+ fitted parameters as a completed model
347
+ """
348
+
349
+ json_data_file = open(filepath, 'r')
350
+ json_data = json.load(json_data_file)
351
+
352
+ model_name = json_data["Name"]
353
+ model_string = json_data["Circuit String"]
354
+ model_initial_guess = json_data["Initial Guess"]
355
+ model_constants = json_data["Constants"]
356
+
357
+ self.initial_guess = model_initial_guess
358
+ self.circuit = model_string
359
+ print(self.circuit)
360
+ self.constants = model_constants
361
+ self.name = model_name
362
+
363
+ if json_data["Fit"]:
364
+ if fitted_as_initial:
365
+ self.initial_guess = np.array(json_data['Parameters'])
366
+ else:
367
+ self.parameters_ = np.array(json_data["Parameters"])
368
+ self.conf_ = np.array(json_data["Confidence"])
369
+
370
+
371
+ class Randles(BaseCircuit):
372
+ """ A Randles circuit model class """
373
+ def __init__(self, CPE=False, **kwargs):
374
+ """ Constructor for the Randles' circuit class
375
+
376
+ Parameters
377
+ ----------
378
+ initial_guess: numpy array
379
+ Initial guess of the circuit values
380
+
381
+ CPE: boolean
382
+ Use a constant phase element instead of a capacitor
383
+ """
384
+ super().__init__(**kwargs)
385
+
386
+ if CPE:
387
+ self.name = 'Randles w/ CPE'
388
+ self.circuit = 'R0-p(R1-Wo1,CPE1)'
389
+ else:
390
+ self.name = 'Randles'
391
+ self.circuit = 'R0-p(R1-Wo1,C1)'
392
+
393
+ circuit_len = calculateCircuitLength(self.circuit)
394
+
395
+ if len(self.initial_guess) + len(self.constants) != circuit_len:
396
+ raise ValueError('The number of initial guesses ' +
397
+ f'({len(self.initial_guess)}) + ' +
398
+ 'the number of constants ' +
399
+ f'({len(self.constants)})' +
400
+ ' must be equal to ' +
401
+ f'the circuit length ({circuit_len})')
402
+
403
+
404
+ class CustomCircuit(BaseCircuit):
405
+ def __init__(self, circuit='', **kwargs):
406
+ """ Constructor for a customizable equivalent circuit model
407
+
408
+ Parameters
409
+ ----------
410
+ initial_guess: numpy array
411
+ Initial guess of the circuit values
412
+
413
+ circuit: string
414
+ A string that should be interpreted as an equivalent circuit
415
+
416
+ Notes
417
+ -----
418
+ A custom circuit is defined as a string comprised of elements in series
419
+ (separated by a `-`) and elements in parallel (grouped as (x,y)).
420
+ Each element can be appended with an integer (e.g. R0) or an underscore
421
+ and an integer (e.g. CPE_1) to make keeping track of multiple elements
422
+ of the same type easier.
423
+
424
+ Example:
425
+ Randles circuit is given by 'R0-p(R1-Wo1,C1)'
426
+
427
+ """
428
+
429
+ super().__init__(**kwargs)
430
+ self.circuit = circuit.replace(" ", "")
431
+
432
+ circuit_len = calculateCircuitLength(self.circuit)
433
+
434
+ if len(self.initial_guess) + len(self.constants) != circuit_len:
435
+ raise ValueError('The number of initial guesses ' +
436
+ f'({len(self.initial_guess)}) + ' +
437
+ 'the number of constants ' +
438
+ f'({len(self.constants)})' +
439
+ ' must be equal to ' +
440
+ f'the circuit length ({circuit_len})')