STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
validate_export.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Summarize an Elodin `elodin-db export --format csv` directory for DB validation.
3
4Checks that expected entity families exist (PT/TC/LC calibrated, actuators, controller, board HB).
5Use after: FORMAT=csv ./scripts/postprocessing/export_elodin_db.sh <DB_PATH> ./export_csv
6
7Exit codes:
8 0 — export looks usable (and passes --strict checks if set)
9 1 — missing export dir, no CSVs, or --strict failure
10"""
11
12from __future__ import annotations
13
14import argparse
15import sys
16from collections import defaultdict
17from pathlib import Path
18
19
20def _count_lines(p: Path) -> int:
21 try:
22 with open(p, "rb") as f:
23 return sum(1 for _ in f)
24 except OSError:
25 return -1
26
27
28def main() -> int:
29 ap = argparse.ArgumentParser(description="Validate Elodin CSV export directory")
30 ap.add_argument(
31 "export_dir",
32 type=Path,
33 nargs="?",
34 default=Path("./export_csv"),
35 help="Directory containing flattened CSVs from elodin-db export",
36 )
37 ap.add_argument(
38 "--strict",
39 action="store_true",
40 help="Fail if no calibrated PT pressure series found (typical hotfire/GSE run)",
41 )
42 args = ap.parse_args()
43 d: Path = args.export_dir
44 if not d.is_dir():
45 print(f"❌ Not a directory: {d}", file=sys.stderr)
46 return 1
47 csvs = sorted(d.glob("*.csv"))
48 if not csvs:
49 print(f"❌ No *.csv in {d.resolve()}", file=sys.stderr)
50 return 1
51
52 by_prefix: dict[str, list[Path]] = defaultdict(list)
53 for f in csvs:
54 stem = f.stem
55 prefix = stem.split(".", 1)[0] if "." in stem else stem
56 by_prefix[prefix].append(f)
57
58 print(f"📂 Export: {d.resolve()}")
59 print(f" Total CSV files: {len(csvs)}")
60 print(" By entity prefix:")
61 for pref in sorted(by_prefix.keys()):
62 print(f" {pref}: {len(by_prefix[pref])} file(s)")
63
64 # Heuristic buckets (names match elodin-db flattened stems)
65 pt_cal = [
66 f
67 for f in csvs
68 if "PT" in f.name and "Cal" in f.name and "pressure_psi" in f.name
69 ]
70 pt_raw = [
71 f
72 for f in csvs
73 if f.name.startswith("PT") and "Cal" not in f.name and "pressure" in f.name
74 ]
75 tc_cal = [f for f in csvs if "TC" in f.name and "Cal" in f.name]
76 rtd_cal = [f for f in csvs if "RTD" in f.name and "Cal" in f.name]
77 lc_cal = [f for f in csvs if "LC" in f.name and "Cal" in f.name]
78 act_cmd = [
79 f for f in csvs if "ACT_CMD" in f.name or "actuator_state_commanded" in f.name
80 ]
81 act_hw = [
82 f
83 for f in csvs
84 if f.name.startswith("ACT") and "CH" in f.name and "actuator_state" in f.name
85 ]
86 ctrl = [f for f in csvs if f.name.startswith("CONTROLLER")]
87 brd = [f for f in csvs if f.name.startswith("BOARD")]
88
89 def rows_sample(paths: list[Path]) -> int:
90 n = 0
91 for p in paths[:20]:
92 c = _count_lines(p)
93 if c > 0:
94 n += max(0, c - 1)
95 return n
96
97 print("\n Content hints (file counts / ~data rows in first 20 files each):")
98 print(
99 f" PT calibrated (PSI): {len(pt_cal)} files, ~{rows_sample(pt_cal)} rows"
100 )
101 print(
102 f" PT raw: {len(pt_raw)} files, ~{rows_sample(pt_raw)} rows"
103 )
104 print(f" TC/RTD cal: {len(tc_cal) + len(rtd_cal)} files")
105 print(f" LC cal: {len(lc_cal)} files")
106 print(f" Actuators (CMD): {len(act_cmd)} files")
107 print(f" Actuators (HW sense): {len(act_hw)} files")
108 print(f" CONTROLLER: {len(ctrl)} files")
109 print(f" BOARD (HB etc.): {len(brd)} files")
110
111 warnings: list[str] = []
112 if not pt_cal:
113 warnings.append(
114 "No PT*_Cal.*pressure_psi.csv — calibrated PT may be missing or export pattern changed."
115 )
116 if not pt_raw and not pt_cal:
117 warnings.append(
118 "No PT raw/cal files — check daq_bridge publish + calibration_service."
119 )
120 if not ctrl:
121 warnings.append(
122 "No CONTROLLER*.csv — controller may be off or not writing to DB."
123 )
124 if not act_cmd and not act_hw:
125 warnings.append("No ACT* actuator_state*.csv — actuator tables may be missing.")
126
127 if warnings:
128 print("\n ⚠️ Warnings:")
129 for w in warnings:
130 print(f" - {w}")
131 else:
132 print("\n ✅ Core sensor/actuator/controller families present.")
133
134 def total_data_rows(paths: list[Path]) -> int:
135 n = 0
136 for p in paths:
137 c = _count_lines(p)
138 if c > 1:
139 n += c - 1
140 return n
141
142 if args.strict:
143 if not pt_cal:
144 print(
145 "\n❌ --strict: require at least one PT calibrated pressure CSV.",
146 file=sys.stderr,
147 )
148 return 1
149 total_pt_rows = total_data_rows(pt_cal)
150 if total_pt_rows < 2:
151 print(
152 "\n❌ --strict: PT cal series have insufficient rows.",
153 file=sys.stderr,
154 )
155 return 1
156
157 return 0
158
159
160if __name__ == "__main__":
161 sys.exit(main())
#define max(a, b)
Definition Arduino.h:43
int _count_lines(Path p)