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.
- impedance_extend-1.0.0/LICENSE +21 -0
- impedance_extend-1.0.0/PKG-INFO +120 -0
- impedance_extend-1.0.0/README.md +89 -0
- impedance_extend-1.0.0/impedance_extend/__init__.py +1 -0
- impedance_extend-1.0.0/impedance_extend/models/__init__.py +0 -0
- impedance_extend-1.0.0/impedance_extend/models/circuits/__init__.py +2 -0
- impedance_extend-1.0.0/impedance_extend/models/circuits/circuits.py +440 -0
- impedance_extend-1.0.0/impedance_extend/models/circuits/elements.py +449 -0
- impedance_extend-1.0.0/impedance_extend/models/circuits/fitting.py +839 -0
- impedance_extend-1.0.0/impedance_extend/preprocessing.py +508 -0
- impedance_extend-1.0.0/impedance_extend/tests/__init__.py +4 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_circuit_elements.py +218 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_circuits.py +221 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_fitting.py +390 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_model_io.py +33 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_preprocessing.py +518 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_validation.py +237 -0
- impedance_extend-1.0.0/impedance_extend/tests/test_visualization.py +77 -0
- impedance_extend-1.0.0/impedance_extend/validation.py +310 -0
- impedance_extend-1.0.0/impedance_extend/visualization.py +332 -0
- impedance_extend-1.0.0/impedance_extend.egg-info/PKG-INFO +120 -0
- impedance_extend-1.0.0/impedance_extend.egg-info/SOURCES.txt +25 -0
- impedance_extend-1.0.0/impedance_extend.egg-info/dependency_links.txt +1 -0
- impedance_extend-1.0.0/impedance_extend.egg-info/requires.txt +7 -0
- impedance_extend-1.0.0/impedance_extend.egg-info/top_level.txt +1 -0
- impedance_extend-1.0.0/setup.cfg +4 -0
- 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]] 
|
|
33
|
+
|
|
34
|
+
 [](#contributors)
|
|
35
|
+
|
|
36
|
+
[](https://github.com/k-vijayaraghavan/impedance_extend.py/actions) [](https://impedancepy.readthedocs.io/en/latest/?badge=latest) [](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
|
+
[](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]] 
|
|
2
|
+
|
|
3
|
+
 [](#contributors)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/k-vijayaraghavan/impedance_extend.py/actions) [](https://impedancepy.readthedocs.io/en/latest/?badge=latest) [](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
|
+
[](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"
|
|
File without changes
|
|
@@ -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})')
|