"""E1 sensor characteristics: rise time, steady response, recovery time, repeatability (CV). python experiments/e1_sensor_characteristics.py data/demo_calibration Needs raw.csv and labels.csv (columns: sample_id, label, concentration; rh_level optional, only rh_level == "mid" is used so humidity does not mix in). Writes to /results/: e1_per_measurement.csv one row per measurement and channel e1_summary.csv mean, SD and CV per gas x concentration x channel e1_curves.png average response curve per gas """ from __future__ import annotations import argparse import sys from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from enose.common import output_dir, plt, save_figure # noqa: E402 from enose.features import _tail_mean, recovery_time, rise_time_90 # noqa: E402 # (name in the report, raw column, heater step or None) CHANNELS: tuple[tuple[str, str, int | None], ...] = ( ("BME688 280°C", "gas_ohm", 2), ("SGP30 乙醇", "ethanol_raw", None), ("SCD40 CO2", "co2_ppm", None), ) def channel_rows(raw: pd.DataFrame, column: str, step: int | None) -> pd.DataFrame: rows = raw if step is None else raw[raw["heater_step"] == step] return rows[rows[column] >= 0] def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("folder", type=Path) arguments = parser.parse_args() raw = pd.read_csv(arguments.folder / "raw.csv") labels = pd.read_csv(arguments.folder / "labels.csv") if "rh_level" in labels: labels = labels[labels["rh_level"] == "mid"] results = output_dir(arguments.folder / "results") records = [] for sample in labels.itertuples(): measurement = raw[raw["sample_id"] == sample.sample_id] for name, column, step in CHANNELS: rows = channel_rows(measurement, column, step) purge, during, recover = (rows[rows["phase"] == phase] for phase in ("purge", "sample", "recover")) baseline, steady = _tail_mean(purge, column), _tail_mean(during, column) records.append({ "sample_id": sample.sample_id, "gas": sample.label, "concentration": sample.concentration, "channel": name, "baseline": baseline, "steady": steady, "relative_change": (steady - baseline) / abs(baseline) if "CO2" not in name else steady - baseline, "rise_time_90_s": rise_time_90(during, column, baseline), "recovery_time_s": recovery_time(recover, column, baseline), }) table = pd.DataFrame(records) table.to_csv(results / "e1_per_measurement.csv", index=False) summary = table.groupby(["channel", "gas", "concentration"]).agg( n=("relative_change", "size"), mean_change=("relative_change", "mean"), sd_change=("relative_change", "std"), rise_time_90_s=("rise_time_90_s", "mean"), recovery_time_s=("recovery_time_s", "mean")).reset_index() summary["cv_pct"] = 100 * summary["sd_change"] / summary["mean_change"].abs() summary.to_csv(results / "e1_summary.csv", index=False) print(summary.round(3).to_string(index=False)) print(f"\nCV < 10% 的條件:{(summary['cv_pct'] < 10).sum()} / {len(summary)}") figure, axes = plt.subplots(1, len(CHANNELS), figsize=(5 * len(CHANNELS), 3.6)) middle = labels[labels["concentration"] == sorted(labels["concentration"].unique())[len(labels["concentration"].unique()) // 2]] for axis, (name, column, step) in zip(axes, CHANNELS): for gas, group in middle.groupby("label"): curves = [] for sample_id in group["sample_id"]: measurement = raw[raw["sample_id"] == sample_id] rows = channel_rows(measurement, column, step).copy() # 10-second bins from the start of the measurement, so curves of different samples line up rows["t"] = (rows["device_ms"] - measurement["device_ms"].min()) // 10000 * 10 curves.append(rows.groupby("t")[column].mean()) mean_curve = pd.concat(curves, axis=1).mean(axis=1) axis.plot(mean_curve.index, mean_curve.to_numpy(), label=gas) axis.set_title(name) axis.set_xlabel("時間(秒)") axes[0].legend() save_figure(figure, results / "e1_curves.png") if __name__ == "__main__": main()