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.
Files changed (34) hide show
  1. blowing_bubbles/__init__.py +19 -0
  2. blowing_bubbles/circuit_impedance/__init__.py +15 -0
  3. blowing_bubbles/circuit_impedance/impedance.py +288 -0
  4. blowing_bubbles/classes/__init__.py +25 -0
  5. blowing_bubbles/classes/circuit_classes.py +999 -0
  6. blowing_bubbles/classes/data_classes.py +1036 -0
  7. blowing_bubbles/py.typed +0 -0
  8. blowing_bubbles/scripts/__init__.py +18 -0
  9. blowing_bubbles/scripts/fit_circuit.py +601 -0
  10. blowing_bubbles/scripts/global_minimize.py +1393 -0
  11. blowing_bubbles/scripts/quality.py +224 -0
  12. blowing_bubbles/ui/__init__.py +26 -0
  13. blowing_bubbles/ui/__main__.py +13 -0
  14. blowing_bubbles/ui/analysis.py +250 -0
  15. blowing_bubbles/ui/analysis_plots.py +127 -0
  16. blowing_bubbles/ui/app.py +613 -0
  17. blowing_bubbles/ui/assets/icon-128.png +0 -0
  18. blowing_bubbles/ui/assets/icon-16.png +0 -0
  19. blowing_bubbles/ui/assets/icon-256.png +0 -0
  20. blowing_bubbles/ui/assets/icon-32.png +0 -0
  21. blowing_bubbles/ui/assets/icon-48.png +0 -0
  22. blowing_bubbles/ui/assets/icon-512.png +0 -0
  23. blowing_bubbles/ui/assets/icon-64.png +0 -0
  24. blowing_bubbles/ui/assets/icon.png +0 -0
  25. blowing_bubbles/ui/assets/icon.svg +6 -0
  26. blowing_bubbles/ui/batch.py +250 -0
  27. blowing_bubbles/ui/desktop.py +219 -0
  28. blowing_bubbles/ui/folder_picker.py +73 -0
  29. blowing_bubbles/ui/live.py +129 -0
  30. blowing_bubbles/ui/plots.py +159 -0
  31. blowing_bubbles-0.2.0.dist-info/METADATA +108 -0
  32. blowing_bubbles-0.2.0.dist-info/RECORD +34 -0
  33. blowing_bubbles-0.2.0.dist-info/WHEEL +4 -0
  34. 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
+ ]