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\t o\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