gamry-dta 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: gamry-dta
3
+ Version: 0.1.0
4
+ Summary: Read Gamry .DTA files as polars dataframes. Also extract metadata as dictionaries.
5
+ Author: kristianfkt
6
+ Author-email: kristianfkt <kristianfkt@gmail.com>
7
+ Requires-Dist: pandas>=3.0.6
8
+ Requires-Dist: polars>=1.42.0
9
+ Requires-Dist: pyarrow>=25.0.1
10
+ Requires-Dist: matplotlib>=3.11.2 ; extra == 'plot'
11
+ Requires-Python: >=3.11
12
+ Provides-Extra: plot
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Gamry DTA
16
+ - Library for reading gamry DTA files
17
+
18
+ # Installing
19
+ `pip install gamrydta`
20
+
21
+ # Quick start
22
+ ```
23
+ import gamrydta
24
+
25
+ path = ".../some_file.dta"
26
+ dta = gamrydta.read()
27
+ ```
28
+
29
+ # Contributing
30
+ - Bug reports
31
+ - ...
32
+
33
+ # Future:
34
+ - Parse column name-units according to https://github.com/emmo-repo/domain-electrochemistry
35
+ - Parse header fields according to https://github.com/emmo-repo/domain-electrochemistry
36
+ - Infer header field types, text, float, etc
37
+ - Add retry-cases for encoding errors
38
+ - Set default 'decimal' to None. Infer if not provided.
39
+ - Add an explorative dashboardu
40
+
@@ -0,0 +1,19 @@
1
+ gamrydta/__init__.py,sha256=S87ikEWJDEDnRxuZeQm8kLQJQd8JeSCS_Jh_uRKY1KY,284
2
+ gamrydta/aliases.py,sha256=SJl0aj1tFnOo1r8BxIB_oc6C1q-5iIEYtAQYU_qo6Bo,87
3
+ gamrydta/common.py,sha256=sdmIaPMmyTL8Y6qAiPiP9tQq3L8hML7Go2PiTwVSN8Y,294
4
+ gamrydta/experiments/__init__.py,sha256=EcymHLuqs9ZFc9y02yQeIPUbL8ZlESlOPC9Y8C6zjrA,1725
5
+ gamrydta/experiments/chrono.py,sha256=NqGw-umdZ-Tqw81MLGmKVu643T0ejy3XGK2-KdPZKp0,428
6
+ gamrydta/experiments/experiment.py,sha256=28zgYEYSJQUrh0oNd6TlJsHc06n1ygyLsalV9yzrj5E,1848
7
+ gamrydta/experiments/impedance.py,sha256=d1Wl9wxxiRnKg76hU4-jwtnMcXdwzswfYhy8Nq-J5OU,150
8
+ gamrydta/experiments/opencircuit.py,sha256=IJHHFmfjAiplYp0r8UqILvYjBJcwA76ej2OgJEqb9Jk,155
9
+ gamrydta/experiments/voltammetry.py,sha256=8La-c-QSdXV95BJ-7_eQ9e1A7cJZbSaP9p_xnswLpso,316
10
+ gamrydta/parsing/__init__.py,sha256=L-i50VD0c3paB_LnSLuWrZVWxFyu_5XYMu5saGDh_40,116
11
+ gamrydta/parsing/curves.py,sha256=S7G9blZYmcmI2KNp4O8uk7yEp_77tDUIBP7j-HIaJZk,972
12
+ gamrydta/parsing/dta.py,sha256=E1m2EGoOIdPWjDaeOIqsBp57df84CM7Jy4YmLa9DyrA,684
13
+ gamrydta/parsing/header.py,sha256=lKM4WPqOwPZaDhqFA0Tyn_zFbHrK6NV_Pl1JFqJZ2CY,671
14
+ gamrydta/parsing/ocv.py,sha256=exb0wAqIghMzXblc9kC3N0WGI8qekPkki-iH3QebBBE,905
15
+ gamrydta/parsing/table.py,sha256=G83V6tlbVytwhDRFyCQ6EeJ7CElcbzmaARRO3Wl4mC4,741
16
+ gamrydta/variables.py,sha256=yGldx47K-hVPyEm4jh7AYk-Et94dKnvowH-ElFmAuC4,1159
17
+ gamry_dta-0.1.0.dist-info/WHEEL,sha256=9IUYslYuG_asT4WlWvfk_K76nveuLvSTKDoLeL0cV-Q,80
18
+ gamry_dta-0.1.0.dist-info/METADATA,sha256=CwbYg-SSJVgsr3Bv7NMoeQZ_dtlJ0UU_ZSL5BDfvYZY,1019
19
+ gamry_dta-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
gamrydta/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from pathlib import Path
2
+
3
+ from gamrydta.experiments import read
4
+
5
+
6
+ def find(
7
+ path: str | Path,
8
+ pattern: str = "*.DTA",
9
+ ) -> list[Path]:
10
+ if isinstance(path, str):
11
+ path = Path(path)
12
+
13
+ files = list(path.rglob(pattern))
14
+ return files
15
+
16
+
17
+ __all__ = ["find", "read"]
gamrydta/aliases.py ADDED
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+ Line = list[str]
4
+ Lines = list[Line]
5
+ DTA = str | Path | Lines
gamrydta/common.py ADDED
@@ -0,0 +1,26 @@
1
+ from typing import Literal
2
+
3
+ Decimal = Literal[".", ","]
4
+ DECIMAL = "."
5
+
6
+
7
+ class ParsingError(Exception):
8
+ """
9
+ Parsing somehow failed
10
+ """
11
+
12
+
13
+ def is_text(value: str):
14
+ pass
15
+
16
+
17
+ def is_float(value: str):
18
+ pass
19
+
20
+
21
+ def is_int(value: str):
22
+ pass
23
+
24
+
25
+ def is_datetime(value: str):
26
+ pass
@@ -0,0 +1,62 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from gamrydta import parsing
5
+ from gamrydta.common import DECIMAL, Decimal, ParsingError
6
+ from gamrydta.experiments.chrono import (
7
+ Chronoamperometry,
8
+ Chronocoulometry,
9
+ Chronopotentiometry,
10
+ )
11
+ from gamrydta.experiments.experiment import Experiment
12
+ from gamrydta.experiments.impedance import PotentiostaticEIS
13
+ from gamrydta.experiments.opencircuit import CorrosionPotential, OpenCircuitPotential
14
+ from gamrydta.experiments.voltammetry import CyclicVoltammetry, LinearSweepVoltammetry
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ EXPERIMENTS = {
19
+ "CHRONOA": Chronoamperometry,
20
+ "CHRONOP": Chronopotentiometry,
21
+ "CHRONOC": Chronocoulometry,
22
+ "CV": CyclicVoltammetry,
23
+ "LSV": LinearSweepVoltammetry,
24
+ "OCP": OpenCircuitPotential,
25
+ "CORPOT": CorrosionPotential,
26
+ "EISPOT": PotentiostaticEIS,
27
+ }
28
+
29
+
30
+ def read(
31
+ path: str | Path,
32
+ encoding: str | None = None,
33
+ decimal: Decimal = DECIMAL,
34
+ ):
35
+ for line in parsing.dta.scan(path, encoding=encoding):
36
+ if line[0] == "TAG":
37
+ tag = line[1]
38
+ break
39
+ else:
40
+ raise ParsingError("Failed to detect experiement TAG")
41
+
42
+ if tag in EXPERIMENTS:
43
+ experiment = EXPERIMENTS[tag]
44
+ logger.info(f"TAG {tag} known - returning {experiment}")
45
+ else:
46
+ experiment = Experiment
47
+ logger.info(f"TAG {tag} unknonw - returning {experiment}")
48
+ return experiment.read(path, encoding=encoding, decimal=decimal)
49
+
50
+
51
+ __all__ = [
52
+ "Chronoamperometry",
53
+ "Chronocoulometry",
54
+ "Chronopotentiometry",
55
+ "CorrosionPotential",
56
+ "CyclicVoltammetry",
57
+ "DTAData",
58
+ "LinearSweepVoltammetry",
59
+ "OpenCircuitPotential",
60
+ "PotentiostaticEIS",
61
+ "read",
62
+ ]
@@ -0,0 +1,19 @@
1
+ from gamrydta.experiments.experiment import Experiment
2
+
3
+
4
+ class Chronoamperometry(Experiment):
5
+ @property
6
+ def current(self) -> float:
7
+ return self._header["current"]
8
+
9
+
10
+ class Chronopotentiometry(Experiment):
11
+ @property
12
+ def current(self) -> float:
13
+ return self._header["current"]
14
+
15
+
16
+ class Chronocoulometry(Experiment):
17
+ @property
18
+ def current(self) -> float:
19
+ return self._header["current"]
@@ -0,0 +1,81 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+ from typing import Any, Self
4
+
5
+ import polars as pl
6
+
7
+ from gamrydta import parsing
8
+ from gamrydta.common import DECIMAL, Decimal
9
+
10
+
11
+ class Experiment:
12
+ def __init__(
13
+ self,
14
+ *,
15
+ path: str | Path,
16
+ curves: pl.DataFrame,
17
+ header: Mapping[str, Any],
18
+ ocv: pl.DataFrame | None,
19
+ ):
20
+ self._path = parsing.dta.resolve(path)
21
+ self._curves = curves
22
+ self._header = header
23
+ self._ocv = ocv
24
+
25
+ @classmethod
26
+ def read(
27
+ cls,
28
+ path: str | Path,
29
+ encoding: str | None = None,
30
+ decimal: Decimal = DECIMAL,
31
+ ) -> Self:
32
+
33
+ path = parsing.dta.resolve(path)
34
+ lines = parsing.dta.read(path, encoding=encoding)
35
+
36
+ return cls(
37
+ path=path,
38
+ curves=parsing.curves.parse(lines, decimal=decimal),
39
+ header=parsing.header.parse(lines, decimal=decimal),
40
+ ocv=parsing.ocv.parse(lines, decimal=decimal),
41
+ )
42
+
43
+ @property
44
+ def path(self) -> Path:
45
+ return self._path
46
+
47
+ @property
48
+ def curves(self) -> pl.DataFrame:
49
+ return self._curves
50
+
51
+ @property
52
+ def data(self) -> pl.DataFrame:
53
+ return self._curves
54
+
55
+ @property
56
+ def header(self) -> Mapping[str, Any]:
57
+ return self._header
58
+
59
+ @property
60
+ def metadata(self) -> Mapping[str, Any]:
61
+ return self._header
62
+
63
+ @property
64
+ def experiment(self) -> str:
65
+ return self._header["TITLE"]
66
+
67
+ @property
68
+ def tag(self) -> str:
69
+ return self._header["TAG"]
70
+
71
+ @property
72
+ def ocv(self) -> pl.DataFrame | None:
73
+ return self._ocv
74
+
75
+ def to_dict(self) -> dict:
76
+ return {
77
+ "path": self.path,
78
+ "header": self.header,
79
+ "curve": self.curves,
80
+ "ocv": self.ocv,
81
+ }
@@ -0,0 +1,9 @@
1
+ from gamrydta.experiments.experiment import Experiment
2
+
3
+
4
+ class PotentiostaticEIS(Experiment):
5
+ pass
6
+
7
+
8
+ class GalvanostaticEIS(Experiment):
9
+ pass
@@ -0,0 +1,9 @@
1
+ from gamrydta.experiments.experiment import Experiment
2
+
3
+
4
+ class OpenCircuitPotential(Experiment):
5
+ pass
6
+
7
+
8
+ class CorrosionPotential(Experiment):
9
+ pass
@@ -0,0 +1,13 @@
1
+ from gamrydta.experiments.experiment import Experiment
2
+
3
+
4
+ class CyclicVoltammetry(Experiment):
5
+ @property
6
+ def sweeprate(self) -> float:
7
+ return self._header["SWEEPRATE"]
8
+
9
+
10
+ class LinearSweepVoltammetry(Experiment):
11
+ @property
12
+ def sweeprate(self) -> float:
13
+ return self._header["SWEEPRATE"]
@@ -0,0 +1,3 @@
1
+ from gamrydta.parsing import curves, dta, header, ocv, table
2
+
3
+ __all__ = ["curves", "dta", "header", "ocv", "table"]
@@ -0,0 +1,39 @@
1
+ import re
2
+
3
+ import polars as pl
4
+
5
+ from gamrydta import parsing
6
+ from gamrydta.aliases import Lines
7
+ from gamrydta.common import DECIMAL, Decimal
8
+
9
+
10
+ def match(line: list[str]) -> bool:
11
+ return bool(re.fullmatch(r"(?:ZCURVE|CURVE(?:[1-9][0-9]*)?)", line[0]))
12
+
13
+
14
+ def parse(
15
+ lines: Lines,
16
+ decimal: Decimal = DECIMAL,
17
+ ) -> pl.DataFrame:
18
+ """
19
+ Extracts the curve data from a DTA file and returns it as a DataFrame.
20
+ """
21
+
22
+ indices_start = [idx + 1 for idx, line in enumerate(lines) if match(line)]
23
+ indices_end = [idx - 1 for idx in indices_start[1::]] + [len(lines)]
24
+ curves = []
25
+ for s, e in zip(indices_start, indices_end):
26
+ curve = parsing.table.parse(
27
+ lines[s:e],
28
+ decimal=decimal,
29
+ )
30
+
31
+ curves.append(curve)
32
+
33
+ if len(curves) > 1:
34
+ curves = [
35
+ curve.with_columns(pl.lit(i).alias("curve"))
36
+ for i, curve in enumerate(curves, start=1)
37
+ ]
38
+
39
+ return pl.concat(curves)
@@ -0,0 +1,26 @@
1
+ from collections.abc import Iterator
2
+ from pathlib import Path
3
+
4
+
5
+ def resolve(path: str | Path) -> Path:
6
+ """
7
+ Ensures that a path is Path.only.
8
+ """
9
+ return Path(path).resolve(strict=True)
10
+
11
+
12
+ def split(line: str) -> list[str]:
13
+ return line.strip().removesuffix("\n").split("\t")
14
+
15
+
16
+ def read(path: str | Path, encoding: str | None) -> list[list[str]]:
17
+ path = resolve(path)
18
+ with open(path, "r", encoding=encoding) as f:
19
+ return [split(line) for line in f]
20
+
21
+
22
+ def scan(path: str | Path, encoding: str | None) -> Iterator[list[str]]:
23
+ path = resolve(path)
24
+ with open(path, "r", encoding=encoding) as f:
25
+ for line in f:
26
+ yield split(line)
@@ -0,0 +1,26 @@
1
+ from gamrydta.aliases import Lines
2
+ from gamrydta.common import DECIMAL, Decimal
3
+ from gamrydta.parsing import curves, ocv
4
+
5
+
6
+ def parse(
7
+ lines: Lines,
8
+ decimal: Decimal = DECIMAL,
9
+ ) -> dict:
10
+ """
11
+ Extracts the header fields from a DTA file and returns them as a dictionary.
12
+ """
13
+
14
+ header = {}
15
+ for line in lines:
16
+ if curves.match(line):
17
+ return header
18
+ if not line[0].strip() or ocv.match(line):
19
+ continue
20
+ if len(line) == 2:
21
+ key, value = line
22
+ header[key] = value
23
+ if len(line) == 4:
24
+ code, kind, value, name = line
25
+ header[name] = value
26
+ return header
@@ -0,0 +1,36 @@
1
+ import re
2
+
3
+ import polars as pl
4
+
5
+ from gamrydta import parsing
6
+ from gamrydta.aliases import Lines
7
+ from gamrydta.common import DECIMAL, Decimal, ParsingError
8
+
9
+
10
+ def match(line: list[str]) -> bool:
11
+ return line[0] == "OCVCURVE"
12
+
13
+
14
+ def parse(
15
+ lines: Lines,
16
+ decimal: Decimal = DECIMAL,
17
+ ) -> pl.DataFrame | None:
18
+ """
19
+ Extracts the OCV data from a DTA file and returns it as a DataFrame.
20
+ """
21
+
22
+ for idx, line in enumerate(lines):
23
+ if match(line):
24
+ index_start = idx + 1
25
+ break
26
+ else:
27
+ return None
28
+
29
+ for idx, line in enumerate(lines[index_start + 2 : :], start=index_start + 2):
30
+ if not bool(re.fullmatch(r"[0-9]+", line[0])):
31
+ index_end = idx
32
+ break
33
+ else:
34
+ raise ParsingError("Reached en of lines without findingn OCV curve end")
35
+
36
+ return parsing.table.parse(lines[index_start:index_end], decimal=decimal)
@@ -0,0 +1,35 @@
1
+ import csv
2
+ from io import StringIO
3
+
4
+ import polars as pl
5
+
6
+ from gamrydta.aliases import Lines
7
+ from gamrydta.common import DECIMAL, Decimal
8
+
9
+
10
+ def parse(
11
+ table: Lines,
12
+ decimal: Decimal = DECIMAL,
13
+ ) -> pl.DataFrame:
14
+ """
15
+ Parses lines from
16
+ CURVE TABLE
17
+ ..units
18
+ ...vvalues
19
+ """
20
+ names = table[0]
21
+ units = table[1]
22
+
23
+ buffer = StringIO()
24
+ writer = csv.writer(buffer, delimiter="\t")
25
+ writer.writerow([f"{name} / {unit}" for name, unit in zip(names, units)])
26
+ writer.writerows(table[2::])
27
+ buffer.seek(0)
28
+ return pl.read_csv(
29
+ buffer,
30
+ has_header=True,
31
+ infer_schema=True,
32
+ infer_schema_length=None,
33
+ separator="\t",
34
+ decimal_comma=(decimal == ","),
35
+ )
gamrydta/variables.py ADDED
@@ -0,0 +1,56 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import TypedDict
3
+
4
+ import polars as pl
5
+
6
+
7
+ class Name(TypedDict):
8
+ code: str
9
+ label: str
10
+
11
+
12
+ class Unit(TypedDict):
13
+ code: str
14
+ label: str
15
+
16
+
17
+ @dataclass
18
+ class Variable:
19
+ name: Name
20
+ unit: Unit
21
+ code: str = field(init=False)
22
+ label: str = field(init=False)
23
+
24
+ def __post_init__(self):
25
+ self.code = f"{self.name['code']}_{self.unit['code']}"
26
+
27
+ self.label = f"{self.name['label']} / {self.unit['label']}"
28
+
29
+
30
+ UNITS = {
31
+ "#": Unit(code="1", label="1"),
32
+ "s": Unit(code="second", label="s"),
33
+ "V vs. Ref.": Unit(code="voltage_versus_reference", label="V vs Ref"),
34
+ }
35
+
36
+
37
+ VARIABLES: dict[str, str] = {
38
+ "Pt": "Record count / 1",
39
+ "T": "Test time / s",
40
+ "curve": "Cycle count / 1",
41
+ "Vf": "Voltage / V",
42
+ "Im": "Current / A",
43
+ "Q": "Charge / C",
44
+ "Freq": "Frequency / Hz",
45
+ "Zreal": "Zr / Ohm",
46
+ "Zimag": "Zi / Ohm",
47
+ "Zmod": "Zmod / Ohm",
48
+ "Zphz": "Zphz / degrees",
49
+ }
50
+
51
+
52
+ def rename(data: pl.DataFrame) -> pl.DataFrame:
53
+ """
54
+ Rename some mapped variables to better described names
55
+ """
56
+ return data.rename(VARIABLES, strict=False)