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.
Files changed (81) hide show
  1. eisyfit-0.1.2/PKG-INFO +140 -0
  2. eisyfit-0.1.2/README.md +107 -0
  3. eisyfit-0.1.2/circuit_structure.py +157 -0
  4. eisyfit-0.1.2/eis_gui.py +14502 -0
  5. eisyfit-0.1.2/eis_model.py +381 -0
  6. eisyfit-0.1.2/eis_project.py +892 -0
  7. eisyfit-0.1.2/eis_services.py +1774 -0
  8. eisyfit-0.1.2/eisyfit.egg-info/PKG-INFO +140 -0
  9. eisyfit-0.1.2/eisyfit.egg-info/SOURCES.txt +79 -0
  10. eisyfit-0.1.2/eisyfit.egg-info/dependency_links.txt +1 -0
  11. eisyfit-0.1.2/eisyfit.egg-info/entry_points.txt +2 -0
  12. eisyfit-0.1.2/eisyfit.egg-info/requires.txt +13 -0
  13. eisyfit-0.1.2/eisyfit.egg-info/top_level.txt +11 -0
  14. eisyfit-0.1.2/explorer_filter.py +119 -0
  15. eisyfit-0.1.2/extract_relaxis.py +401 -0
  16. eisyfit-0.1.2/main.py +50 -0
  17. eisyfit-0.1.2/ml/__init__.py +22 -0
  18. eisyfit-0.1.2/ml/audit_manual_frequency_range.py +198 -0
  19. eisyfit-0.1.2/ml/automatic_preprocessing.py +111 -0
  20. eisyfit-0.1.2/ml/benchmark_outlier.py +57 -0
  21. eisyfit-0.1.2/ml/benchmark_point_validity.py +50 -0
  22. eisyfit-0.1.2/ml/complete_ml_range_cache.py +162 -0
  23. eisyfit-0.1.2/ml/continue_sample178_outliers.py +43 -0
  24. eisyfit-0.1.2/ml/dataset.py +233 -0
  25. eisyfit-0.1.2/ml/diagnostics.py +55 -0
  26. eisyfit-0.1.2/ml/evaluate_frequency_limit_ml.py +154 -0
  27. eisyfit-0.1.2/ml/evaluate_frequency_selection_voltage.py +101 -0
  28. eisyfit-0.1.2/ml/evaluate_low_frequency_selector.py +149 -0
  29. eisyfit-0.1.2/ml/evaluate_point_validity.py +162 -0
  30. eisyfit-0.1.2/ml/evaluate_stage4_parameters.py +349 -0
  31. eisyfit-0.1.2/ml/evaluate_staged_datasets.py +93 -0
  32. eisyfit-0.1.2/ml/evaluate_topology_automatic_preprocessing.py +136 -0
  33. eisyfit-0.1.2/ml/finalize_topology_for_unseen_178.py +265 -0
  34. eisyfit-0.1.2/ml/frequency_limit_ml.py +85 -0
  35. eisyfit-0.1.2/ml/frequency_range.py +147 -0
  36. eisyfit-0.1.2/ml/frequency_range_visualization.py +65 -0
  37. eisyfit-0.1.2/ml/gui_results.py +384 -0
  38. eisyfit-0.1.2/ml/infer_sample178_new_frequency.py +73 -0
  39. eisyfit-0.1.2/ml/infer_unseen_sample_178.py +139 -0
  40. eisyfit-0.1.2/ml/inspect_eisfit.py +114 -0
  41. eisyfit-0.1.2/ml/l0_decision.py +213 -0
  42. eisyfit-0.1.2/ml/low_frequency_selector.py +164 -0
  43. eisyfit-0.1.2/ml/metrics.py +126 -0
  44. eisyfit-0.1.2/ml/migrate_embedded_results.py +82 -0
  45. eisyfit-0.1.2/ml/outlier_cache.py +220 -0
  46. eisyfit-0.1.2/ml/parameter_limits.py +92 -0
  47. eisyfit-0.1.2/ml/parameter_prediction.py +164 -0
  48. eisyfit-0.1.2/ml/pipeline.py +229 -0
  49. eisyfit-0.1.2/ml/point_validity.py +251 -0
  50. eisyfit-0.1.2/ml/preprocessing.py +72 -0
  51. eisyfit-0.1.2/ml/results_schema.py +51 -0
  52. eisyfit-0.1.2/ml/run_frequency_range.py +56 -0
  53. eisyfit-0.1.2/ml/run_frequency_topology.py +55 -0
  54. eisyfit-0.1.2/ml/run_stage4a_parameters.py +193 -0
  55. eisyfit-0.1.2/ml/run_stage4b_parameters.py +444 -0
  56. eisyfit-0.1.2/ml/run_stage5a_initial_parameters.py +511 -0
  57. eisyfit-0.1.2/ml/run_staged_topology.py +37 -0
  58. eisyfit-0.1.2/ml/run_topology.py +51 -0
  59. eisyfit-0.1.2/ml/run_topology_cached.py +183 -0
  60. eisyfit-0.1.2/ml/runtime_inference.py +380 -0
  61. eisyfit-0.1.2/ml/staged_topology.py +154 -0
  62. eisyfit-0.1.2/ml/topology_classifier.py +142 -0
  63. eisyfit-0.1.2/plot_export.py +90 -0
  64. eisyfit-0.1.2/pyproject.toml +68 -0
  65. eisyfit-0.1.2/setup.cfg +4 -0
  66. eisyfit-0.1.2/spectrum_simulator.py +74 -0
  67. eisyfit-0.1.2/tests/test_batch_stop.py +127 -0
  68. eisyfit-0.1.2/tests/test_circuit_structure.py +38 -0
  69. eisyfit-0.1.2/tests/test_explorer_filter.py +53 -0
  70. eisyfit-0.1.2/tests/test_fit_optimizers.py +49 -0
  71. eisyfit-0.1.2/tests/test_low_frequency_selector.py +53 -0
  72. eisyfit-0.1.2/tests/test_ml.py +150 -0
  73. eisyfit-0.1.2/tests/test_ml_results_architecture.py +50 -0
  74. eisyfit-0.1.2/tests/test_mpr_import.py +198 -0
  75. eisyfit-0.1.2/tests/test_plot_export.py +45 -0
  76. eisyfit-0.1.2/tests/test_point_validity.py +128 -0
  77. eisyfit-0.1.2/tests/test_relaxis_mask_limits_models.py +137 -0
  78. eisyfit-0.1.2/tests/test_relaxis_project_load.py +83 -0
  79. eisyfit-0.1.2/tests/test_spectrum_simulator.py +46 -0
  80. eisyfit-0.1.2/tests/test_stage4_parameter_prediction.py +89 -0
  81. 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
@@ -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