diff --git a/src/pygui/backend/cluster_average.py b/src/pygui/backend/cluster_average.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pygui/backend/csv_recorder.py b/src/pygui/backend/csv_recorder.py new file mode 100644 index 0000000..8e123ed --- /dev/null +++ b/src/pygui/backend/csv_recorder.py @@ -0,0 +1,46 @@ +"""Append live frames""" +from __future__ import annotations +from datetime import datetime +from pathlib import Path +from typing import Optional, TextIO + + +from pygui.backend.frame import Frame +from pygui.backend.zwafer_models import Sensor + +class CsvRecorder: + def __init__(self) -> None: + self._file_handle: Optional[TextIO] = None + self._oath: Optional[str] = None + + + @property + def is_recording(self) -> bool: + return self._file_handle is not None + + + def start(self, path: str, sensors:list[Sensor], serial: str = "") -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + file_handle = open(path, "w", encoding="utf-8", newline ="") + file_handle.write(f"Wafer ID={serial}\n") + file_handle.write(f"Acquisition Date={datetime.now():%m/%d/%Y}\n") + file_handle.write("Label," + ",".join(s.label for s in sensors) + "\n") + file_handle.write("X (mm)," + ",".join(str(s.x) for s in sensors) + "\n") + file_handle.write("Y (mm)," + ",".join(str(s.y) for s in sensors) + "\n") + file_handle.write("data\n") + file_handle.flush() + self._file_handle, self._path = file_handle, path + + def write(self, frame: Frame) -> None: + if self._file_handle is None: + return + row = [f"{frame.time:.3f}"] + [f"{v:.3f}" for v in frame.values] + self._file_handle.write(",".join(row) + "\n") + self._file_handle.flush() + + def stop(self) -> Optional[str]: + if self._file_handle is None: + return None + self._file_handle.close() + path, self._file_handle, self._path = self._path, None, None + return path diff --git a/src/pygui/backend/data_records.py b/src/pygui/backend/data_records.py new file mode 100644 index 0000000..7b5ffef --- /dev/null +++ b/src/pygui/backend/data_records.py @@ -0,0 +1,66 @@ +"""Read the data section of a Z-wafer CSV into DataRecords, or the official headerless CSV.""" +from __future__ import annotations +from pathlib import Path + +from pygui.backend.zwafer_models import DataRecord, Sensor + +def read_data_records(file_path: str) -> list[DataRecord]: + records: list[DataRecord] = [] + in_data = False + with Path(file_path).open("r", encoding="utf-8") as file_handle: + for line in file_handle: + line = line.rstrip().rstrip(",").strip() + if not line: + continue + if not in_data: + if line.split(",")[0].lower() == "data": + in_data = True + continue + cols = [c.strip() for c in line.split(",") if c.strip() != ""] + try: + nums = [float(c) for c in cols] + except ValueError: + continue + if not nums: + continue + records.append(DataRecord(time=nums[0], values=nums[1:])) + return records + + +def is_official_csv(file_path: str) -> bool: + """Return True if the file uses the headerless official format (sensor-name header row, no 'data' sentinel).""" + with Path(file_path).open("r", encoding="utf-8") as f: + first = f.readline().rstrip().rstrip(",") + if not first: + return False + parts = [p.strip() for p in first.split(",") if p.strip()] + # Official format: first cell looks like "SensorN" (starts with letter, not key=value) + return bool(parts) and parts[0].lower().startswith("sensor") and "=" not in parts[0] + + +def read_official_csv(file_path: str) -> tuple[list[Sensor], list[DataRecord]]: + """Parse the headerless official CSV: row 0 = sensor names, rows 1+ = values (no timestamp).""" + from pygui.backend.wafer_layouts import load_layout_for_wafer_id + + stem = Path(file_path).stem + wafer_id = stem.split("-")[0] if "-" in stem else stem + try: + sensors = load_layout_for_wafer_id(wafer_id) + except KeyError: + return [], [] + + records: list[DataRecord] = [] + with Path(file_path).open("r", encoding="utf-8") as f: + f.readline() # skip sensor-name header + for i, line in enumerate(f): + line = line.rstrip().rstrip(",") + if not line: + continue + cols = [c.strip() for c in line.split(",") if c.strip()] + try: + values = [float(c) for c in cols] + except ValueError: + continue + if values: + records.append(DataRecord(time=float(i) * 0.5, values=values)) + return sensors, records diff --git a/src/pygui/backend/file_browser.py b/src/pygui/backend/file_browser.py index 748c0be..e65537a 100644 --- a/src/pygui/backend/file_browser.py +++ b/src/pygui/backend/file_browser.py @@ -120,24 +120,37 @@ class FileBrowser(QObject): for csv_path in sorted(self._current_directory.glob("*.csv")): try: metadata = self._load_metadata(csv_path) + wafer_type = metadata.get_wafer_type().upper() or (csv_path.stem[0].upper() if csv_path.stem else "") + date_str = metadata.string_date_format() + serial = metadata.wafer[1:] if len(metadata.wafer) > 1 else "" + time_str = date_str[11:] if len(date_str) > 10 else "" self._files.append( { "selected": selected_state.get(str(csv_path), False), + "baseName": csv_path.stem, "wafer": metadata.wafer, - "date": metadata.string_date_format(), + "waferType": wafer_type, + "serialNumber": serial, + "date": date_str, + "timeStr": time_str, "chamber": metadata.chamber, "notes": metadata.notes, "masterType": master_state.get(str(csv_path), "") or metadata.master_type, "fileName": str(csv_path), - "highlight": metadata.get_wafer_type() in {"A", "B", "C"}, + "highlight": wafer_type in {"A", "B", "C"}, } ) except Exception: + stem = csv_path.stem self._files.append( { "selected": selected_state.get(str(csv_path), False), - "wafer": csv_path.stem, + "baseName": stem, + "wafer": stem, + "waferType": stem[0].upper() if stem else "", + "serialNumber": stem[1:] if len(stem) > 1 else "", "date": "", + "timeStr": "", "chamber": "", "notes": "Unable to parse metadata", "masterType": master_state.get(str(csv_path), ""), @@ -186,7 +199,7 @@ class FileBrowser(QObject): def _metadata_from_filename(self, csv_path: Path) -> CSVFileMetadata: match = re.match( - r"^(?P[^-]+)-(?P\d{8})(?:_(?P\d+))?$", + r"^(?P[^-]+)-(?P\d{8})(?:_(?P