Skip to content

Commit 1a69be8

Browse files
added plotgraph tool
1 parent 51f2c9e commit 1a69be8

File tree

1 file changed

+275
-0
lines changed

1 file changed

+275
-0
lines changed

utils/plotgraph.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#!/usr/bin/env python3
2+
"""
3+
plot_lamp_filepicker.py
4+
5+
Opens a file-picker dialog so you can choose a .dat (or other) file.
6+
Reads the selected TCD1304-style two-column file (pixel, ADC), estimates
7+
ADC_dark from dummy pixels, computes Ipixel = ADC_dark - ADC, creates an
8+
interpolated curve (uses scipy if available, otherwise numpy.interp fallback),
9+
and writes all outputs into a new folder next to the selected file.
10+
11+
Output folder name (created next to the .dat file):
12+
<original_filename_stem>_<creation-timestamp>
13+
14+
The timestamp is the file system creation time of the selected file, formatted
15+
as YYYYMMDD_HHMMSS so it is safe in filenames.
16+
17+
Files created inside that folder:
18+
- <prefix>_Ipixel.csv (pixel, ADC, Ipixel)
19+
- <prefix>_interpolated_sample.csv (high-res sampled interpolation)
20+
- <prefix>_plot.png (ADC + Ipixel plot with interpolation overlay)
21+
- <prefix>_interpolator.pkl (pickled interpolator object, optional)
22+
23+
Usage:
24+
- Double-click or run from CMD:
25+
python plot_lamp_filepicker.py
26+
A file dialog will open; pick your lamp.dat (or any .dat file).
27+
28+
- Or run and pass a path to skip the dialog:
29+
python plot_lamp_filepicker.py "C:\path\to\lamp.dat"
30+
31+
Notes:
32+
- If you prefer not to have a GUI, pass the file path as the first argument.
33+
- Be careful when unpickling .pkl files from untrusted sources.
34+
"""
35+
from __future__ import annotations
36+
import argparse
37+
from pathlib import Path
38+
import sys
39+
import os
40+
import csv
41+
import pickle
42+
import numpy as np
43+
import matplotlib
44+
matplotlib.use("Agg") # safe backend for non-interactive use
45+
import matplotlib.pyplot as plt
46+
from datetime import datetime
47+
48+
def choose_file_with_dialog(initialdir: str | None = None) -> Path | None:
49+
try:
50+
# Use tkinter filedialog to open a native file selection dialog
51+
import tkinter as tk
52+
from tkinter import filedialog
53+
except Exception:
54+
return None
55+
root = tk.Tk()
56+
root.withdraw()
57+
# On Windows the dialog will be a native explorer-file dialog
58+
filetypes = [("DAT files", "*.dat"), ("All files", "*.*")]
59+
filename = filedialog.askopenfilename(title="Select .dat file", initialdir=initialdir or ".", filetypes=filetypes)
60+
root.update()
61+
root.destroy()
62+
if not filename:
63+
return None
64+
return Path(filename).expanduser().resolve()
65+
66+
def read_file_lines(filename: str):
67+
encodings = ["utf-8", "cp1252", "latin-1"]
68+
for enc in encodings:
69+
try:
70+
with open(filename, "r", encoding=enc) as f:
71+
lines = f.readlines()
72+
print(f"Opened file using encoding: {enc}")
73+
return lines
74+
except UnicodeDecodeError:
75+
continue
76+
except Exception as e:
77+
# other I/O error -> raise
78+
raise
79+
# fallback: binary decode with replacement
80+
with open(filename, "rb") as f:
81+
raw = f.read()
82+
lines = raw.decode("utf-8", errors="replace").splitlines()
83+
print("Opened file via binary fallback, decoded with errors='replace'.")
84+
return lines
85+
86+
def parse_lines_to_arrays(lines):
87+
pixels = []
88+
adcs = []
89+
for line in lines:
90+
s = line.strip()
91+
if not s or s.startswith("#"):
92+
continue
93+
parts = s.split()
94+
if len(parts) < 2:
95+
continue
96+
try:
97+
p = int(parts[0])
98+
a = float(parts[1])
99+
except Exception:
100+
try:
101+
p = int(''.join(ch for ch in parts[0] if (ch.isdigit() or ch == '-')))
102+
a = float(''.join(ch for ch in parts[1] if (ch.isdigit() or ch in ".-eE")))
103+
except Exception:
104+
continue
105+
pixels.append(p)
106+
adcs.append(a)
107+
if not pixels:
108+
raise ValueError("No valid pixel/ADC pairs found in file.")
109+
return np.array(pixels, dtype=int), np.array(adcs, dtype=float)
110+
111+
def estimate_dark(pixels: np.ndarray, adcs: np.ndarray, method: str = "median"):
112+
mask = ((pixels >= 1) & (pixels <= 32)) | ((pixels >= 3679) & (pixels <= 3694))
113+
dark_vals = adcs[mask]
114+
if dark_vals.size == 0:
115+
n = pixels.size
116+
if n >= 64:
117+
dark_vals = np.concatenate((adcs[:32], adcs[-32:]))
118+
else:
119+
dark_vals = adcs[:max(1, n//10)]
120+
if method == "median":
121+
return float(np.median(dark_vals)), dark_vals
122+
else:
123+
return float(np.mean(dark_vals)), dark_vals
124+
125+
def make_interpolator(pixels: np.ndarray, intensities: np.ndarray, method: str = "spline"):
126+
try:
127+
if method == "spline":
128+
from scipy.interpolate import UnivariateSpline
129+
s = max(0.0, len(pixels) * np.var(intensities) * 0.02)
130+
spline = UnivariateSpline(pixels, intensities, s=s)
131+
return spline, f"UnivariateSpline(s={s:.3g})"
132+
else:
133+
from scipy.interpolate import interp1d
134+
kind = "cubic" if method == "cubic" else "linear"
135+
f = interp1d(pixels, intensities, kind=kind, bounds_error=False, fill_value="extrapolate")
136+
return f, f"interp1d({kind})"
137+
except Exception:
138+
def lininterp(x):
139+
return np.interp(x, pixels, intensities)
140+
return lininterp, "numpy.interp(linear,fallback)"
141+
142+
def save_pixel_csv(pixels: np.ndarray, adcs: np.ndarray, intensities: np.ndarray, out_csv: Path):
143+
header = ["pixel", "ADC", "Ipixel"]
144+
with out_csv.open("w", newline="", encoding="utf-8") as f:
145+
w = csv.writer(f)
146+
w.writerow(header)
147+
for p, a, i in zip(pixels, adcs, intensities):
148+
w.writerow([int(p), float(a), float(i)])
149+
150+
def format_ctime_for_name(path: Path) -> str:
151+
# Use filesystem creation time when available
152+
try:
153+
ctime = path.stat().st_ctime
154+
except Exception:
155+
ctime = os.path.getctime(str(path))
156+
dt = datetime.fromtimestamp(ctime)
157+
return dt.strftime("%Y%m%d_%H%M%S")
158+
159+
def main():
160+
parser = argparse.ArgumentParser(description="Open a .dat file via dialog and produce intensity + interpolation outputs in a new timestamped folder.")
161+
parser.add_argument("infile", nargs="?", default=None, help="optional path to .dat file (if omitted, a file dialog will open)")
162+
parser.add_argument("--interp", choices=["spline","cubic","linear"], default="spline", help="interpolation method for overlay and sampled CSV")
163+
parser.add_argument("--dark-method", choices=["median","mean"], default="median", help="how to estimate ADC_dark")
164+
parser.add_argument("--samples", type=int, default=10000, help="number of points to sample the interpolated function")
165+
args = parser.parse_args()
166+
167+
infile_path: Path | None = None
168+
if args.infile:
169+
infile_path = Path(args.infile).expanduser().resolve()
170+
if not infile_path.is_file():
171+
print(f"Error: provided infile not found: {infile_path}", file=sys.stderr)
172+
sys.exit(2)
173+
else:
174+
chosen = choose_file_with_dialog()
175+
if chosen is None:
176+
# If dialog isn't available or user canceled, ask for path fallback
177+
if args.infile:
178+
infile_path = Path(args.infile).expanduser().resolve()
179+
else:
180+
p = input("No file chosen. Enter the path to the .dat file (or press Enter to quit): ").strip()
181+
if not p:
182+
print("No file provided. Exiting.")
183+
sys.exit(1)
184+
infile_path = Path(p).expanduser().resolve()
185+
else:
186+
infile_path = chosen
187+
188+
if not infile_path or not infile_path.is_file():
189+
print(f"Input file not found: {infile_path}", file=sys.stderr)
190+
sys.exit(2)
191+
192+
# Determine creation timestamp of the original file for folder name
193+
timestamp_str = format_ctime_for_name(infile_path)
194+
out_dir = infile_path.parent / f"{infile_path.stem}_{timestamp_str}"
195+
out_dir.mkdir(parents=True, exist_ok=True)
196+
out_prefix = infile_path.stem
197+
198+
print(f"Selected file: {infile_path}")
199+
print(f"Outputs will be written to: {out_dir}")
200+
201+
# Read and parse
202+
lines = read_file_lines(str(infile_path))
203+
pixels, adcs = parse_lines_to_arrays(lines)
204+
print(f"Read {pixels.size} samples (pixel range {pixels.min()}..{pixels.max()})")
205+
206+
adc_dark, dark_values = estimate_dark(pixels, adcs, method=args.dark_method)
207+
print(f"Estimated ADC_dark ({args.dark_method}) = {adc_dark:.3f} from {dark_values.size} dummy pixels")
208+
intensities = adc_dark - adcs
209+
210+
# Save per-pixel CSV
211+
out_csv = out_dir / f"{out_prefix}_Ipixel.csv"
212+
save_pixel_csv(pixels, adcs, intensities, out_csv)
213+
print(f"Wrote per-pixel CSV -> {out_csv}")
214+
215+
# Make interpolator and sample
216+
interp_fn, interp_kind = make_interpolator(pixels, intensities, method=args.interp)
217+
xs = np.linspace(pixels.min(), pixels.max(), max(1000, args.samples))
218+
try:
219+
ys = interp_fn(xs)
220+
ys = np.asarray(ys, dtype=float)
221+
except Exception as e:
222+
print("Interpolator evaluation failed, falling back to numpy.interp:", e)
223+
ys = np.interp(xs, pixels, intensities)
224+
interp_kind = "numpy.interp(fallback)"
225+
226+
sampled_csv = out_dir / f"{out_prefix}_interpolated_sample.csv"
227+
np.savetxt(sampled_csv, np.vstack([xs, ys]).T, fmt="%.6f,%.6f",
228+
header="pixel,Ipixel_interp", comments="")
229+
print(f"Saved interpolated samples ({interp_kind}) -> {sampled_csv}")
230+
231+
# Try to pickle interpolator
232+
pklfile = out_dir / f"{out_prefix}_interpolator.pkl"
233+
try:
234+
with pklfile.open("wb") as pf:
235+
pickle.dump(interp_fn, pf)
236+
print(f"Pickled interpolator -> {pklfile}")
237+
except Exception as e:
238+
print("Could not pickle interpolator (non-fatal):", e)
239+
240+
# Plot: raw ADC and Ipixel with overlay
241+
fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
242+
ax0.plot(pixels, adcs, color="tab:blue", lw=0.6, label="ADC (raw)")
243+
ax0.axhline(adc_dark, color="tab:orange", ls="--", lw=1.0, label=f"ADC_dark={adc_dark:.2f}")
244+
ax0.set_ylabel("ADC counts")
245+
ax0.legend(fontsize="small")
246+
ax0.grid(True, alpha=0.3)
247+
248+
ax1.plot(pixels, intensities, color="tab:green", lw=0.6, label="Ipixel (per-pixel)")
249+
ax1.plot(xs, ys, color="red", lw=1.0, alpha=0.9, label=f"Interpolated ({interp_kind})")
250+
ax1.set_xlabel("pixel number")
251+
ax1.set_ylabel("Ipixel = ADC_dark - ADC")
252+
ax1.legend(fontsize="small")
253+
ax1.grid(True, alpha=0.3)
254+
255+
# Shade dummy pixel regions if within range
256+
try:
257+
pmin, pmax = pixels.min(), pixels.max()
258+
if pmin <= 32:
259+
ax0.fill_betweenx(ax0.get_ylim(), 1, 32, color="gray", alpha=0.08)
260+
ax1.fill_betweenx(ax1.get_ylim(), 1, 32, color="gray", alpha=0.08)
261+
if pmax >= 3679:
262+
ax0.fill_betweenx(ax0.get_ylim(), 3679, 3694, color="gray", alpha=0.08)
263+
ax1.fill_betweenx(ax1.get_ylim(), 3679, 3694, color="gray", alpha=0.08)
264+
except Exception:
265+
pass
266+
267+
pngfile = out_dir / f"{out_prefix}_plot.png"
268+
plt.tight_layout()
269+
fig.savefig(str(pngfile), dpi=200)
270+
plt.close(fig)
271+
print(f"Saved plot -> {pngfile}")
272+
print("Done. All generated files are in:", out_dir)
273+
274+
if __name__ == "__main__":
275+
main()

0 commit comments

Comments
 (0)