rosetta-eclab 0.1.2__tar.gz → 0.1.4__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.
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/PKG-INFO +7 -5
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/README.md +5 -3
- rosetta_eclab-0.1.4/namer/galvani_ref.py +67 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/library.py +7 -3
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/mprfile.py +33 -14
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/pyproject.toml +6 -3
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/PKG-INFO +7 -5
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/SOURCES.txt +3 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/corpus.py +2 -2
- rosetta_eclab-0.1.4/spike2/galvani_0.5.0_colmap.json +129 -0
- rosetta_eclab-0.1.4/spike2/galvani_snapshot.py +48 -0
- rosetta_eclab-0.1.4/tests/test_cli.py +158 -0
- rosetta_eclab-0.1.2/tests/test_cli.py +0 -82
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/LICENSE +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/__init__.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/cli.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/data/stats.json +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/data/stats_features.npy +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/debug.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/engine.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/evaluate.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/fallback.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/golden.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/misses.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/dependency_links.txt +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/entry_points.txt +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/requires.txt +1 -1
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/top_level.txt +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/setup.cfg +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/__init__.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/agent.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/api_check.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/baselines.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/dsl.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/elim.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/features.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/lock.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/run.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/score.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/tasks.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/vocab.py +0 -0
- {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/tests/test_relations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rosetta-eclab
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Name the columns of BioLogic EC-Lab .mpr files that your parser does not know - no language model, evidence for every answer
|
|
5
5
|
Author: Nataniel Ataseven
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -18,9 +18,9 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: numpy>=2.0
|
|
20
20
|
Requires-Dist: yadg==7.0.1
|
|
21
|
-
Requires-Dist: galvani==0.5.0
|
|
22
21
|
Provides-Extra: test
|
|
23
22
|
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Requires-Dist: galvani==0.5.0; extra == "test"
|
|
24
24
|
Dynamic: license-file
|
|
25
25
|
|
|
26
26
|
# Rosetta
|
|
@@ -52,6 +52,8 @@ Python 3.10 or later:
|
|
|
52
52
|
pip install rosetta-eclab
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
Rosetta does not install galvani. If galvani or datalab-org-galvani is installed, "unknown to galvani" means unknown to the galvani you actually have. Without either, Rosetta uses a frozen copy of galvani 0.5.0's column map and says so. The names Rosetta gives never depend on which galvani is installed.
|
|
56
|
+
|
|
55
57
|
To run the tests or reproduce the benchmark, clone the repository instead:
|
|
56
58
|
|
|
57
59
|
```sh
|
|
@@ -96,7 +98,7 @@ peis.issue_225.with_ece.mpr
|
|
|
96
98
|
every column ID is in galvani's column map; nothing to name
|
|
97
99
|
```
|
|
98
100
|
|
|
99
|
-
The fix was released in datalab-org-galvani 0.6.0 ([galvani-mirror#11](https://github.com/datalab-org/galvani-mirror/issues/11)). IDs that are missing from the map altogether, such as 880 or 883 and 884, are what Rosetta names.
|
|
101
|
+
The fix was released in datalab-org-galvani 0.6.0 ([galvani-mirror#11](https://github.com/datalab-org/galvani-mirror/issues/11)). Rosetta prints the note only when the installed galvani reads the low byte. IDs that are missing from the map altogether, such as 880 or 883 and 884, are what Rosetta names.
|
|
100
102
|
|
|
101
103
|
## How it works
|
|
102
104
|
|
|
@@ -137,7 +139,7 @@ The namer improved between hold-outs, so the total includes earlier versions. Th
|
|
|
137
139
|
- **Values EC-Lab computes after the fact** cannot be reconstructed reliably. For example, a capacitance is held per segment.
|
|
138
140
|
- **Noise-like DC currents** in impedance records, and **external analog inputs** with no sibling column, usually get ABSTAIN.
|
|
139
141
|
- **Two quantities with the same definition in a file** cannot be told apart from the data. In galvanostatic cycling, for example, `dQ` and `Q charge or discharge` reset at the same points. A prior over how common each name is for the technique decides between them, and the alternative is shown.
|
|
140
|
-
- **Supported formats:** EC-Lab data-module versions 2, 3, 10 and 11.
|
|
142
|
+
- **Supported formats:** EC-Lab data-module versions 0, 2, 3, 10 and 11. Version 0 (older EC-Lab) is read with galvani's offsets, because yadg does not read it. All 59 public version-0 files decode byte for byte like galvani 0.5.0, and the two with an EC-Lab text export match it. The file technique is not read from version-0 files, so the namer works without it.
|
|
141
143
|
|
|
142
144
|
## Reproduce the benchmark
|
|
143
145
|
|
|
@@ -155,4 +157,4 @@ pytest tests
|
|
|
155
157
|
|
|
156
158
|
## Data and licence
|
|
157
159
|
|
|
158
|
-
Rosetta is free software under the **GNU General Public License v3.0 or later** (see `LICENSE`). It uses [yadg](https://github.com/dgbowl/yadg) and [galvani](https://github.com/echemdata/galvani)
|
|
160
|
+
Rosetta is free software under the **GNU General Public License v3.0 or later** (see `LICENSE`). It uses [yadg](https://github.com/dgbowl/yadg) (GPL), and `spike2/galvani_0.5.0_colmap.json` is a copy of the column map of [galvani](https://github.com/echemdata/galvani) 0.5.0 (GPL-3.0-or-later). Measurement files are downloaded at run time and never committed. Sources, licences and attributions are listed in [`DATA_SOURCES.md`](https://github.com/Nataniel0212/rosetta-eclab/blob/main/DATA_SOURCES.md). Rosetta is not affiliated with BioLogic.
|
|
@@ -27,6 +27,8 @@ Python 3.10 or later:
|
|
|
27
27
|
pip install rosetta-eclab
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Rosetta does not install galvani. If galvani or datalab-org-galvani is installed, "unknown to galvani" means unknown to the galvani you actually have. Without either, Rosetta uses a frozen copy of galvani 0.5.0's column map and says so. The names Rosetta gives never depend on which galvani is installed.
|
|
31
|
+
|
|
30
32
|
To run the tests or reproduce the benchmark, clone the repository instead:
|
|
31
33
|
|
|
32
34
|
```sh
|
|
@@ -71,7 +73,7 @@ peis.issue_225.with_ece.mpr
|
|
|
71
73
|
every column ID is in galvani's column map; nothing to name
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
The fix was released in datalab-org-galvani 0.6.0 ([galvani-mirror#11](https://github.com/datalab-org/galvani-mirror/issues/11)). IDs that are missing from the map altogether, such as 880 or 883 and 884, are what Rosetta names.
|
|
76
|
+
The fix was released in datalab-org-galvani 0.6.0 ([galvani-mirror#11](https://github.com/datalab-org/galvani-mirror/issues/11)). Rosetta prints the note only when the installed galvani reads the low byte. IDs that are missing from the map altogether, such as 880 or 883 and 884, are what Rosetta names.
|
|
75
77
|
|
|
76
78
|
## How it works
|
|
77
79
|
|
|
@@ -112,7 +114,7 @@ The namer improved between hold-outs, so the total includes earlier versions. Th
|
|
|
112
114
|
- **Values EC-Lab computes after the fact** cannot be reconstructed reliably. For example, a capacitance is held per segment.
|
|
113
115
|
- **Noise-like DC currents** in impedance records, and **external analog inputs** with no sibling column, usually get ABSTAIN.
|
|
114
116
|
- **Two quantities with the same definition in a file** cannot be told apart from the data. In galvanostatic cycling, for example, `dQ` and `Q charge or discharge` reset at the same points. A prior over how common each name is for the technique decides between them, and the alternative is shown.
|
|
115
|
-
- **Supported formats:** EC-Lab data-module versions 2, 3, 10 and 11.
|
|
117
|
+
- **Supported formats:** EC-Lab data-module versions 0, 2, 3, 10 and 11. Version 0 (older EC-Lab) is read with galvani's offsets, because yadg does not read it. All 59 public version-0 files decode byte for byte like galvani 0.5.0, and the two with an EC-Lab text export match it. The file technique is not read from version-0 files, so the namer works without it.
|
|
116
118
|
|
|
117
119
|
## Reproduce the benchmark
|
|
118
120
|
|
|
@@ -130,4 +132,4 @@ pytest tests
|
|
|
130
132
|
|
|
131
133
|
## Data and licence
|
|
132
134
|
|
|
133
|
-
Rosetta is free software under the **GNU General Public License v3.0 or later** (see `LICENSE`). It uses [yadg](https://github.com/dgbowl/yadg) and [galvani](https://github.com/echemdata/galvani)
|
|
135
|
+
Rosetta is free software under the **GNU General Public License v3.0 or later** (see `LICENSE`). It uses [yadg](https://github.com/dgbowl/yadg) (GPL), and `spike2/galvani_0.5.0_colmap.json` is a copy of the column map of [galvani](https://github.com/echemdata/galvani) 0.5.0 (GPL-3.0-or-later). Measurement files are downloaded at run time and never committed. Sources, licences and attributions are listed in [`DATA_SOURCES.md`](https://github.com/Nataniel0212/rosetta-eclab/blob/main/DATA_SOURCES.md). Rosetta is not affiliated with BioLogic.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""galvani as a reference parser, without depending on one particular galvani.
|
|
2
|
+
|
|
3
|
+
Two distributions install the module `galvani`: galvani (echemdata, 0.5.0 on PyPI)
|
|
4
|
+
and datalab-org-galvani (0.6.0 reads EC-Lab 11.50+ column IDs in full). Rosetta
|
|
5
|
+
does not install either, so it never overwrites the one a user already has.
|
|
6
|
+
|
|
7
|
+
reference() returns the installed galvani's own column map, and whether that
|
|
8
|
+
galvani reads only the low byte of each column ID in the 11.50+ layout. Without
|
|
9
|
+
galvani installed, the frozen 0.5.0 map (spike2/galvani_snapshot.py) stands in.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from spike2.galvani_snapshot import snapshot_map
|
|
18
|
+
|
|
19
|
+
LOW_BYTE_IDIOM = "column_types[1::2]" # galvani 0.5.0 keeps every second byte of the ID array
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Reference:
|
|
24
|
+
colmap: dict[int, tuple[str, str]]
|
|
25
|
+
label: str # e.g. "galvani 0.5.0", "datalab-org-galvani 0.6.0"
|
|
26
|
+
low_byte: bool | None # True: reads only the low byte of 11.50+ column IDs; None: cannot tell
|
|
27
|
+
installed: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _label() -> str:
|
|
31
|
+
from importlib.metadata import PackageNotFoundError, packages_distributions, version
|
|
32
|
+
|
|
33
|
+
parts = []
|
|
34
|
+
for d in sorted(set(packages_distributions().get("galvani", []))):
|
|
35
|
+
try:
|
|
36
|
+
parts.append(f"{d} {version(d)}")
|
|
37
|
+
except PackageNotFoundError:
|
|
38
|
+
parts.append(d)
|
|
39
|
+
if not parts:
|
|
40
|
+
return "galvani"
|
|
41
|
+
if len(parts) == 1:
|
|
42
|
+
return parts[0]
|
|
43
|
+
return f"galvani (installed by both {' and '.join(parts)}; its files are from whichever was installed last)"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _reads_low_byte(biologic) -> bool | None:
|
|
47
|
+
try:
|
|
48
|
+
src = inspect.getsource(biologic.MPRfile.__init__)
|
|
49
|
+
except (OSError, TypeError, AttributeError):
|
|
50
|
+
return None
|
|
51
|
+
return LOW_BYTE_IDIOM in src
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def installed() -> Reference | None:
|
|
55
|
+
try:
|
|
56
|
+
from galvani import BioLogic
|
|
57
|
+
except ImportError:
|
|
58
|
+
return None
|
|
59
|
+
colmap = {int(k): (v[0], v[1]) for k, v in BioLogic.VMPdata_colID_dtype_map.items()}
|
|
60
|
+
return Reference(colmap, _label(), _reads_low_byte(BioLogic), installed=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reference() -> Reference:
|
|
64
|
+
ref = installed()
|
|
65
|
+
if ref is not None:
|
|
66
|
+
return ref
|
|
67
|
+
return Reference(snapshot_map(), "galvani 0.5.0", low_byte=True, installed=False)
|
|
@@ -451,15 +451,15 @@ def is_code(ctx, lo=0, hi=200, max_distinct=8) -> float:
|
|
|
451
451
|
# --- expected storage types (public parser maps: yadg 7.0.1 and galvani 0.5.0) ---------
|
|
452
452
|
|
|
453
453
|
def expected_dtypes() -> dict[str, set[str]]:
|
|
454
|
-
from galvani import BioLogic
|
|
455
454
|
from yadg.extractors.eclab.mpr_columns import data_columns
|
|
456
455
|
|
|
457
456
|
from spike2 import vocab
|
|
457
|
+
from spike2.galvani_snapshot import snapshot_map # frozen 0.5.0 map, whichever galvani is installed
|
|
458
458
|
|
|
459
459
|
out: dict[str, set[str]] = {}
|
|
460
460
|
for dt, name, _unit in data_columns.values():
|
|
461
461
|
out.setdefault(vocab.canonical(name), set()).add(dt)
|
|
462
|
-
for full, dt in
|
|
462
|
+
for full, dt in snapshot_map().values():
|
|
463
463
|
name = full.split("/")[0] if not full.startswith("Q charge/discharge") else "Q charge/discharge"
|
|
464
464
|
out.setdefault(vocab.canonical(name), set()).add(dt)
|
|
465
465
|
return out
|
|
@@ -561,7 +561,11 @@ def build() -> dict[str, Quantity]:
|
|
|
561
561
|
q("z cycle", shapes=[Shape("integer counter in a sequence with impedance", lambda c: 0.75 * is_counter(c) if c.col("freq") is not None else 0.0)])
|
|
562
562
|
q("Ns", shapes=[Shape("sequence index from 0 that changes with the control", lambda c: ns_shape(c))])
|
|
563
563
|
q("I Range", shapes=[Shape("instrument range code (few values, not starting at 0)", lambda c: irange_shape(c))])
|
|
564
|
-
q("Pwe", relations=[
|
|
564
|
+
q("Pwe", relations=[
|
|
565
|
+
Relation("Ewe*I", ("Ewe", CURRENT), lambda c: _e(c) * _i(c), [W_PER_V_MA]),
|
|
566
|
+
# some older EC-Lab files store the power's magnitude (galvani's bio_logic1.mpr and bio_logic3.mpr)
|
|
567
|
+
Relation("|Ewe*I|", ("Ewe", CURRENT), lambda c: np.abs(_e(c) * _i(c)), [W_PER_V_MA]),
|
|
568
|
+
])
|
|
565
569
|
q("Ewe", relations=[
|
|
566
570
|
Relation("Pwe/I", ("Pwe", ("I", "dq", "(Q-Qo)")), lambda c: c.col("Pwe") / _i(c), [1000.0]),
|
|
567
571
|
Relation("follows potential control", ("control",), lambda c: c.col("control"), [1.0]),
|
|
@@ -16,6 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
|
|
19
|
+
from . import galvani_ref
|
|
20
|
+
|
|
19
21
|
FLAG_IDS = (1, 2, 3, 21, 31, 65)
|
|
20
22
|
CANDIDATES = {1: ["|u1"], 2: ["<u2", "<i2"], 4: ["<f4", "<u4", "<i4"], 8: ["<f8", "<u8", "<i8"]}
|
|
21
23
|
|
|
@@ -79,25 +81,29 @@ def _plausibility(v: np.ndarray, dtype: str) -> float:
|
|
|
79
81
|
return 0.9 if span < 2 ** (8 * width - 4) else 0.2
|
|
80
82
|
|
|
81
83
|
|
|
82
|
-
def
|
|
83
|
-
"""
|
|
84
|
+
def _galvani_notes(full_ids: list[int], ref: galvani_ref.Reference, layout_11_50: bool) -> list[str]:
|
|
85
|
+
"""What the reference galvani makes of this file, beyond plain unknown IDs.
|
|
84
86
|
|
|
87
|
+
galvani 0.5.0 reads only the low byte of each column ID in the 11.50+ layout.
|
|
85
88
|
IDs above 255 then become other IDs (471 -> 215), which galvani either does not
|
|
86
89
|
know (it stops) or knows as a different column. Its map repeats a few entries at
|
|
87
90
|
the low-byte ID (211 = 467), so only IDs whose low-byte entry differs are listed.
|
|
88
91
|
"""
|
|
89
|
-
|
|
90
|
-
if not
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
notes = []
|
|
93
|
+
if not ref.installed:
|
|
94
|
+
notes.append("galvani is not installed, so 'known to galvani' means the column map of galvani 0.5.0 "
|
|
95
|
+
"(bundled with Rosetta)")
|
|
96
|
+
if layout_11_50 and ref.low_byte:
|
|
97
|
+
gmap = ref.colmap
|
|
98
|
+
misread = [x for x in full_ids if x > 255 and gmap.get(x & 0xFF) != gmap.get(x)]
|
|
99
|
+
if misread:
|
|
100
|
+
pairs = ", ".join(f"{x} as {x & 0xFF}" for x in misread)
|
|
101
|
+
notes.append(f"{ref.label} reads only the low byte of each column ID in this file layout, "
|
|
102
|
+
f"so it misreads {pairs}")
|
|
103
|
+
return notes
|
|
97
104
|
|
|
98
105
|
|
|
99
106
|
def read(path: str | Path, reference: str = "galvani") -> MprData:
|
|
100
|
-
from galvani import BioLogic
|
|
101
107
|
from yadg.extractors.eclab.mpr import parse_columns, process_settings
|
|
102
108
|
|
|
103
109
|
logging.getLogger("yadg").setLevel(logging.CRITICAL)
|
|
@@ -125,16 +131,29 @@ def read(path: str | Path, reference: str = "galvani") -> MprData:
|
|
|
125
131
|
elif version in (2, 3):
|
|
126
132
|
yids = full = np.frombuffer(data, offset=5, dtype="<u2", count=n_columns).tolist()
|
|
127
133
|
offset = 0x195 if version == 2 else 0x196
|
|
134
|
+
elif version == 0: # older EC-Lab files; offsets as in galvani (yadg 7.0.1 does not read version 0)
|
|
135
|
+
if data[5]: # one byte per column ID; all public version-0 files use this layout
|
|
136
|
+
yids = full = list(data[5:5 + n_columns])
|
|
137
|
+
offset = 100
|
|
138
|
+
else: # galvani: EC-Lab 11.50+ writes [0 1 0 3 0 174 ...]; no public file to test against
|
|
139
|
+
yids = full = list(data[6:5 + 2 * n_columns:2])
|
|
140
|
+
offset = 1007
|
|
128
141
|
else:
|
|
129
142
|
raise NotImplementedError(f"data module version {version} is not supported yet")
|
|
143
|
+
if n_points == 0:
|
|
144
|
+
raise ValueError("the file has no data points")
|
|
130
145
|
region = len(data) - offset
|
|
131
|
-
if
|
|
146
|
+
if region % n_points:
|
|
132
147
|
raise ValueError(f"record size is not a whole number of bytes ({region} / {n_points})")
|
|
133
148
|
rec = region // n_points
|
|
134
149
|
|
|
135
150
|
names, dtypes, units, _ = parse_columns(yids, technique) # yadg's map: names for known columns
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
gref = galvani_ref.reference() if reference == "galvani" else None
|
|
152
|
+
gmap = gref.colmap if gref else {}
|
|
153
|
+
notes = _galvani_notes(full, gref, version in (10, 11)) if gref else []
|
|
154
|
+
if version == 0 and reference == "yadg":
|
|
155
|
+
notes.append("yadg 7.0.1 cannot read this file at all (data module version 0); "
|
|
156
|
+
"'known' here means listed in yadg's column map")
|
|
138
157
|
layout = [] # record layout in file order: (raw_id or None for the flags byte, ...)
|
|
139
158
|
for yid, fid in zip(yids, full):
|
|
140
159
|
if yid in FLAG_IDS: # all flag IDs share one byte, at the position of the first one
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rosetta-eclab"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Name the columns of BioLogic EC-Lab .mpr files that your parser does not know - no language model, evidence for every answer"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -19,10 +19,12 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Topic :: Scientific/Engineering :: Chemistry",
|
|
21
21
|
]
|
|
22
|
+
# galvani is not a dependency: two distributions install the module (galvani and datalab-org-galvani),
|
|
23
|
+
# and requiring one would overwrite the other. Rosetta uses whichever is installed, or a frozen copy
|
|
24
|
+
# of galvani 0.5.0's column map (spike2/galvani_0.5.0_colmap.json).
|
|
22
25
|
dependencies = [
|
|
23
26
|
"numpy>=2.0",
|
|
24
27
|
"yadg==7.0.1",
|
|
25
|
-
"galvani==0.5.0",
|
|
26
28
|
]
|
|
27
29
|
|
|
28
30
|
[project.urls]
|
|
@@ -30,7 +32,7 @@ Repository = "https://github.com/Nataniel0212/rosetta-eclab"
|
|
|
30
32
|
Issues = "https://github.com/Nataniel0212/rosetta-eclab/issues"
|
|
31
33
|
|
|
32
34
|
[project.optional-dependencies]
|
|
33
|
-
test = ["pytest"]
|
|
35
|
+
test = ["pytest", "galvani==0.5.0"]
|
|
34
36
|
|
|
35
37
|
[project.scripts]
|
|
36
38
|
rosetta = "rosetta_eclab.cli:main"
|
|
@@ -45,3 +47,4 @@ rosetta_eclab = "namer"
|
|
|
45
47
|
|
|
46
48
|
[tool.setuptools.package-data]
|
|
47
49
|
rosetta_eclab = ["data/*.json", "data/*.npy"]
|
|
50
|
+
spike2 = ["*.json"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rosetta-eclab
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Name the columns of BioLogic EC-Lab .mpr files that your parser does not know - no language model, evidence for every answer
|
|
5
5
|
Author: Nataniel Ataseven
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -18,9 +18,9 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: numpy>=2.0
|
|
20
20
|
Requires-Dist: yadg==7.0.1
|
|
21
|
-
Requires-Dist: galvani==0.5.0
|
|
22
21
|
Provides-Extra: test
|
|
23
22
|
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Requires-Dist: galvani==0.5.0; extra == "test"
|
|
24
24
|
Dynamic: license-file
|
|
25
25
|
|
|
26
26
|
# Rosetta
|
|
@@ -52,6 +52,8 @@ Python 3.10 or later:
|
|
|
52
52
|
pip install rosetta-eclab
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
Rosetta does not install galvani. If galvani or datalab-org-galvani is installed, "unknown to galvani" means unknown to the galvani you actually have. Without either, Rosetta uses a frozen copy of galvani 0.5.0's column map and says so. The names Rosetta gives never depend on which galvani is installed.
|
|
56
|
+
|
|
55
57
|
To run the tests or reproduce the benchmark, clone the repository instead:
|
|
56
58
|
|
|
57
59
|
```sh
|
|
@@ -96,7 +98,7 @@ peis.issue_225.with_ece.mpr
|
|
|
96
98
|
every column ID is in galvani's column map; nothing to name
|
|
97
99
|
```
|
|
98
100
|
|
|
99
|
-
The fix was released in datalab-org-galvani 0.6.0 ([galvani-mirror#11](https://github.com/datalab-org/galvani-mirror/issues/11)). IDs that are missing from the map altogether, such as 880 or 883 and 884, are what Rosetta names.
|
|
101
|
+
The fix was released in datalab-org-galvani 0.6.0 ([galvani-mirror#11](https://github.com/datalab-org/galvani-mirror/issues/11)). Rosetta prints the note only when the installed galvani reads the low byte. IDs that are missing from the map altogether, such as 880 or 883 and 884, are what Rosetta names.
|
|
100
102
|
|
|
101
103
|
## How it works
|
|
102
104
|
|
|
@@ -137,7 +139,7 @@ The namer improved between hold-outs, so the total includes earlier versions. Th
|
|
|
137
139
|
- **Values EC-Lab computes after the fact** cannot be reconstructed reliably. For example, a capacitance is held per segment.
|
|
138
140
|
- **Noise-like DC currents** in impedance records, and **external analog inputs** with no sibling column, usually get ABSTAIN.
|
|
139
141
|
- **Two quantities with the same definition in a file** cannot be told apart from the data. In galvanostatic cycling, for example, `dQ` and `Q charge or discharge` reset at the same points. A prior over how common each name is for the technique decides between them, and the alternative is shown.
|
|
140
|
-
- **Supported formats:** EC-Lab data-module versions 2, 3, 10 and 11.
|
|
142
|
+
- **Supported formats:** EC-Lab data-module versions 0, 2, 3, 10 and 11. Version 0 (older EC-Lab) is read with galvani's offsets, because yadg does not read it. All 59 public version-0 files decode byte for byte like galvani 0.5.0, and the two with an EC-Lab text export match it. The file technique is not read from version-0 files, so the namer works without it.
|
|
141
143
|
|
|
142
144
|
## Reproduce the benchmark
|
|
143
145
|
|
|
@@ -155,4 +157,4 @@ pytest tests
|
|
|
155
157
|
|
|
156
158
|
## Data and licence
|
|
157
159
|
|
|
158
|
-
Rosetta is free software under the **GNU General Public License v3.0 or later** (see `LICENSE`). It uses [yadg](https://github.com/dgbowl/yadg) and [galvani](https://github.com/echemdata/galvani)
|
|
160
|
+
Rosetta is free software under the **GNU General Public License v3.0 or later** (see `LICENSE`). It uses [yadg](https://github.com/dgbowl/yadg) (GPL), and `spike2/galvani_0.5.0_colmap.json` is a copy of the column map of [galvani](https://github.com/echemdata/galvani) 0.5.0 (GPL-3.0-or-later). Measurement files are downloaded at run time and never committed. Sources, licences and attributions are listed in [`DATA_SOURCES.md`](https://github.com/Nataniel0212/rosetta-eclab/blob/main/DATA_SOURCES.md). Rosetta is not affiliated with BioLogic.
|
|
@@ -7,6 +7,7 @@ namer/debug.py
|
|
|
7
7
|
namer/engine.py
|
|
8
8
|
namer/evaluate.py
|
|
9
9
|
namer/fallback.py
|
|
10
|
+
namer/galvani_ref.py
|
|
10
11
|
namer/golden.py
|
|
11
12
|
namer/library.py
|
|
12
13
|
namer/misses.py
|
|
@@ -27,6 +28,8 @@ spike2/corpus.py
|
|
|
27
28
|
spike2/dsl.py
|
|
28
29
|
spike2/elim.py
|
|
29
30
|
spike2/features.py
|
|
31
|
+
spike2/galvani_0.5.0_colmap.json
|
|
32
|
+
spike2/galvani_snapshot.py
|
|
30
33
|
spike2/lock.py
|
|
31
34
|
spike2/run.py
|
|
32
35
|
spike2/score.py
|
|
@@ -96,9 +96,9 @@ def decode(path: Path) -> FileData:
|
|
|
96
96
|
raise NotImplementedError(f"data module version {data_version}")
|
|
97
97
|
names, dtypes, units, _flags = parse_columns(ids, technique)
|
|
98
98
|
arr = np.frombuffer(data, offset=offset, dtype=np.dtype(list(zip(names, dtypes))), count=n_points)
|
|
99
|
-
from
|
|
99
|
+
from .galvani_snapshot import snapshot_map
|
|
100
100
|
|
|
101
|
-
km =
|
|
101
|
+
km = snapshot_map() # galvani 0.5.0 (frozen copy): full IDs it knows
|
|
102
102
|
cols, pos = [], 0
|
|
103
103
|
# yadg's own flag test (on its reading of the IDs) decides which entries are flags
|
|
104
104
|
data_ids = [(i, f) for i, f in zip(ids, full_ids) if i not in (1, 2, 3, 21, 31, 65)]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": "galvani 0.5.0 (GPL-3.0), galvani/BioLogic.py, VMPdata_colID_dtype_map: column ID -> [name/unit, numpy dtype]",
|
|
3
|
+
"columns": {
|
|
4
|
+
"4": ["time/s", "<f8"],
|
|
5
|
+
"5": ["control/V/mA", "<f4"],
|
|
6
|
+
"6": ["Ewe/V", "<f4"],
|
|
7
|
+
"7": ["dq/mA.h", "<f8"],
|
|
8
|
+
"8": ["I/mA", "<f4"],
|
|
9
|
+
"9": ["Ece/V", "<f4"],
|
|
10
|
+
"11": ["<I>/mA", "<f8"],
|
|
11
|
+
"13": ["(Q-Qo)/mA.h", "<f8"],
|
|
12
|
+
"16": ["Analog IN 1/V", "<f4"],
|
|
13
|
+
"17": ["Analog IN 2/V", "<f4"],
|
|
14
|
+
"19": ["control/V", "<f4"],
|
|
15
|
+
"20": ["control/mA", "<f4"],
|
|
16
|
+
"23": ["dQ/mA.h", "<f8"],
|
|
17
|
+
"24": ["cycle number", "<f8"],
|
|
18
|
+
"26": ["Rapp/Ohm", "<f4"],
|
|
19
|
+
"27": ["Ewe-Ece/V", "<f4"],
|
|
20
|
+
"32": ["freq/Hz", "<f4"],
|
|
21
|
+
"33": ["|Ewe|/V", "<f4"],
|
|
22
|
+
"34": ["|I|/A", "<f4"],
|
|
23
|
+
"35": ["Phase(Z)/deg", "<f4"],
|
|
24
|
+
"36": ["|Z|/Ohm", "<f4"],
|
|
25
|
+
"37": ["Re(Z)/Ohm", "<f4"],
|
|
26
|
+
"38": ["-Im(Z)/Ohm", "<f4"],
|
|
27
|
+
"39": ["I Range", "<u2"],
|
|
28
|
+
"69": ["R/Ohm", "<f4"],
|
|
29
|
+
"70": ["P/W", "<f4"],
|
|
30
|
+
"74": ["|Energy|/W.h", "<f8"],
|
|
31
|
+
"75": ["Analog OUT/V", "<f4"],
|
|
32
|
+
"76": ["<I>/mA", "<f4"],
|
|
33
|
+
"77": ["<Ewe>/V", "<f4"],
|
|
34
|
+
"78": ["Cs-2/µF-2", "<f4"],
|
|
35
|
+
"96": ["|Ece|/V", "<f4"],
|
|
36
|
+
"98": ["Phase(Zce)/deg", "<f4"],
|
|
37
|
+
"99": ["|Zce|/Ohm", "<f4"],
|
|
38
|
+
"100": ["Re(Zce)/Ohm", "<f4"],
|
|
39
|
+
"101": ["-Im(Zce)/Ohm", "<f4"],
|
|
40
|
+
"123": ["Energy charge/W.h", "<f8"],
|
|
41
|
+
"124": ["Energy discharge/W.h", "<f8"],
|
|
42
|
+
"125": ["Capacitance charge/µF", "<f8"],
|
|
43
|
+
"126": ["Capacitance discharge/µF", "<f8"],
|
|
44
|
+
"131": ["Ns", "<u2"],
|
|
45
|
+
"163": ["|Estack|/V", "<f4"],
|
|
46
|
+
"168": ["Rcmp/Ohm", "<f4"],
|
|
47
|
+
"169": ["Cs/µF", "<f4"],
|
|
48
|
+
"172": ["Cp/µF", "<f4"],
|
|
49
|
+
"173": ["Cp-2/µF-2", "<f4"],
|
|
50
|
+
"174": ["<Ewe>/V", "<f4"],
|
|
51
|
+
"178": ["(Q-Qo)/C", "<f4"],
|
|
52
|
+
"179": ["dQ/C", "<f4"],
|
|
53
|
+
"211": ["Q charge/discharge/mA.h", "<f8"],
|
|
54
|
+
"212": ["half cycle", "<u4"],
|
|
55
|
+
"213": ["z cycle", "<u4"],
|
|
56
|
+
"217": ["THD Ewe/%", "<f4"],
|
|
57
|
+
"218": ["THD I/%", "<f4"],
|
|
58
|
+
"220": ["NSD Ewe/%", "<f4"],
|
|
59
|
+
"221": ["NSD I/%", "<f4"],
|
|
60
|
+
"223": ["NSR Ewe/%", "<f4"],
|
|
61
|
+
"224": ["NSR I/%", "<f4"],
|
|
62
|
+
"230": ["|Ewe h2|/V", "<f4"],
|
|
63
|
+
"231": ["|Ewe h3|/V", "<f4"],
|
|
64
|
+
"232": ["|Ewe h4|/V", "<f4"],
|
|
65
|
+
"233": ["|Ewe h5|/V", "<f4"],
|
|
66
|
+
"234": ["|Ewe h6|/V", "<f4"],
|
|
67
|
+
"235": ["|Ewe h7|/V", "<f4"],
|
|
68
|
+
"236": ["|I h2|/A", "<f4"],
|
|
69
|
+
"237": ["|I h3|/A", "<f4"],
|
|
70
|
+
"238": ["|I h4|/A", "<f4"],
|
|
71
|
+
"239": ["|I h5|/A", "<f4"],
|
|
72
|
+
"240": ["|I h6|/A", "<f4"],
|
|
73
|
+
"241": ["|I h7|/A", "<f4"],
|
|
74
|
+
"242": ["|E2|/V", "<f4"],
|
|
75
|
+
"271": ["Phase(Z1) / deg", "<f4"],
|
|
76
|
+
"272": ["Phase(Z2) / deg", "<f4"],
|
|
77
|
+
"301": ["|Z1|/Ohm", "<f4"],
|
|
78
|
+
"302": ["|Z2|/Ohm", "<f4"],
|
|
79
|
+
"331": ["Re(Z1)/Ohm", "<f4"],
|
|
80
|
+
"332": ["Re(Z2)/Ohm", "<f4"],
|
|
81
|
+
"361": ["-Im(Z1)/Ohm", "<f4"],
|
|
82
|
+
"362": ["-Im(Z2)/Ohm", "<f4"],
|
|
83
|
+
"391": ["<E1>/V", "<f4"],
|
|
84
|
+
"392": ["<E2>/V", "<f4"],
|
|
85
|
+
"422": ["Phase(Zstack)/deg", "<f4"],
|
|
86
|
+
"423": ["|Zstack|/Ohm", "<f4"],
|
|
87
|
+
"424": ["Re(Zstack)/Ohm", "<f4"],
|
|
88
|
+
"425": ["-Im(Zstack)/Ohm", "<f4"],
|
|
89
|
+
"426": ["<Estack>/V", "<f4"],
|
|
90
|
+
"430": ["Phase(Zwe-ce)/deg", "<f4"],
|
|
91
|
+
"431": ["|Zwe-ce|/Ohm", "<f4"],
|
|
92
|
+
"432": ["Re(Zwe-ce)/Ohm", "<f4"],
|
|
93
|
+
"433": ["-Im(Zwe-ce)/Ohm", "<f4"],
|
|
94
|
+
"434": ["(Q-Qo)/C", "<f4"],
|
|
95
|
+
"435": ["dQ/C", "<f4"],
|
|
96
|
+
"438": ["step time/s", "<f8"],
|
|
97
|
+
"441": ["<Ecv>/V", "<f4"],
|
|
98
|
+
"462": ["Temperature/°C", "<f4"],
|
|
99
|
+
"467": ["Q charge/discharge/mA.h", "<f8"],
|
|
100
|
+
"468": ["half cycle", "<u4"],
|
|
101
|
+
"469": ["z cycle", "<u4"],
|
|
102
|
+
"471": ["<Ece>/V", "<f4"],
|
|
103
|
+
"473": ["THD Ewe/%", "<f4"],
|
|
104
|
+
"474": ["THD I/%", "<f4"],
|
|
105
|
+
"476": ["NSD Ewe/%", "<f4"],
|
|
106
|
+
"477": ["NSD I/%", "<f4"],
|
|
107
|
+
"479": ["NSR Ewe/%", "<f4"],
|
|
108
|
+
"480": ["NSR I/%", "<f4"],
|
|
109
|
+
"486": ["|Ewe h2|/V", "<f4"],
|
|
110
|
+
"487": ["|Ewe h3|/V", "<f4"],
|
|
111
|
+
"488": ["|Ewe h4|/V", "<f4"],
|
|
112
|
+
"489": ["|Ewe h5|/V", "<f4"],
|
|
113
|
+
"490": ["|Ewe h6|/V", "<f4"],
|
|
114
|
+
"491": ["|Ewe h7|/V", "<f4"],
|
|
115
|
+
"492": ["|I h2|/A", "<f4"],
|
|
116
|
+
"493": ["|I h3|/A", "<f4"],
|
|
117
|
+
"494": ["|I h4|/A", "<f4"],
|
|
118
|
+
"495": ["|I h5|/A", "<f4"],
|
|
119
|
+
"496": ["|I h6|/A", "<f4"],
|
|
120
|
+
"497": ["|I h7|/A", "<f4"],
|
|
121
|
+
"498": ["Q charge/mA.h", "<f8"],
|
|
122
|
+
"499": ["Q discharge/mA.h", "<f8"],
|
|
123
|
+
"500": ["step time/s", "<f8"],
|
|
124
|
+
"501": ["Efficiency/%", "<f8"],
|
|
125
|
+
"502": ["Capacity/mA.h", "<f8"],
|
|
126
|
+
"505": ["Rdc/Ohm", "<f4"],
|
|
127
|
+
"509": ["Acir/Dcir Control", "<u1"]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""galvani 0.5.0's column map, frozen in this repository.
|
|
2
|
+
|
|
3
|
+
The benchmarks and the namer's expected storage types use this copy, so their
|
|
4
|
+
answers do not depend on which galvani (if any) is installed. namer/galvani_ref.py
|
|
5
|
+
uses the installed galvani for "known to galvani" instead.
|
|
6
|
+
|
|
7
|
+
python -m spike2.galvani_snapshot --write # regenerate from an installed galvani 0.5.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
SNAPSHOT = Path(__file__).resolve().parent / "galvani_0.5.0_colmap.json"
|
|
18
|
+
SOURCE = "galvani 0.5.0 (GPL-3.0), galvani/BioLogic.py, VMPdata_colID_dtype_map: column ID -> [name/unit, numpy dtype]"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=1)
|
|
22
|
+
def snapshot_map() -> dict[int, tuple[str, str]]:
|
|
23
|
+
d = json.loads(SNAPSHOT.read_text(encoding="utf-8"))
|
|
24
|
+
return {int(k): (v[0], v[1]) for k, v in d["columns"].items()}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write() -> int:
|
|
28
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
ok = version("galvani") == "0.5.0"
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
ok = False
|
|
34
|
+
if not ok:
|
|
35
|
+
print("needs galvani 0.5.0 installed (pip install galvani==0.5.0)", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
from galvani import BioLogic
|
|
38
|
+
|
|
39
|
+
rows = [f" {json.dumps(str(k))}: {json.dumps([v[0], v[1]], ensure_ascii=False)}"
|
|
40
|
+
for k, v in sorted(BioLogic.VMPdata_colID_dtype_map.items())]
|
|
41
|
+
text = '{\n"source": ' + json.dumps(SOURCE) + ',\n"columns": {\n' + ",\n".join(rows) + "\n}\n}\n"
|
|
42
|
+
SNAPSHOT.write_text(text, encoding="utf-8", newline="\n")
|
|
43
|
+
print(f"wrote {SNAPSHOT} ({len(rows)} columns)")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
raise SystemExit(write() if "--write" in sys.argv else 0)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Fast tests for the command-line tool (the full regression check is `python -m namer.golden check`).
|
|
2
|
+
|
|
3
|
+
Needs the public test files: python scripts/fetch_testdata.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from namer import cli, galvani_ref, mprfile
|
|
15
|
+
from spike2 import corpus
|
|
16
|
+
from spike2.galvani_snapshot import snapshot_map
|
|
17
|
+
|
|
18
|
+
EXT = corpus.EXT
|
|
19
|
+
pytestmark = pytest.mark.skipif(not (EXT / "gcpl.mpr").exists(), reason="run scripts/fetch_testdata.py first")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def recode(src: Path, dst: Path, old_id: int, new_id: int) -> None:
|
|
23
|
+
"""Simulate a vendor format change: give one column a column ID no parser knows."""
|
|
24
|
+
raw = bytearray(src.read_bytes())
|
|
25
|
+
i = raw.index(b"VMP data")
|
|
26
|
+
m = raw.index(b"MODULE", i - 10)
|
|
27
|
+
data_start = None
|
|
28
|
+
for name, _ver, mdata in mprfile._modules(bytes(raw)):
|
|
29
|
+
if name == "VMP data":
|
|
30
|
+
data_start = bytes(raw).index(mdata)
|
|
31
|
+
assert data_start is not None and m >= 0
|
|
32
|
+
n_cols = raw[data_start + 4]
|
|
33
|
+
ids = np.frombuffer(bytes(raw[data_start + 5: data_start + 5 + 2 * n_cols]), dtype="<u2").tolist()
|
|
34
|
+
k = ids.index(old_id)
|
|
35
|
+
raw[data_start + 5 + 2 * k: data_start + 7 + 2 * k] = int(new_id).to_bytes(2, "little")
|
|
36
|
+
dst.write_bytes(bytes(raw))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_reader_matches_reference_decoder():
|
|
40
|
+
for name in ("gcpl.mpr", "bcd.issue_241.mpr", "peis.mpr", "cv.mpr"):
|
|
41
|
+
ref = corpus.decode(EXT / name)
|
|
42
|
+
got = mprfile.read(EXT / name, reference="yadg")
|
|
43
|
+
a = [(c.name, c.dtype, c.values.tobytes()) for c in got.columns]
|
|
44
|
+
b = [(c.name, c.dtype, np.asarray(c.values).tobytes()) for c in ref.columns]
|
|
45
|
+
assert a == b, name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _mpt(path: Path) -> dict[str, np.ndarray]:
|
|
49
|
+
lines = path.read_bytes().decode("latin-1").splitlines()
|
|
50
|
+
n_header = int(lines[1].split(":")[1])
|
|
51
|
+
head = lines[n_header - 1].rstrip("\t").split("\t")
|
|
52
|
+
rows = [line.rstrip("\t").split("\t") for line in lines[n_header:] if line.strip()]
|
|
53
|
+
return {h: np.array([float(r[i].replace(",", ".")) for r in rows]) for i, h in enumerate(head)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_version_0_files_match_the_vendor_export():
|
|
57
|
+
"""Data module version 0 (older EC-Lab): yadg cannot read it; the values must match EC-Lab's own .mpt."""
|
|
58
|
+
m = mprfile.read(EXT / "bio_logic1.mpr", reference="galvani")
|
|
59
|
+
mpt = _mpt(EXT / "bio_logic1.mpt")
|
|
60
|
+
cols = {c.name: c.values.astype("f8") for c in m.columns}
|
|
61
|
+
assert m.n_points == len(mpt["time/s"]) == 3119 and not m.unknown
|
|
62
|
+
for name, ref in (("time", "time/s"), ("Ewe", "Ewe/V"), ("dq", "dq/mA.h"), ("Pwe", "P/W")):
|
|
63
|
+
np.testing.assert_allclose(cols[name], mpt[ref], rtol=1e-6, atol=1e-9, err_msg=name)
|
|
64
|
+
assert any("yadg 7.0.1 cannot read" in n for n in mprfile.read(EXT / "bio_logic1.mpr", reference="yadg").notes)
|
|
65
|
+
assert not any("yadg" in n for n in m.notes)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_empty_file_is_reported_plainly():
|
|
69
|
+
with pytest.raises(ValueError, match="no data points"):
|
|
70
|
+
mprfile.read(EXT / "wait.mpr")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_power_magnitude_in_version_0_file_is_named(tmp_path):
|
|
74
|
+
"""Version-0 files store |Ewe*I| as the power; hidden, it must come back as Pwe (it was named Ece)."""
|
|
75
|
+
raw = bytearray((EXT / "bio_logic1.mpr").read_bytes())
|
|
76
|
+
mdata = next(md for name, _v, md in mprfile._modules(bytes(raw)) if name == "VMP data")
|
|
77
|
+
start = bytes(raw).index(mdata)
|
|
78
|
+
ids = list(raw[start + 5: start + 5 + raw[start + 4]])
|
|
79
|
+
raw[start + 5 + ids.index(70)] = 255 # one-byte column IDs in version 0
|
|
80
|
+
f = tmp_path / "old_drift.mpr"
|
|
81
|
+
f.write_bytes(bytes(raw))
|
|
82
|
+
m = mprfile.read(f, reference="galvani")
|
|
83
|
+
assert [c.raw_id for c in m.unknown] == [255]
|
|
84
|
+
res = cli.name_unknown(m)
|
|
85
|
+
assert res[0]["verdict"] == "NAME" and res[0]["name"] == "Pwe", res[0]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_real_drift_duplicates_are_reported():
|
|
89
|
+
m = mprfile.read(EXT / "gcpl.issue_230.mpr", reference="galvani")
|
|
90
|
+
assert [c.raw_id for c in m.unknown] == [883, 884]
|
|
91
|
+
res = cli.name_unknown(m)
|
|
92
|
+
assert [r["verdict"] for r in res] == ["DUPLICATE", "DUPLICATE"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_galvani_low_byte_misreads_are_reported():
|
|
96
|
+
"""galvani 0.5.0 stops on this file only because it reads 471 (<Ece>/V) as 215; nothing is unknown."""
|
|
97
|
+
m = mprfile.read(EXT / "peis.issue_225.with_ece.mpr", reference="galvani")
|
|
98
|
+
assert m.unknown == []
|
|
99
|
+
assert any("471 as 215" in n for n in m.notes), m.notes
|
|
100
|
+
assert not any("low byte" in n for n in mprfile.read(EXT / "gcpl.mpr", reference="galvani").notes)
|
|
101
|
+
assert not any("low byte" in n for n in mprfile.read(EXT / "peis.issue_225.with_ece.mpr", reference="yadg").notes)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _galvani_version() -> str | None:
|
|
105
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
return version("galvani")
|
|
109
|
+
except PackageNotFoundError:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_frozen_map_is_galvani_050():
|
|
114
|
+
if _galvani_version() != "0.5.0":
|
|
115
|
+
pytest.skip("needs galvani 0.5.0 (pip install '.[test]')")
|
|
116
|
+
from galvani import BioLogic
|
|
117
|
+
|
|
118
|
+
assert snapshot_map() == {int(k): (v[0], v[1]) for k, v in BioLogic.VMPdata_colID_dtype_map.items()}
|
|
119
|
+
ref = galvani_ref.installed()
|
|
120
|
+
assert ref.label == "galvani 0.5.0" and ref.low_byte is True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_full_id_galvani_gets_no_low_byte_note(monkeypatch):
|
|
124
|
+
"""A galvani that reads column IDs in full (datalab-org-galvani 0.6.0) misreads nothing."""
|
|
125
|
+
ref = galvani_ref.Reference(snapshot_map(), "datalab-org-galvani 0.6.0", low_byte=False, installed=True)
|
|
126
|
+
monkeypatch.setattr(galvani_ref, "reference", lambda: ref)
|
|
127
|
+
m = mprfile.read(EXT / "peis.issue_225.with_ece.mpr", reference="galvani")
|
|
128
|
+
assert m.unknown == [] and not any("low byte" in n for n in m.notes), m.notes
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_without_galvani_the_frozen_map_stands_in(monkeypatch):
|
|
132
|
+
monkeypatch.setattr(galvani_ref, "installed", lambda: None)
|
|
133
|
+
m = mprfile.read(EXT / "gcpl.issue_230.mpr", reference="galvani")
|
|
134
|
+
assert [c.raw_id for c in m.unknown] == [883, 884]
|
|
135
|
+
assert any("not installed" in n for n in m.notes), m.notes
|
|
136
|
+
assert any("galvani 0.5.0 reads only the low byte" in n and "438 as 182" in n for n in m.notes), m.notes
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize("old_id,expected", [(467, "Q charge or discharge"), (7, "dq"), (4, "time")])
|
|
140
|
+
def test_simulated_drift_is_named(tmp_path, old_id, expected):
|
|
141
|
+
f = tmp_path / "drift.mpr"
|
|
142
|
+
recode(EXT / "gcpl.mpr", f, old_id, 950)
|
|
143
|
+
m = mprfile.read(f, reference="galvani")
|
|
144
|
+
assert [c.raw_id for c in m.unknown] == [950]
|
|
145
|
+
res = cli.name_unknown(m)
|
|
146
|
+
assert res[0]["verdict"] == "NAME" and res[0]["name"] == expected, res[0]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_noise_column_is_not_named(tmp_path):
|
|
150
|
+
"""Replace a column's bytes with noise: the tool must abstain, not invent a name."""
|
|
151
|
+
f = tmp_path / "noise.mpr"
|
|
152
|
+
shutil.copy(EXT / "gcpl.mpr", f)
|
|
153
|
+
recode(EXT / "gcpl.mpr", f, 467, 950)
|
|
154
|
+
m = mprfile.read(f, reference="galvani")
|
|
155
|
+
c = m.unknown[0]
|
|
156
|
+
c.values = np.random.default_rng(0).normal(5.0, 2.0, c.values.size)
|
|
157
|
+
res = cli.name_unknown(m)
|
|
158
|
+
assert res[0]["verdict"] != "NAME", res[0]
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Fast tests for the command-line tool (the full regression check is `python -m namer.golden check`).
|
|
2
|
-
|
|
3
|
-
Needs the public test files: python scripts/fetch_testdata.py
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import shutil
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import numpy as np
|
|
12
|
-
import pytest
|
|
13
|
-
|
|
14
|
-
from namer import cli, mprfile
|
|
15
|
-
from spike2 import corpus
|
|
16
|
-
|
|
17
|
-
EXT = corpus.EXT
|
|
18
|
-
pytestmark = pytest.mark.skipif(not (EXT / "gcpl.mpr").exists(), reason="run scripts/fetch_testdata.py first")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def recode(src: Path, dst: Path, old_id: int, new_id: int) -> None:
|
|
22
|
-
"""Simulate a vendor format change: give one column a column ID no parser knows."""
|
|
23
|
-
raw = bytearray(src.read_bytes())
|
|
24
|
-
i = raw.index(b"VMP data")
|
|
25
|
-
m = raw.index(b"MODULE", i - 10)
|
|
26
|
-
data_start = None
|
|
27
|
-
for name, _ver, mdata in mprfile._modules(bytes(raw)):
|
|
28
|
-
if name == "VMP data":
|
|
29
|
-
data_start = bytes(raw).index(mdata)
|
|
30
|
-
assert data_start is not None and m >= 0
|
|
31
|
-
n_cols = raw[data_start + 4]
|
|
32
|
-
ids = np.frombuffer(bytes(raw[data_start + 5: data_start + 5 + 2 * n_cols]), dtype="<u2").tolist()
|
|
33
|
-
k = ids.index(old_id)
|
|
34
|
-
raw[data_start + 5 + 2 * k: data_start + 7 + 2 * k] = int(new_id).to_bytes(2, "little")
|
|
35
|
-
dst.write_bytes(bytes(raw))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_reader_matches_reference_decoder():
|
|
39
|
-
for name in ("gcpl.mpr", "bcd.issue_241.mpr", "peis.mpr", "cv.mpr"):
|
|
40
|
-
ref = corpus.decode(EXT / name)
|
|
41
|
-
got = mprfile.read(EXT / name, reference="yadg")
|
|
42
|
-
a = [(c.name, c.dtype, c.values.tobytes()) for c in got.columns]
|
|
43
|
-
b = [(c.name, c.dtype, np.asarray(c.values).tobytes()) for c in ref.columns]
|
|
44
|
-
assert a == b, name
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_real_drift_duplicates_are_reported():
|
|
48
|
-
m = mprfile.read(EXT / "gcpl.issue_230.mpr", reference="galvani")
|
|
49
|
-
assert [c.raw_id for c in m.unknown] == [883, 884]
|
|
50
|
-
res = cli.name_unknown(m)
|
|
51
|
-
assert [r["verdict"] for r in res] == ["DUPLICATE", "DUPLICATE"]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_galvani_low_byte_misreads_are_reported():
|
|
55
|
-
"""galvani 0.5.0 stops on this file only because it reads 471 (<Ece>/V) as 215; nothing is unknown."""
|
|
56
|
-
m = mprfile.read(EXT / "peis.issue_225.with_ece.mpr", reference="galvani")
|
|
57
|
-
assert m.unknown == []
|
|
58
|
-
assert any("471 as 215" in n for n in m.notes), m.notes
|
|
59
|
-
assert not any("low byte" in n for n in mprfile.read(EXT / "gcpl.mpr", reference="galvani").notes)
|
|
60
|
-
assert not any("low byte" in n for n in mprfile.read(EXT / "peis.issue_225.with_ece.mpr", reference="yadg").notes)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@pytest.mark.parametrize("old_id,expected", [(467, "Q charge or discharge"), (7, "dq"), (4, "time")])
|
|
64
|
-
def test_simulated_drift_is_named(tmp_path, old_id, expected):
|
|
65
|
-
f = tmp_path / "drift.mpr"
|
|
66
|
-
recode(EXT / "gcpl.mpr", f, old_id, 950)
|
|
67
|
-
m = mprfile.read(f, reference="galvani")
|
|
68
|
-
assert [c.raw_id for c in m.unknown] == [950]
|
|
69
|
-
res = cli.name_unknown(m)
|
|
70
|
-
assert res[0]["verdict"] == "NAME" and res[0]["name"] == expected, res[0]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_noise_column_is_not_named(tmp_path):
|
|
74
|
-
"""Replace a column's bytes with noise: the tool must abstain, not invent a name."""
|
|
75
|
-
f = tmp_path / "noise.mpr"
|
|
76
|
-
shutil.copy(EXT / "gcpl.mpr", f)
|
|
77
|
-
recode(EXT / "gcpl.mpr", f, 467, 950)
|
|
78
|
-
m = mprfile.read(f, reference="galvani")
|
|
79
|
-
c = m.unknown[0]
|
|
80
|
-
c.values = np.random.default_rng(0).normal(5.0, 2.0, c.values.size)
|
|
81
|
-
res = cli.name_unknown(m)
|
|
82
|
-
assert res[0]["verdict"] != "NAME", res[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|