eisyfit 0.1.2__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.
- eisyfit-0.1.2/PKG-INFO +140 -0
- eisyfit-0.1.2/README.md +107 -0
- eisyfit-0.1.2/circuit_structure.py +157 -0
- eisyfit-0.1.2/eis_gui.py +14502 -0
- eisyfit-0.1.2/eis_model.py +381 -0
- eisyfit-0.1.2/eis_project.py +892 -0
- eisyfit-0.1.2/eis_services.py +1774 -0
- eisyfit-0.1.2/eisyfit.egg-info/PKG-INFO +140 -0
- eisyfit-0.1.2/eisyfit.egg-info/SOURCES.txt +79 -0
- eisyfit-0.1.2/eisyfit.egg-info/dependency_links.txt +1 -0
- eisyfit-0.1.2/eisyfit.egg-info/entry_points.txt +2 -0
- eisyfit-0.1.2/eisyfit.egg-info/requires.txt +13 -0
- eisyfit-0.1.2/eisyfit.egg-info/top_level.txt +11 -0
- eisyfit-0.1.2/explorer_filter.py +119 -0
- eisyfit-0.1.2/extract_relaxis.py +401 -0
- eisyfit-0.1.2/main.py +50 -0
- eisyfit-0.1.2/ml/__init__.py +22 -0
- eisyfit-0.1.2/ml/audit_manual_frequency_range.py +198 -0
- eisyfit-0.1.2/ml/automatic_preprocessing.py +111 -0
- eisyfit-0.1.2/ml/benchmark_outlier.py +57 -0
- eisyfit-0.1.2/ml/benchmark_point_validity.py +50 -0
- eisyfit-0.1.2/ml/complete_ml_range_cache.py +162 -0
- eisyfit-0.1.2/ml/continue_sample178_outliers.py +43 -0
- eisyfit-0.1.2/ml/dataset.py +233 -0
- eisyfit-0.1.2/ml/diagnostics.py +55 -0
- eisyfit-0.1.2/ml/evaluate_frequency_limit_ml.py +154 -0
- eisyfit-0.1.2/ml/evaluate_frequency_selection_voltage.py +101 -0
- eisyfit-0.1.2/ml/evaluate_low_frequency_selector.py +149 -0
- eisyfit-0.1.2/ml/evaluate_point_validity.py +162 -0
- eisyfit-0.1.2/ml/evaluate_stage4_parameters.py +349 -0
- eisyfit-0.1.2/ml/evaluate_staged_datasets.py +93 -0
- eisyfit-0.1.2/ml/evaluate_topology_automatic_preprocessing.py +136 -0
- eisyfit-0.1.2/ml/finalize_topology_for_unseen_178.py +265 -0
- eisyfit-0.1.2/ml/frequency_limit_ml.py +85 -0
- eisyfit-0.1.2/ml/frequency_range.py +147 -0
- eisyfit-0.1.2/ml/frequency_range_visualization.py +65 -0
- eisyfit-0.1.2/ml/gui_results.py +384 -0
- eisyfit-0.1.2/ml/infer_sample178_new_frequency.py +73 -0
- eisyfit-0.1.2/ml/infer_unseen_sample_178.py +139 -0
- eisyfit-0.1.2/ml/inspect_eisfit.py +114 -0
- eisyfit-0.1.2/ml/l0_decision.py +213 -0
- eisyfit-0.1.2/ml/low_frequency_selector.py +164 -0
- eisyfit-0.1.2/ml/metrics.py +126 -0
- eisyfit-0.1.2/ml/migrate_embedded_results.py +82 -0
- eisyfit-0.1.2/ml/outlier_cache.py +220 -0
- eisyfit-0.1.2/ml/parameter_limits.py +92 -0
- eisyfit-0.1.2/ml/parameter_prediction.py +164 -0
- eisyfit-0.1.2/ml/pipeline.py +229 -0
- eisyfit-0.1.2/ml/point_validity.py +251 -0
- eisyfit-0.1.2/ml/preprocessing.py +72 -0
- eisyfit-0.1.2/ml/results_schema.py +51 -0
- eisyfit-0.1.2/ml/run_frequency_range.py +56 -0
- eisyfit-0.1.2/ml/run_frequency_topology.py +55 -0
- eisyfit-0.1.2/ml/run_stage4a_parameters.py +193 -0
- eisyfit-0.1.2/ml/run_stage4b_parameters.py +444 -0
- eisyfit-0.1.2/ml/run_stage5a_initial_parameters.py +511 -0
- eisyfit-0.1.2/ml/run_staged_topology.py +37 -0
- eisyfit-0.1.2/ml/run_topology.py +51 -0
- eisyfit-0.1.2/ml/run_topology_cached.py +183 -0
- eisyfit-0.1.2/ml/runtime_inference.py +380 -0
- eisyfit-0.1.2/ml/staged_topology.py +154 -0
- eisyfit-0.1.2/ml/topology_classifier.py +142 -0
- eisyfit-0.1.2/plot_export.py +90 -0
- eisyfit-0.1.2/pyproject.toml +68 -0
- eisyfit-0.1.2/setup.cfg +4 -0
- eisyfit-0.1.2/spectrum_simulator.py +74 -0
- eisyfit-0.1.2/tests/test_batch_stop.py +127 -0
- eisyfit-0.1.2/tests/test_circuit_structure.py +38 -0
- eisyfit-0.1.2/tests/test_explorer_filter.py +53 -0
- eisyfit-0.1.2/tests/test_fit_optimizers.py +49 -0
- eisyfit-0.1.2/tests/test_low_frequency_selector.py +53 -0
- eisyfit-0.1.2/tests/test_ml.py +150 -0
- eisyfit-0.1.2/tests/test_ml_results_architecture.py +50 -0
- eisyfit-0.1.2/tests/test_mpr_import.py +198 -0
- eisyfit-0.1.2/tests/test_plot_export.py +45 -0
- eisyfit-0.1.2/tests/test_point_validity.py +128 -0
- eisyfit-0.1.2/tests/test_relaxis_mask_limits_models.py +137 -0
- eisyfit-0.1.2/tests/test_relaxis_project_load.py +83 -0
- eisyfit-0.1.2/tests/test_spectrum_simulator.py +46 -0
- eisyfit-0.1.2/tests/test_stage4_parameter_prediction.py +89 -0
- eisyfit-0.1.2/tests/test_stage5a_initial_parameters.py +57 -0
eisyfit-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eisyfit
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Interactive desktop application for exploring, cleaning, and fitting BioLogic electrochemical impedance spectra.
|
|
5
|
+
Author: Jaroslav Herman
|
|
6
|
+
Project-URL: Homepage, https://github.com/jaroslav-herman/EIS_fitting
|
|
7
|
+
Project-URL: Repository, https://github.com/jaroslav-herman/EIS_fitting
|
|
8
|
+
Project-URL: Issues, https://github.com/jaroslav-herman/EIS_fitting/issues
|
|
9
|
+
Keywords: electrochemistry,impedance spectroscopy,EIS,BioLogic,equivalent circuit
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: eisyfit-bayes-drt2>=0.2
|
|
21
|
+
Requires-Dist: eisyfit-hybrid-drt>=0.1
|
|
22
|
+
Requires-Dist: impedance>=1.7.1
|
|
23
|
+
Requires-Dist: matplotlib>=3.11.1
|
|
24
|
+
Requires-Dist: natsort>=8.4.0
|
|
25
|
+
Requires-Dist: numpy>=2.5.1
|
|
26
|
+
Requires-Dist: pandas>=3.0.3
|
|
27
|
+
Requires-Dist: scipy>=1.18.0
|
|
28
|
+
Requires-Dist: pyarrow>=25.0.1
|
|
29
|
+
Requires-Dist: scikit-learn>=1.5.0
|
|
30
|
+
Requires-Dist: pygad>=3.6.0
|
|
31
|
+
Requires-Dist: pyswarms>=1.3
|
|
32
|
+
Requires-Dist: eisyfit-wepy>=0.1.2
|
|
33
|
+
|
|
34
|
+
## EIS Fitting
|
|
35
|
+
|
|
36
|
+
EIS Fitting is a desktop application for exploring, cleaning, and fitting
|
|
37
|
+
electrochemical impedance spectroscopy (EIS) data exported by BioLogic as
|
|
38
|
+
`.mpt` (PEIS) files. It combines an interactive Nyquist plot with circuit
|
|
39
|
+
parameter editing, cycle-aware analysis, DRT-assisted outlier detection, and
|
|
40
|
+
batch fitting, so spectra can be reviewed and fitted without leaving the GUI.
|
|
41
|
+
|
|
42
|
+
The application:
|
|
43
|
+
|
|
44
|
+
- Supports multiple files and multiple measurement cycles
|
|
45
|
+
- Displays spectra beside an editable equivalent-circuit parameter table
|
|
46
|
+
- Lets you include or exclude individual points while preserving cycle state
|
|
47
|
+
- Runs fitting and outlier detection in the background so the GUI stays responsive
|
|
48
|
+
- Uses `bayes-drt2` ridge analysis to identify outliers and initialize circuit fits
|
|
49
|
+
- Compares measured and fitted points at identical frequencies with residual connectors
|
|
50
|
+
- Provides a sortable spectra explorer for cycle and acquisition metadata
|
|
51
|
+
- Exports fitted parameters and spectrum metadata to CSV and a ready-to-run Python script
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
### Requirements
|
|
56
|
+
|
|
57
|
+
- Windows, macOS, or Linux with a desktop environment
|
|
58
|
+
- Python 3.14 or newer
|
|
59
|
+
- [`uv`](https://docs.astral.sh/uv/getting-started/installation/), the environment and dependency manager used by this project
|
|
60
|
+
|
|
61
|
+
### Install from GitHub
|
|
62
|
+
|
|
63
|
+
In PowerShell, Terminal, or a shell:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
git clone https://github.com/jaroslav-herman/EIS_fitting.git
|
|
67
|
+
cd EIS_fitting
|
|
68
|
+
uv sync
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`uv sync` creates the project environment and installs the versions recorded
|
|
72
|
+
in `uv.lock`, including the required EIS and DRT libraries. Re-run it after
|
|
73
|
+
pulling changes to update the environment.
|
|
74
|
+
|
|
75
|
+
### Start the application
|
|
76
|
+
|
|
77
|
+
From the repository directory, run:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
uv run python main.py
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To open a BioLogic file immediately and select a cycle:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
uv run python main.py PEIS_at_N2_flow_80_sccm_automated_01_PEIS.mpt --cycle 1
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
On Windows, `run_gui.cmd` can also be double-clicked. It checks that `uv` is
|
|
90
|
+
available and starts the application from the repository directory.
|
|
91
|
+
|
|
92
|
+
If `uv` is not available, install it using the official instructions linked
|
|
93
|
+
above, then open a new terminal and repeat `uv sync`.
|
|
94
|
+
|
|
95
|
+
## Run
|
|
96
|
+
|
|
97
|
+
The command-line entry point also accepts options for the initial control
|
|
98
|
+
channel, outlier threshold, and equivalent-circuit model:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
uv run python main.py PEIS_at_N2_flow_80_sccm_automated_01_PEIS.mpt --cycle 1 --threshold 1.0 --circuit "R0-L0-p(R1,CPE1)"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### GUI controls
|
|
105
|
+
|
|
106
|
+
- Click a point: toggle include/exclude
|
|
107
|
+
- Cycle arrows or selector: move between cycles
|
|
108
|
+
- Spectra explorer: select a spectrum or sort rows by source file, cycle, voltage, current, point count, or frequency limits
|
|
109
|
+
- Fitting model: select a preset or enter an `impedance.py` circuit string, then set it
|
|
110
|
+
- Apply to current cycle: use the frequency range only for the displayed spectrum
|
|
111
|
+
- Apply to all cycles: use the frequency range for every spectrum in the file
|
|
112
|
+
- Outliers: current: run the bayes-drt2 ridge analysis on active points only, apply its outlier mask without reactivating excluded points, and initialize the EEC fit from its ohmic resistance, inductance, and strongest DRT peaks
|
|
113
|
+
- Outliers: all cycles: perform the same active-point analysis and EEC initialization for every spectrum in the background
|
|
114
|
+
- Fit spectrum: fit included points with the parameter table values
|
|
115
|
+
- Batch fit from current: fit toward higher cycles, using each result to initialize the next
|
|
116
|
+
- Export to Python: save fitted parameters and spectrum metadata from all loaded files to CSV, create a ready-to-run pandas script, and open it in VS Code or another available editor
|
|
117
|
+
- Reset points: include all points and clear detected outliers
|
|
118
|
+
|
|
119
|
+
The **File** menu contains additive multi-file data import, project load/save, mask saving, fit-parameter and Python-workspace exports, and exit commands. Import accepts several `.mpt` files at once and keeps previously loaded spectra.
|
|
120
|
+
|
|
121
|
+
The **Fit** menu fits the selected spectrum or batch-fits upward/downward through the explorer's visible order. Metadata-limited batches use the last clicked numeric explorer column and stop at its nearest available target value.
|
|
122
|
+
|
|
123
|
+
Outlier detection never runs automatically. Cycle state is retained while the GUI is open.
|
|
124
|
+
|
|
125
|
+
Keyboard shortcuts:
|
|
126
|
+
|
|
127
|
+
- `Alt+A`: copy fitted values from the previous cycle into the current initial values
|
|
128
|
+
- `Alt+D`: copy fitted values from the next cycle into the current initial values
|
|
129
|
+
- `Alt+S`: fit the selected spectrum
|
|
130
|
+
- `Ctrl+O`: import another `.mpt` data file
|
|
131
|
+
- `Ctrl+Shift+O`: load an EIS fitting project
|
|
132
|
+
- `Ctrl+S`: save the EIS fitting project
|
|
133
|
+
|
|
134
|
+
### Code structure
|
|
135
|
+
|
|
136
|
+
- `eis_model.py`: GUI-independent project and per-cycle state
|
|
137
|
+
- `eis_services.py`: file loading, outlier detection, and fitting operations
|
|
138
|
+
- `eis_project.py`: versioned project persistence and fit CSV export
|
|
139
|
+
- `eis_gui.py`: Tk interface, plot rendering, and event coordination
|
|
140
|
+
- `main.py`: command-line entry point
|
eisyfit-0.1.2/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
## EIS Fitting
|
|
2
|
+
|
|
3
|
+
EIS Fitting is a desktop application for exploring, cleaning, and fitting
|
|
4
|
+
electrochemical impedance spectroscopy (EIS) data exported by BioLogic as
|
|
5
|
+
`.mpt` (PEIS) files. It combines an interactive Nyquist plot with circuit
|
|
6
|
+
parameter editing, cycle-aware analysis, DRT-assisted outlier detection, and
|
|
7
|
+
batch fitting, so spectra can be reviewed and fitted without leaving the GUI.
|
|
8
|
+
|
|
9
|
+
The application:
|
|
10
|
+
|
|
11
|
+
- Supports multiple files and multiple measurement cycles
|
|
12
|
+
- Displays spectra beside an editable equivalent-circuit parameter table
|
|
13
|
+
- Lets you include or exclude individual points while preserving cycle state
|
|
14
|
+
- Runs fitting and outlier detection in the background so the GUI stays responsive
|
|
15
|
+
- Uses `bayes-drt2` ridge analysis to identify outliers and initialize circuit fits
|
|
16
|
+
- Compares measured and fitted points at identical frequencies with residual connectors
|
|
17
|
+
- Provides a sortable spectra explorer for cycle and acquisition metadata
|
|
18
|
+
- Exports fitted parameters and spectrum metadata to CSV and a ready-to-run Python script
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### Requirements
|
|
23
|
+
|
|
24
|
+
- Windows, macOS, or Linux with a desktop environment
|
|
25
|
+
- Python 3.14 or newer
|
|
26
|
+
- [`uv`](https://docs.astral.sh/uv/getting-started/installation/), the environment and dependency manager used by this project
|
|
27
|
+
|
|
28
|
+
### Install from GitHub
|
|
29
|
+
|
|
30
|
+
In PowerShell, Terminal, or a shell:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
git clone https://github.com/jaroslav-herman/EIS_fitting.git
|
|
34
|
+
cd EIS_fitting
|
|
35
|
+
uv sync
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`uv sync` creates the project environment and installs the versions recorded
|
|
39
|
+
in `uv.lock`, including the required EIS and DRT libraries. Re-run it after
|
|
40
|
+
pulling changes to update the environment.
|
|
41
|
+
|
|
42
|
+
### Start the application
|
|
43
|
+
|
|
44
|
+
From the repository directory, run:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
uv run python main.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To open a BioLogic file immediately and select a cycle:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
uv run python main.py PEIS_at_N2_flow_80_sccm_automated_01_PEIS.mpt --cycle 1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
On Windows, `run_gui.cmd` can also be double-clicked. It checks that `uv` is
|
|
57
|
+
available and starts the application from the repository directory.
|
|
58
|
+
|
|
59
|
+
If `uv` is not available, install it using the official instructions linked
|
|
60
|
+
above, then open a new terminal and repeat `uv sync`.
|
|
61
|
+
|
|
62
|
+
## Run
|
|
63
|
+
|
|
64
|
+
The command-line entry point also accepts options for the initial control
|
|
65
|
+
channel, outlier threshold, and equivalent-circuit model:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
uv run python main.py PEIS_at_N2_flow_80_sccm_automated_01_PEIS.mpt --cycle 1 --threshold 1.0 --circuit "R0-L0-p(R1,CPE1)"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### GUI controls
|
|
72
|
+
|
|
73
|
+
- Click a point: toggle include/exclude
|
|
74
|
+
- Cycle arrows or selector: move between cycles
|
|
75
|
+
- Spectra explorer: select a spectrum or sort rows by source file, cycle, voltage, current, point count, or frequency limits
|
|
76
|
+
- Fitting model: select a preset or enter an `impedance.py` circuit string, then set it
|
|
77
|
+
- Apply to current cycle: use the frequency range only for the displayed spectrum
|
|
78
|
+
- Apply to all cycles: use the frequency range for every spectrum in the file
|
|
79
|
+
- Outliers: current: run the bayes-drt2 ridge analysis on active points only, apply its outlier mask without reactivating excluded points, and initialize the EEC fit from its ohmic resistance, inductance, and strongest DRT peaks
|
|
80
|
+
- Outliers: all cycles: perform the same active-point analysis and EEC initialization for every spectrum in the background
|
|
81
|
+
- Fit spectrum: fit included points with the parameter table values
|
|
82
|
+
- Batch fit from current: fit toward higher cycles, using each result to initialize the next
|
|
83
|
+
- Export to Python: save fitted parameters and spectrum metadata from all loaded files to CSV, create a ready-to-run pandas script, and open it in VS Code or another available editor
|
|
84
|
+
- Reset points: include all points and clear detected outliers
|
|
85
|
+
|
|
86
|
+
The **File** menu contains additive multi-file data import, project load/save, mask saving, fit-parameter and Python-workspace exports, and exit commands. Import accepts several `.mpt` files at once and keeps previously loaded spectra.
|
|
87
|
+
|
|
88
|
+
The **Fit** menu fits the selected spectrum or batch-fits upward/downward through the explorer's visible order. Metadata-limited batches use the last clicked numeric explorer column and stop at its nearest available target value.
|
|
89
|
+
|
|
90
|
+
Outlier detection never runs automatically. Cycle state is retained while the GUI is open.
|
|
91
|
+
|
|
92
|
+
Keyboard shortcuts:
|
|
93
|
+
|
|
94
|
+
- `Alt+A`: copy fitted values from the previous cycle into the current initial values
|
|
95
|
+
- `Alt+D`: copy fitted values from the next cycle into the current initial values
|
|
96
|
+
- `Alt+S`: fit the selected spectrum
|
|
97
|
+
- `Ctrl+O`: import another `.mpt` data file
|
|
98
|
+
- `Ctrl+Shift+O`: load an EIS fitting project
|
|
99
|
+
- `Ctrl+S`: save the EIS fitting project
|
|
100
|
+
|
|
101
|
+
### Code structure
|
|
102
|
+
|
|
103
|
+
- `eis_model.py`: GUI-independent project and per-cycle state
|
|
104
|
+
- `eis_services.py`: file loading, outlier detection, and fitting operations
|
|
105
|
+
- `eis_project.py`: versioned project persistence and fit CSV export
|
|
106
|
+
- `eis_gui.py`: Tk interface, plot rendering, and event coordination
|
|
107
|
+
- `main.py`: command-line entry point
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Physical circuit structure helpers.
|
|
2
|
+
|
|
3
|
+
Circuit element suffixes are identifiers, not part of the physical topology.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CircuitNode:
|
|
14
|
+
kind: str
|
|
15
|
+
value: str | None = None
|
|
16
|
+
children: tuple["CircuitNode", ...] = ()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _group(kind: str, children: list[CircuitNode]) -> CircuitNode:
|
|
20
|
+
flattened: list[CircuitNode] = []
|
|
21
|
+
for child in children:
|
|
22
|
+
if child.kind == kind:
|
|
23
|
+
flattened.extend(child.children)
|
|
24
|
+
else:
|
|
25
|
+
flattened.append(child)
|
|
26
|
+
if len(flattened) == 1:
|
|
27
|
+
return flattened[0]
|
|
28
|
+
return CircuitNode(kind, children=tuple(flattened))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Parser:
|
|
32
|
+
def __init__(self, text: str):
|
|
33
|
+
self.text = re.sub(r"\s+", "", text)
|
|
34
|
+
self.index = 0
|
|
35
|
+
|
|
36
|
+
def parse(self) -> CircuitNode:
|
|
37
|
+
if not self.text:
|
|
38
|
+
raise ValueError("circuit is empty")
|
|
39
|
+
node = self._parse_series()
|
|
40
|
+
if self.index != len(self.text):
|
|
41
|
+
raise ValueError(f"unexpected circuit text at position {self.index}")
|
|
42
|
+
return node
|
|
43
|
+
|
|
44
|
+
def _parse_series(self) -> CircuitNode:
|
|
45
|
+
children = [self._parse_term()]
|
|
46
|
+
while self.index < len(self.text) and self.text[self.index] == "-":
|
|
47
|
+
self.index += 1
|
|
48
|
+
children.append(self._parse_term())
|
|
49
|
+
return _group("series", children)
|
|
50
|
+
|
|
51
|
+
def _parse_term(self) -> CircuitNode:
|
|
52
|
+
if self.text.startswith("p(", self.index):
|
|
53
|
+
self.index += 2
|
|
54
|
+
children = [self._parse_series_until(",", ")")]
|
|
55
|
+
while self.index < len(self.text) and self.text[self.index] == ",":
|
|
56
|
+
self.index += 1
|
|
57
|
+
children.append(self._parse_series_until(",", ")"))
|
|
58
|
+
if self.index >= len(self.text) or self.text[self.index] != ")":
|
|
59
|
+
raise ValueError("unclosed parallel circuit block")
|
|
60
|
+
self.index += 1
|
|
61
|
+
return _group("parallel", children)
|
|
62
|
+
start = self.index
|
|
63
|
+
while self.index < len(self.text) and self.text[self.index] not in "-,)":
|
|
64
|
+
self.index += 1
|
|
65
|
+
if start == self.index:
|
|
66
|
+
raise ValueError(f"expected circuit element at position {self.index}")
|
|
67
|
+
token = self.text[start:self.index]
|
|
68
|
+
element_type = re.match(r"[A-Za-z]+", token)
|
|
69
|
+
if element_type is None:
|
|
70
|
+
raise ValueError(f"invalid circuit element {token!r}")
|
|
71
|
+
return CircuitNode("element", token)
|
|
72
|
+
|
|
73
|
+
def _parse_series_until(self, *terminators: str) -> CircuitNode:
|
|
74
|
+
start = self.index
|
|
75
|
+
children = [self._parse_term()]
|
|
76
|
+
while self.index < len(self.text) and self.text[self.index] == "-":
|
|
77
|
+
if self.index + 1 < len(self.text) and self.text[self.index + 1] in terminators:
|
|
78
|
+
break
|
|
79
|
+
self.index += 1
|
|
80
|
+
children.append(self._parse_term())
|
|
81
|
+
if self.index == start:
|
|
82
|
+
raise ValueError("empty circuit block")
|
|
83
|
+
return _group("series", children)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_circuit(circuit: str) -> CircuitNode:
|
|
87
|
+
return _Parser(circuit).parse()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _element_type(token: str) -> str:
|
|
91
|
+
match = re.match(r"[A-Za-z]+", token)
|
|
92
|
+
return match.group(0).casefold() if match else token.casefold()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def canonical_circuit(circuit: str) -> tuple:
|
|
96
|
+
def canonical(node: CircuitNode) -> tuple:
|
|
97
|
+
if node.kind == "element":
|
|
98
|
+
return ("element", _element_type(node.value or ""))
|
|
99
|
+
children = tuple(sorted((canonical(child) for child in node.children), key=repr))
|
|
100
|
+
return (node.kind, children)
|
|
101
|
+
|
|
102
|
+
return canonical(parse_circuit(circuit))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def circuits_equivalent(first: str | None, second: str | None) -> bool:
|
|
106
|
+
try:
|
|
107
|
+
return canonical_circuit(first or "") == canonical_circuit(second or "")
|
|
108
|
+
except ValueError:
|
|
109
|
+
return re.sub(r"\s+", "", str(first or "")).casefold() == re.sub(
|
|
110
|
+
r"\s+", "", str(second or "")
|
|
111
|
+
).casefold()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parameter_name_mapping(source: str, target: str) -> dict[str, str] | None:
|
|
115
|
+
"""Return source-element-name -> target-element-name correspondence."""
|
|
116
|
+
source_root = parse_circuit(source)
|
|
117
|
+
target_root = parse_circuit(target)
|
|
118
|
+
if canonical_circuit(source) != canonical_circuit(target):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
mapping: dict[str, str] = {}
|
|
122
|
+
|
|
123
|
+
def walk(source_node: CircuitNode, target_node: CircuitNode) -> None:
|
|
124
|
+
if source_node.kind == "element" and target_node.kind == "element":
|
|
125
|
+
mapping[source_node.value or ""] = target_node.value or ""
|
|
126
|
+
return
|
|
127
|
+
source_children = sorted(
|
|
128
|
+
source_node.children,
|
|
129
|
+
key=lambda child: (repr(canonical_circuit_from_node(child)), child.value or ""),
|
|
130
|
+
)
|
|
131
|
+
target_children = sorted(
|
|
132
|
+
target_node.children,
|
|
133
|
+
key=lambda child: (repr(canonical_circuit_from_node(child)), child.value or ""),
|
|
134
|
+
)
|
|
135
|
+
for source_child, target_child in zip(source_children, target_children):
|
|
136
|
+
walk(source_child, target_child)
|
|
137
|
+
|
|
138
|
+
def canonical_circuit_from_node(node: CircuitNode) -> tuple:
|
|
139
|
+
if node.kind == "element":
|
|
140
|
+
return ("element", _element_type(node.value or ""))
|
|
141
|
+
return (
|
|
142
|
+
node.kind,
|
|
143
|
+
tuple(sorted((canonical_circuit_from_node(child) for child in node.children), key=repr)),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
walk(source_root, target_root)
|
|
147
|
+
return mapping
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def map_parameter_name(name: str, element_mapping: dict[str, str]) -> str | None:
|
|
151
|
+
for source_element, target_element in sorted(element_mapping.items(), key=lambda item: -len(item[0])):
|
|
152
|
+
if name == source_element:
|
|
153
|
+
return target_element
|
|
154
|
+
prefix = source_element + "_"
|
|
155
|
+
if name.startswith(prefix):
|
|
156
|
+
return target_element + name[len(source_element):]
|
|
157
|
+
return None
|