blowing-bubbles 0.2.0__py3-none-any.whl
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.
- blowing_bubbles/__init__.py +19 -0
- blowing_bubbles/circuit_impedance/__init__.py +15 -0
- blowing_bubbles/circuit_impedance/impedance.py +288 -0
- blowing_bubbles/classes/__init__.py +25 -0
- blowing_bubbles/classes/circuit_classes.py +999 -0
- blowing_bubbles/classes/data_classes.py +1036 -0
- blowing_bubbles/py.typed +0 -0
- blowing_bubbles/scripts/__init__.py +18 -0
- blowing_bubbles/scripts/fit_circuit.py +601 -0
- blowing_bubbles/scripts/global_minimize.py +1393 -0
- blowing_bubbles/scripts/quality.py +224 -0
- blowing_bubbles/ui/__init__.py +26 -0
- blowing_bubbles/ui/__main__.py +13 -0
- blowing_bubbles/ui/analysis.py +250 -0
- blowing_bubbles/ui/analysis_plots.py +127 -0
- blowing_bubbles/ui/app.py +613 -0
- blowing_bubbles/ui/assets/icon-128.png +0 -0
- blowing_bubbles/ui/assets/icon-16.png +0 -0
- blowing_bubbles/ui/assets/icon-256.png +0 -0
- blowing_bubbles/ui/assets/icon-32.png +0 -0
- blowing_bubbles/ui/assets/icon-48.png +0 -0
- blowing_bubbles/ui/assets/icon-512.png +0 -0
- blowing_bubbles/ui/assets/icon-64.png +0 -0
- blowing_bubbles/ui/assets/icon.png +0 -0
- blowing_bubbles/ui/assets/icon.svg +6 -0
- blowing_bubbles/ui/batch.py +250 -0
- blowing_bubbles/ui/desktop.py +219 -0
- blowing_bubbles/ui/folder_picker.py +73 -0
- blowing_bubbles/ui/live.py +129 -0
- blowing_bubbles/ui/plots.py +159 -0
- blowing_bubbles-0.2.0.dist-info/METADATA +108 -0
- blowing_bubbles-0.2.0.dist-info/RECORD +34 -0
- blowing_bubbles-0.2.0.dist-info/WHEEL +4 -0
- blowing_bubbles-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
r"""BlowingBubbles — equivalent-circuit fitting of electrochemical impedance spectra.
|
|
2
|
+
|
|
3
|
+
The main entry point is :class:`~blowing_bubbles.classes.EISData` (load, process
|
|
4
|
+
and fit a spectrum); fitted parameters are held in
|
|
5
|
+
:class:`~blowing_bubbles.classes.CircuitParameters` and
|
|
6
|
+
:class:`~blowing_bubbles.classes.FitResults`. Subpackages:
|
|
7
|
+
|
|
8
|
+
- :mod:`blowing_bubbles.classes` — data, parameter and fit-result containers;
|
|
9
|
+
- :mod:`blowing_bubbles.circuit_impedance` — impedance forward model and
|
|
10
|
+
analytic gradients;
|
|
11
|
+
- :mod:`blowing_bubbles.scripts` — fitting routines and fit-quality metrics.
|
|
12
|
+
|
|
13
|
+
Licensed under MIT (see ``LICENSE``). Cite ``CITATION.bib`` in published
|
|
14
|
+
scientific work; see ``ACADEMIC_CITATION.md``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.2.0"
|
|
18
|
+
|
|
19
|
+
__all__ = ["__version__", "circuit_impedance", "classes", "scripts"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
r"""Forward model and gradients for the BlowingBubbles R–CPE equivalent circuit.
|
|
2
|
+
|
|
3
|
+
Re-exports :func:`~blowing_bubbles.circuit_impedance.impedance.circuit_impedance`
|
|
4
|
+
(complex impedance of the circuit) and
|
|
5
|
+
:func:`~blowing_bubbles.circuit_impedance.impedance.circuit_impedance_parameter_gradient`
|
|
6
|
+
(analytic partial derivatives) from
|
|
7
|
+
:mod:`blowing_bubbles.circuit_impedance.impedance`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .impedance import circuit_impedance, circuit_impedance_parameter_gradient
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"circuit_impedance",
|
|
14
|
+
"circuit_impedance_parameter_gradient",
|
|
15
|
+
]
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
r"""Impedance evaluation and analytic Jacobians for BlowingBubbles.
|
|
2
|
+
|
|
3
|
+
The equivalent-circuit model combines ohmic resistance, a constant-phase inductor,
|
|
4
|
+
and :math:`n` parallel R–CPE branches (Debye / Cole–Cole style elements):
|
|
5
|
+
|
|
6
|
+
.. math::
|
|
7
|
+
|
|
8
|
+
Z(\omega) = R_s + (j\omega)^{n_L} L
|
|
9
|
+
+ \sum_{i=1}^{n} \frac{R_i}{1 + R_i C_i (j\omega)^{e_i}}
|
|
10
|
+
|
|
11
|
+
with angular frequency :math:`\omega = 2\pi f`. Branch time constants are
|
|
12
|
+
:math:`\tau_i = R_i C_i`. Setting :math:`e_i = 1` recovers ideal capacitors;
|
|
13
|
+
:math:`e_i < 1` gives constant-phase elements (CPEs).
|
|
14
|
+
|
|
15
|
+
Public entry points are :func:`circuit_impedance` (forward model) and
|
|
16
|
+
:func:`circuit_impedance_parameter_gradient` (analytic partial derivatives);
|
|
17
|
+
:func:`circuit_assert_arguments` validates their shared arguments.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
_EMPTY_BRANCH_ARRAY = np.zeros(0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def circuit_assert_arguments(
|
|
26
|
+
freq: float | np.ndarray,
|
|
27
|
+
resistance_source: float = 0,
|
|
28
|
+
inductance: float = 0,
|
|
29
|
+
inductance_exponent: float = 1,
|
|
30
|
+
resistances: np.ndarray = _EMPTY_BRANCH_ARRAY,
|
|
31
|
+
capacitances: np.ndarray = _EMPTY_BRANCH_ARRAY,
|
|
32
|
+
exponents: np.ndarray | None = None,
|
|
33
|
+
) -> tuple[np.ndarray, float, float, float, np.ndarray, np.ndarray, np.ndarray, bool]:
|
|
34
|
+
r"""Validate and normalize the arguments of :func:`circuit_impedance`.
|
|
35
|
+
|
|
36
|
+
Converts all inputs to floats / float arrays, coerces a scalar *freq* to a
|
|
37
|
+
length-1 array and defaults *exponents* to ones.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
freq : float or numpy.ndarray
|
|
42
|
+
Frequency or frequencies (Hz); lists and tuples are accepted.
|
|
43
|
+
resistance_source, inductance, inductance_exponent : float
|
|
44
|
+
Global parameters :math:`R_s`, :math:`L`, :math:`n_L`.
|
|
45
|
+
resistances, capacitances : numpy.ndarray, shape (n_branches,)
|
|
46
|
+
Branch parameters :math:`R_i`, :math:`C_i`.
|
|
47
|
+
exponents : numpy.ndarray, shape (n_branches,), optional
|
|
48
|
+
Branch exponents :math:`e_i`. ``None`` (default) means all ones.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
freq : numpy.ndarray, shape (n_freq,)
|
|
53
|
+
Frequencies as a 1-D float array.
|
|
54
|
+
resistance_source, inductance, inductance_exponent : float
|
|
55
|
+
Global parameters as floats.
|
|
56
|
+
resistances, capacitances, exponents : numpy.ndarray
|
|
57
|
+
Branch parameters as float arrays.
|
|
58
|
+
isfloat : bool
|
|
59
|
+
``True`` when a scalar frequency was passed, so the caller can return
|
|
60
|
+
a scalar impedance.
|
|
61
|
+
|
|
62
|
+
Raises
|
|
63
|
+
------
|
|
64
|
+
TypeError
|
|
65
|
+
If an argument cannot be converted to a float or float array.
|
|
66
|
+
ValueError
|
|
67
|
+
If *resistances*, *capacitances* and *exponents* differ in shape.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(freq, (list, tuple)):
|
|
70
|
+
freq = np.asarray(freq)
|
|
71
|
+
isfloat = np.ndim(freq) == 0
|
|
72
|
+
try:
|
|
73
|
+
freq = np.atleast_1d(np.asarray(freq, dtype=float))
|
|
74
|
+
resistance_source = float(resistance_source)
|
|
75
|
+
inductance = float(inductance)
|
|
76
|
+
inductance_exponent = float(inductance_exponent)
|
|
77
|
+
resistances = np.asarray(resistances, dtype=float)
|
|
78
|
+
capacitances = np.asarray(capacitances, dtype=float)
|
|
79
|
+
exponents = (
|
|
80
|
+
np.ones_like(capacitances)
|
|
81
|
+
if exponents is None
|
|
82
|
+
else np.asarray(exponents, dtype=float)
|
|
83
|
+
)
|
|
84
|
+
except (TypeError, ValueError) as exc:
|
|
85
|
+
raise TypeError(f"Circuit arguments must be numeric: {exc}") from exc
|
|
86
|
+
|
|
87
|
+
if not resistances.shape == capacitances.shape == exponents.shape:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"resistances, capacitances and exponents must have the same shape. "
|
|
90
|
+
f"Current shapes: {resistances.shape}, {capacitances.shape}, "
|
|
91
|
+
f"{exponents.shape}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
freq,
|
|
96
|
+
resistance_source,
|
|
97
|
+
inductance,
|
|
98
|
+
inductance_exponent,
|
|
99
|
+
resistances,
|
|
100
|
+
capacitances,
|
|
101
|
+
exponents,
|
|
102
|
+
isfloat,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def circuit_impedance(
|
|
107
|
+
freq: float | np.ndarray,
|
|
108
|
+
resistance_source: float = 0,
|
|
109
|
+
inductance: float = 0,
|
|
110
|
+
inductance_exponent: float = 1,
|
|
111
|
+
resistances: np.ndarray = _EMPTY_BRANCH_ARRAY,
|
|
112
|
+
capacitances: np.ndarray = _EMPTY_BRANCH_ARRAY,
|
|
113
|
+
exponents: np.ndarray | None = None,
|
|
114
|
+
) -> np.complex128 | np.ndarray:
|
|
115
|
+
r"""Calculate the impedance of the R–CPE ladder circuit.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
freq : float or numpy.ndarray
|
|
120
|
+
Frequency or frequencies :math:`f` (Hz); lists and tuples are accepted.
|
|
121
|
+
resistance_source : float, default 0
|
|
122
|
+
Series (source) resistance :math:`R_s` (Ω).
|
|
123
|
+
inductance : float, default 0
|
|
124
|
+
Inductance :math:`L` (H for :math:`n_L = 1`).
|
|
125
|
+
inductance_exponent : float, default 1
|
|
126
|
+
Inductor exponent :math:`n_L`.
|
|
127
|
+
resistances : numpy.ndarray, shape (n_branches,), default empty
|
|
128
|
+
Branch resistances :math:`R_i` (Ω).
|
|
129
|
+
capacitances : numpy.ndarray, shape (n_branches,), default empty
|
|
130
|
+
Branch capacitances :math:`C_i` (F for :math:`e_i = 1`).
|
|
131
|
+
exponents : numpy.ndarray, shape (n_branches,), optional
|
|
132
|
+
Branch CPE exponents :math:`e_i`. ``None`` (default) means all ones
|
|
133
|
+
(ideal capacitors).
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Z : numpy.complex128 or numpy.ndarray
|
|
138
|
+
Complex impedance; a scalar if *freq* is a scalar, otherwise an array
|
|
139
|
+
of shape ``(n_freq,)``.
|
|
140
|
+
|
|
141
|
+
See Also
|
|
142
|
+
--------
|
|
143
|
+
circuit_assert_arguments : Argument validation (raises ``TypeError`` for
|
|
144
|
+
non-numeric input and ``ValueError`` for mismatched branch shapes).
|
|
145
|
+
circuit_impedance_parameter_gradient : Analytic partial derivatives.
|
|
146
|
+
|
|
147
|
+
Notes
|
|
148
|
+
-----
|
|
149
|
+
.. math::
|
|
150
|
+
|
|
151
|
+
Z(\omega) = R_s + (j\omega)^{n_L} L
|
|
152
|
+
+ \sum_{i=1}^{n} \frac{R_i}{1 + R_i C_i (j\omega)^{e_i}}
|
|
153
|
+
|
|
154
|
+
with :math:`\omega = 2\pi f` and :math:`n` the number of branches. With
|
|
155
|
+
the default arguments the circuit is empty and :math:`Z = 0`. Capacitive
|
|
156
|
+
branches give :math:`\mathrm{Im}(Z) < 0`, the inductor
|
|
157
|
+
:math:`\mathrm{Im}(Z) > 0`.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
(
|
|
161
|
+
freq,
|
|
162
|
+
resistance_source,
|
|
163
|
+
inductance,
|
|
164
|
+
inductance_exponent,
|
|
165
|
+
resistances,
|
|
166
|
+
capacitances,
|
|
167
|
+
exponents,
|
|
168
|
+
isfloat,
|
|
169
|
+
) = circuit_assert_arguments(
|
|
170
|
+
freq,
|
|
171
|
+
resistance_source,
|
|
172
|
+
inductance,
|
|
173
|
+
inductance_exponent,
|
|
174
|
+
resistances,
|
|
175
|
+
capacitances,
|
|
176
|
+
exponents,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
s = 1j * (2 * np.pi * freq)
|
|
180
|
+
|
|
181
|
+
parallel_impedances = resistances / (
|
|
182
|
+
1 + resistances * capacitances * s[:, None] ** exponents
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
series_impedances = (
|
|
186
|
+
resistance_source
|
|
187
|
+
+ s**inductance_exponent * inductance
|
|
188
|
+
+ np.sum(parallel_impedances, axis=1)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if isfloat:
|
|
192
|
+
series_impedances = series_impedances[0]
|
|
193
|
+
|
|
194
|
+
return series_impedances
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def circuit_impedance_parameter_gradient(
|
|
198
|
+
freq: float | np.ndarray,
|
|
199
|
+
resistance_source: float = 0,
|
|
200
|
+
inductance: float = 0,
|
|
201
|
+
inductance_exponent: float = 1,
|
|
202
|
+
resistances: np.ndarray = _EMPTY_BRANCH_ARRAY,
|
|
203
|
+
capacitances: np.ndarray = _EMPTY_BRANCH_ARRAY,
|
|
204
|
+
exponents: np.ndarray | None = None,
|
|
205
|
+
) -> dict[str, np.ndarray]:
|
|
206
|
+
r"""Return the analytic partial derivatives of :func:`circuit_impedance`.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
freq : float or numpy.ndarray
|
|
211
|
+
Frequency or frequencies :math:`f` (Hz).
|
|
212
|
+
resistance_source, inductance, inductance_exponent : float
|
|
213
|
+
Global parameters :math:`R_s` (Ω), :math:`L`, :math:`n_L`; defaults
|
|
214
|
+
0, 0, 1.
|
|
215
|
+
resistances, capacitances : numpy.ndarray, shape (n_branches,)
|
|
216
|
+
Branch parameters :math:`R_i` (Ω), :math:`C_i`; default empty.
|
|
217
|
+
exponents : numpy.ndarray, shape (n_branches,), optional
|
|
218
|
+
Branch exponents :math:`e_i`. ``None`` (default) means all ones.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
gradient : dict of str to numpy.ndarray
|
|
223
|
+
Partial derivatives :math:`\partial Z / \partial p`, keyed by
|
|
224
|
+
parameter name: ``resistance_source``, ``inductance`` and
|
|
225
|
+
``inductance_exponent`` with shape ``(n_freq,)``; ``resistances``,
|
|
226
|
+
``capacitances`` and ``exponents`` with shape
|
|
227
|
+
``(n_freq, n_branches)``. Arrays are returned even for a scalar
|
|
228
|
+
*freq* (``n_freq = 1``).
|
|
229
|
+
|
|
230
|
+
See Also
|
|
231
|
+
--------
|
|
232
|
+
circuit_impedance : The differentiated model.
|
|
233
|
+
circuit_assert_arguments : Argument validation.
|
|
234
|
+
|
|
235
|
+
Notes
|
|
236
|
+
-----
|
|
237
|
+
With :math:`s = j\omega` and :math:`Y_i = 1 + R_i C_i s^{e_i}`:
|
|
238
|
+
|
|
239
|
+
.. math::
|
|
240
|
+
|
|
241
|
+
\frac{\partial Z}{\partial R_s} = 1, \quad
|
|
242
|
+
\frac{\partial Z}{\partial L} = s^{n_L}, \quad
|
|
243
|
+
\frac{\partial Z}{\partial n_L} = s^{n_L} L \ln s,
|
|
244
|
+
|
|
245
|
+
\frac{\partial Z}{\partial R_i} =
|
|
246
|
+
\frac{1}{Y_i} - \frac{R_i C_i s^{e_i}}{Y_i^2} = \frac{1}{Y_i^2},
|
|
247
|
+
\quad
|
|
248
|
+
\frac{\partial Z}{\partial C_i} = -\frac{R_i^2 s^{e_i}}{Y_i^2},
|
|
249
|
+
\quad
|
|
250
|
+
\frac{\partial Z}{\partial e_i} =
|
|
251
|
+
-\frac{R_i^2 C_i s^{e_i} \ln s}{Y_i^2}.
|
|
252
|
+
|
|
253
|
+
The derivative with respect to :math:`R_s` is returned as a real array.
|
|
254
|
+
"""
|
|
255
|
+
(
|
|
256
|
+
freq,
|
|
257
|
+
resistance_source,
|
|
258
|
+
inductance,
|
|
259
|
+
inductance_exponent,
|
|
260
|
+
resistances,
|
|
261
|
+
capacitances,
|
|
262
|
+
exponents,
|
|
263
|
+
_isfloat,
|
|
264
|
+
) = circuit_assert_arguments(
|
|
265
|
+
freq,
|
|
266
|
+
resistance_source,
|
|
267
|
+
inductance,
|
|
268
|
+
inductance_exponent,
|
|
269
|
+
resistances,
|
|
270
|
+
capacitances,
|
|
271
|
+
exponents,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
s = 1j * (2 * np.pi * freq)
|
|
275
|
+
log_s = np.log(s)
|
|
276
|
+
s_l = s**inductance_exponent
|
|
277
|
+
S = s[:, None] ** exponents
|
|
278
|
+
D2 = (1 + resistances * capacitances * S) ** 2
|
|
279
|
+
d_capacitances = -(resistances**2) * S / D2
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"resistance_source": np.ones_like(freq),
|
|
283
|
+
"inductance": s_l,
|
|
284
|
+
"inductance_exponent": s_l * log_s * inductance,
|
|
285
|
+
"resistances": 1 / D2,
|
|
286
|
+
"capacitances": d_capacitances,
|
|
287
|
+
"exponents": d_capacitances * capacitances * log_s[:, None],
|
|
288
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
r"""EIS data handling and equivalent-circuit parameter containers.
|
|
2
|
+
|
|
3
|
+
Re-exports :class:`~blowing_bubbles.classes.data_classes.EISData` (load,
|
|
4
|
+
process and fit impedance spectra) from
|
|
5
|
+
:mod:`blowing_bubbles.classes.data_classes`, and
|
|
6
|
+
:class:`~blowing_bubbles.classes.circuit_classes.CircuitParameters`,
|
|
7
|
+
:class:`~blowing_bubbles.classes.circuit_classes.FitResults` and
|
|
8
|
+
:func:`~blowing_bubbles.classes.circuit_classes.align_axes_height` (circuit
|
|
9
|
+
parameters, fit results, plot helper) from
|
|
10
|
+
:mod:`blowing_bubbles.classes.circuit_classes`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from blowing_bubbles.classes.circuit_classes import (
|
|
14
|
+
CircuitParameters,
|
|
15
|
+
FitResults,
|
|
16
|
+
align_axes_height,
|
|
17
|
+
)
|
|
18
|
+
from blowing_bubbles.classes.data_classes import EISData
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CircuitParameters",
|
|
22
|
+
"EISData",
|
|
23
|
+
"FitResults",
|
|
24
|
+
"align_axes_height",
|
|
25
|
+
]
|