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.
Files changed (42) hide show
  1. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/PKG-INFO +7 -5
  2. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/README.md +5 -3
  3. rosetta_eclab-0.1.4/namer/galvani_ref.py +67 -0
  4. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/library.py +7 -3
  5. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/mprfile.py +33 -14
  6. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/pyproject.toml +6 -3
  7. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/PKG-INFO +7 -5
  8. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/SOURCES.txt +3 -0
  9. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/corpus.py +2 -2
  10. rosetta_eclab-0.1.4/spike2/galvani_0.5.0_colmap.json +129 -0
  11. rosetta_eclab-0.1.4/spike2/galvani_snapshot.py +48 -0
  12. rosetta_eclab-0.1.4/tests/test_cli.py +158 -0
  13. rosetta_eclab-0.1.2/tests/test_cli.py +0 -82
  14. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/LICENSE +0 -0
  15. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/__init__.py +0 -0
  16. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/cli.py +0 -0
  17. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/data/stats.json +0 -0
  18. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/data/stats_features.npy +0 -0
  19. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/debug.py +0 -0
  20. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/engine.py +0 -0
  21. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/evaluate.py +0 -0
  22. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/fallback.py +0 -0
  23. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/golden.py +0 -0
  24. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/namer/misses.py +0 -0
  25. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/dependency_links.txt +0 -0
  26. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/entry_points.txt +0 -0
  27. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/requires.txt +1 -1
  28. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/rosetta_eclab.egg-info/top_level.txt +0 -0
  29. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/setup.cfg +0 -0
  30. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/__init__.py +0 -0
  31. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/agent.py +0 -0
  32. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/api_check.py +0 -0
  33. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/baselines.py +0 -0
  34. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/dsl.py +0 -0
  35. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/elim.py +0 -0
  36. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/features.py +0 -0
  37. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/lock.py +0 -0
  38. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/run.py +0 -0
  39. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/score.py +0 -0
  40. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/tasks.py +0 -0
  41. {rosetta_eclab-0.1.2 → rosetta_eclab-0.1.4}/spike2/vocab.py +0 -0
  42. {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.2
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. Very old files (version 0) are not supported yet.
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), both GPL. 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.
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. Very old files (version 0) are not supported yet.
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), both GPL. 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.
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 BioLogic.VMPdata_colID_dtype_map.values():
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=[Relation("Ewe*I", ("Ewe", CURRENT), lambda c: _e(c) * _i(c), [W_PER_V_MA])])
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 _galvani_low_byte_note(full_ids: list[int], gmap: dict) -> list[str]:
83
- """galvani 0.5.0 reads only the low byte of each column ID in the 11.50+ layout.
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
- misread = [x for x in full_ids if x > 255 and gmap.get(x & 0xFF) != gmap.get(x)]
90
- if not misread:
91
- return []
92
- from importlib.metadata import version
93
-
94
- pairs = ", ".join(f"{x} as {x & 0xFF}" for x in misread)
95
- return [f"galvani {version('galvani')} reads only the low byte of each column ID in this file layout, "
96
- f"so it misreads {pairs}"]
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 n_points == 0 or region % n_points:
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
- gmap = {int(k): v for k, v in BioLogic.VMPdata_colID_dtype_map.items()}
137
- notes = _galvani_low_byte_note(full, gmap) if version in (10, 11) and reference == "galvani" else []
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.2"
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.2
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. Very old files (version 0) are not supported yet.
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), both GPL. 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.
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 galvani import BioLogic
99
+ from .galvani_snapshot import snapshot_map
100
100
 
101
- km = {int(k): v for k, v in BioLogic.VMPdata_colID_dtype_map.items()} # galvani 0.5.0: full IDs it knows
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
@@ -1,6 +1,6 @@
1
1
  numpy>=2.0
2
2
  yadg==7.0.1
3
- galvani==0.5.0
4
3
 
5
4
  [test]
6
5
  pytest
6
+ galvani==0.5.0
File without changes