Source code for psc_sim.eis_csv

"""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)