"""Load measured EIS CSV (see docs/conventions.md)."""
from __future__ import annotations
import csv
from pathlib import Path
import numpy as np
[docs]
def load_eis_csv(path: str | Path) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Return (freq_hz, zreal_ohm, zimag_ohm). Accepts header aliases."""
path = Path(path)
with path.open(newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if reader.fieldnames is None:
raise ValueError("CSV has no header row")
fields = {h.strip().lower(): h for h in reader.fieldnames if h}
def pick(*names: str) -> str:
for n in names:
key = n.lower()
if key in fields:
return fields[key]
raise KeyError(f"None of columns {names} found in {reader.fieldnames!r}")
try:
c_f = pick("freq_hz", "f", "freq", "frequency_hz", "frequency")
c_re = pick("zreal_ohm", "zre", "zreal", "re", "z'")
c_im = pick("zimag_ohm", "zim", "zimag", "im", "z''", "z_imag")
except KeyError as e:
raise ValueError(
"Expected columns like freq_hz,f,zreal_ohm,zre,zimag_ohm,zim — see docs/conventions.md"
) from e
freqs, zre, zim = [], [], []
for row in reader:
try:
freqs.append(float(row[c_f]))
zre.append(float(row[c_re]))
zim.append(float(row[c_im]))
except (TypeError, ValueError):
continue
return (
np.asarray(freqs, dtype=float),
np.asarray(zre, dtype=float),
np.asarray(zim, dtype=float),
)
CANONICAL_EIS_HEADER = ("freq_hz", "zreal_ohm", "zimag_ohm")
[docs]
def write_eis_csv(
path: str | Path,
freqs_hz: np.ndarray,
zre: np.ndarray,
zim: np.ndarray,
*,
zre_lt: np.ndarray | None = None,
zim_lt: np.ndarray | None = None,
) -> None:
"""Write EIS CSV with canonical headers (see docs/conventions.md)."""
path = Path(path)
freqs = np.asarray(freqs_hz, dtype=float)
zre_a = np.asarray(zre, dtype=float)
zim_a = np.asarray(zim, dtype=float)
include_lt = (
zre_lt is not None
and zim_lt is not None
and len(zre_lt) == len(freqs)
and len(zim_lt) == len(freqs)
)
with path.open("w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
header = list(CANONICAL_EIS_HEADER)
if include_lt:
header.extend(["zre_lt", "zim_lt"])
w.writerow(header)
for i, (f_hz, re, im) in enumerate(zip(freqs, zre_a, zim_a, strict=True)):
row = [f"{f_hz:.6g}", f"{re:.12g}", f"{im:.12g}"]
if include_lt:
assert zre_lt is not None and zim_lt is not None
row.extend([f"{zre_lt[i]:.12g}", f"{zim_lt[i]:.12g}"])
w.writerow(row)
[docs]
def rmse_z(
f_meas: np.ndarray,
zre_meas: np.ndarray,
zim_meas: np.ndarray,
zre_model: np.ndarray,
zim_model: np.ndarray,
) -> float:
"""RMSE on real and imag (same length arrays). `f_meas` is accepted for API stability."""
del f_meas
from psc_sim.compare_metrics import eis_z_rmse
return eis_z_rmse(zre_meas, zim_meas, zre_model, zim_model)