2626 A file dialog will open; pick your lamp.dat (or any .dat file).
2727
2828 - Or run and pass a path to skip the dialog:
29- python plot_lamp_filepicker.py "C:\path\t o \lamp.dat"
29+ python plot_lamp_filepicker.py "C:\\ path\\ to \ \ lamp.dat"
3030
3131Notes:
3232 - If you prefer not to have a GUI, pass the file path as the first argument.
3838import sys
3939import os
4040import csv
41- import pickle
4241import numpy as np
4342import matplotlib
44- matplotlib .use ("Agg" ) # safe backend for non-interactive use
43+
44+ matplotlib .use ("Agg" ) # safe backend for non-interactive use
4545import matplotlib .pyplot as plt
4646from datetime import datetime
47+ from scipy .interpolate import UnivariateSpline , interp1d
48+
4749
4850def choose_file_with_dialog (initialdir : str | None = None ) -> Path | None :
4951 try :
@@ -56,13 +58,16 @@ def choose_file_with_dialog(initialdir: str | None = None) -> Path | None:
5658 root .withdraw ()
5759 # On Windows the dialog will be a native explorer-file dialog
5860 filetypes = [("DAT files" , "*.dat" ), ("All files" , "*.*" )]
59- filename = filedialog .askopenfilename (title = "Select .dat file" , initialdir = initialdir or "." , filetypes = filetypes )
61+ filename = filedialog .askopenfilename (
62+ title = "Select .dat file" , initialdir = initialdir or "." , filetypes = filetypes
63+ )
6064 root .update ()
6165 root .destroy ()
6266 if not filename :
6367 return None
6468 return Path (filename ).expanduser ().resolve ()
6569
70+
6671def read_file_lines (filename : str ):
6772 encodings = ["utf-8" , "cp1252" , "latin-1" ]
6873 for enc in encodings :
@@ -83,6 +88,7 @@ def read_file_lines(filename: str):
8388 print ("Opened file via binary fallback, decoded with errors='replace'." )
8489 return lines
8590
91+
8692def parse_lines_to_arrays (lines ):
8793 pixels = []
8894 adcs = []
@@ -98,8 +104,10 @@ def parse_lines_to_arrays(lines):
98104 a = float (parts [1 ])
99105 except Exception :
100106 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" )))
107+ p = int ("" .join (ch for ch in parts [0 ] if (ch .isdigit () or ch == "-" )))
108+ a = float (
109+ "" .join (ch for ch in parts [1 ] if (ch .isdigit () or ch in ".-eE" ))
110+ )
103111 except Exception :
104112 continue
105113 pixels .append (p )
@@ -108,6 +116,7 @@ def parse_lines_to_arrays(lines):
108116 raise ValueError ("No valid pixel/ADC pairs found in file." )
109117 return np .array (pixels , dtype = int ), np .array (adcs , dtype = float )
110118
119+
111120def estimate_dark (pixels : np .ndarray , adcs : np .ndarray , method : str = "median" ):
112121 mask = ((pixels >= 1 ) & (pixels <= 32 )) | ((pixels >= 3679 ) & (pixels <= 3694 ))
113122 dark_vals = adcs [mask ]
@@ -116,42 +125,58 @@ def estimate_dark(pixels: np.ndarray, adcs: np.ndarray, method: str = "median"):
116125 if n >= 64 :
117126 dark_vals = np .concatenate ((adcs [:32 ], adcs [- 32 :]))
118127 else :
119- dark_vals = adcs [:max (1 , n // 10 )]
128+ dark_vals = adcs [: max (1 , n // 10 )]
120129 if method == "median" :
121130 return float (np .median (dark_vals )), dark_vals
122131 else :
123132 return float (np .mean (dark_vals )), dark_vals
124133
125- def make_interpolator (pixels : np .ndarray , intensities : np .ndarray , method : str = "spline" , smooth : float = 0.2 ):
134+
135+ def make_interpolator (
136+ pixels : np .ndarray ,
137+ intensities : np .ndarray ,
138+ method : str = "spline" ,
139+ smooth : float = 0.2 ,
140+ ):
126141 """Create an interpolator. `smooth` increases smoothing when using spline (larger -> smoother).
127142
128143 Returns (callable, description)
129144 """
130145 try :
131146 if method == "spline" :
132- from scipy .interpolate import UnivariateSpline
133147 # stronger smoothing by default: scale factor (user-controlled)
134148 s = max (0.0 , len (pixels ) * np .var (intensities ) * float (smooth ))
135149 spline = UnivariateSpline (pixels , intensities , s = s )
136150 return spline , f"UnivariateSpline(s={ s :.3g} )"
137151 else :
138- from scipy .interpolate import interp1d
139152 kind = "cubic" if method == "cubic" else "linear"
140- f = interp1d (pixels , intensities , kind = kind , bounds_error = False , fill_value = "extrapolate" )
153+ f = interp1d (
154+ pixels ,
155+ intensities ,
156+ kind = kind ,
157+ bounds_error = False ,
158+ fill_value = "extrapolate" ,
159+ )
141160 return f , f"interp1d({ kind } )"
142161 except Exception :
162+
143163 def lininterp (x ):
144164 return np .interp (x , pixels , intensities )
165+
145166 return lininterp , "numpy.interp(linear,fallback)"
146167
147- def save_pixel_csv (pixels : np .ndarray , adcs : np .ndarray , intensities : np .ndarray , out_csv : Path ):
168+
169+ def save_pixel_csv (
170+ pixels : np .ndarray , adcs : np .ndarray , intensities : np .ndarray , out_csv : Path
171+ ):
148172 header = ["pixel" , "ADC" , "Ipixel" ]
149173 with out_csv .open ("w" , newline = "" , encoding = "utf-8" ) as f :
150174 w = csv .writer (f )
151175 w .writerow (header )
152176 for p , a , i in zip (pixels , adcs , intensities ):
153177 w .writerow ([int (p ), float (a ), float (i )])
154178
179+
155180def format_ctime_for_name (path : Path ) -> str :
156181 # Use filesystem creation time when available
157182 try :
@@ -161,16 +186,54 @@ def format_ctime_for_name(path: Path) -> str:
161186 dt = datetime .fromtimestamp (ctime )
162187 return dt .strftime ("%Y%m%d_%H%M%S" )
163188
189+
164190def main ():
165- parser = argparse .ArgumentParser (description = "Open a .dat file via dialog and produce intensity + regression outputs in a new timestamped folder." )
166- parser .add_argument ("infile" , nargs = "?" , default = None , help = "optional path to .dat file (if omitted, a file dialog will open)" )
167- parser .add_argument ("--interp" , choices = ["spline" ,"cubic" ,"linear" ], default = "spline" , help = "regression method for overlay and sampled CSV" )
168- parser .add_argument ("--dark-method" , choices = ["median" ,"mean" ], default = "median" , help = "how to estimate ADC_dark" )
169- parser .add_argument ("--samples" , type = int , default = 10000 , help = "number of points to sample the interpolated function" )
170- parser .add_argument ("--smooth" , type = float , default = 0.2 , help = "smoothing multiplier for spline (larger -> smoother). Default 0.2" )
191+ parser = argparse .ArgumentParser (
192+ description = "Open a .dat file via dialog and produce intensity + regression outputs in a new timestamped folder."
193+ )
194+ parser .add_argument (
195+ "infile" ,
196+ nargs = "?" ,
197+ default = None ,
198+ help = "optional path to .dat file (if omitted, a file dialog will open)" ,
199+ )
200+ parser .add_argument (
201+ "--interp" ,
202+ choices = ["spline" , "cubic" , "linear" ],
203+ default = "spline" ,
204+ help = "regression method for overlay and sampled CSV" ,
205+ )
206+ parser .add_argument (
207+ "--dark-method" ,
208+ choices = ["median" , "mean" ],
209+ default = "median" ,
210+ help = "how to estimate ADC_dark" ,
211+ )
212+ parser .add_argument (
213+ "--samples" ,
214+ type = int ,
215+ default = 10000 ,
216+ help = "number of points to sample the interpolated function" ,
217+ )
218+ parser .add_argument (
219+ "--smooth" ,
220+ type = float ,
221+ default = 0.2 ,
222+ help = "smoothing multiplier for spline (larger -> smoother). Default 0.2" ,
223+ )
171224 # Default smooths: keep only the weaker values (remove 0.1 and 0.2 as requested)
172- parser .add_argument ("--smooths" , type = str , default = "0.01,0.02,0.05" , help = "comma-separated list of smoothing multipliers to plot multiple regressions (e.g. '0.01,0.02,0.05')" )
173- parser .add_argument ("--linewidth" , type = float , default = 0.6 , help = "default line width for PNG output (thin lines)" )
225+ parser .add_argument (
226+ "--smooths" ,
227+ type = str ,
228+ default = "0.01,0.02,0.05" ,
229+ help = "comma-separated list of smoothing multipliers to plot multiple regressions (e.g. '0.01,0.02,0.05')" ,
230+ )
231+ parser .add_argument (
232+ "--linewidth" ,
233+ type = float ,
234+ default = 0.6 ,
235+ help = "default line width for PNG output (thin lines)" ,
236+ )
174237 args = parser .parse_args ()
175238
176239 infile_path : Path | None = None
@@ -186,7 +249,9 @@ def main():
186249 if args .infile :
187250 infile_path = Path (args .infile ).expanduser ().resolve ()
188251 else :
189- p = input ("No file chosen. Enter the path to the .dat file (or press Enter to quit): " ).strip ()
252+ p = input (
253+ "No file chosen. Enter the path to the .dat file (or press Enter to quit): "
254+ ).strip ()
190255 if not p :
191256 print ("No file provided. Exiting." )
192257 sys .exit (1 )
@@ -213,7 +278,9 @@ def main():
213278 print (f"Read { pixels .size } samples (pixel range { pixels .min ()} ..{ pixels .max ()} )" )
214279
215280 adc_dark , dark_values = estimate_dark (pixels , adcs , method = args .dark_method )
216- print (f"Estimated ADC_dark ({ args .dark_method } ) = { adc_dark :.3f} from { dark_values .size } dummy pixels" )
281+ print (
282+ f"Estimated ADC_dark ({ args .dark_method } ) = { adc_dark :.3f} from { dark_values .size } dummy pixels"
283+ )
217284 intensities = adc_dark - adcs
218285
219286 # Save per-pixel CSV
@@ -227,13 +294,17 @@ def main():
227294
228295 # Parse multi-smoothing values for plotting several regression strengths
229296 try :
230- smooth_values = [float (s .strip ()) for s in str (args .smooths ).split ("," ) if s .strip ()]
297+ smooth_values = [
298+ float (s .strip ()) for s in str (args .smooths ).split ("," ) if s .strip ()
299+ ]
231300 except Exception :
232301 smooth_values = [args .smooth ]
233302
234303 interp_results = []
235304 for s_val in smooth_values :
236- interp_fn_i , interp_kind_i = make_interpolator (pixels , intensities , method = args .interp , smooth = s_val )
305+ interp_fn_i , interp_kind_i = make_interpolator (
306+ pixels , intensities , method = args .interp , smooth = s_val
307+ )
237308 try :
238309 ys_i = interp_fn_i (xs )
239310 ys_i = np .asarray (ys_i , dtype = float )
@@ -246,7 +317,9 @@ def main():
246317 fig , (ax0 , ax1 , ax2 ) = plt .subplots (3 , 1 , figsize = (12 , 10 ), sharex = True )
247318 lw = float (args .linewidth )
248319 ax0 .plot (pixels , adcs , color = "tab:blue" , lw = lw , label = "ADC (raw)" )
249- ax0 .axhline (adc_dark , color = "tab:orange" , ls = "--" , lw = lw , label = f"ADC_dark={ adc_dark :.2f} " )
320+ ax0 .axhline (
321+ adc_dark , color = "tab:orange" , ls = "--" , lw = lw , label = f"ADC_dark={ adc_dark :.2f} "
322+ )
250323 ax0 .set_ylabel ("ADC counts" )
251324 ax0 .legend (fontsize = "small" )
252325 ax0 .grid (True , alpha = 0.3 )
@@ -255,7 +328,13 @@ def main():
255328 # Use max-based inversion to keep signal in the same positive range
256329 y_flipped = np .max (intensities ) - intensities
257330 # Make the flipped trace more visually distinct: green, slightly thicker
258- ax1 .plot (pixels , y_flipped , color = "green" , lw = max (0.8 , lw * 1.2 ), label = "Ipixel (flipped)" )
331+ ax1 .plot (
332+ pixels ,
333+ y_flipped ,
334+ color = "green" ,
335+ lw = max (0.8 , lw * 1.2 ),
336+ label = "Ipixel (flipped)" ,
337+ )
259338 ax1 .set_ylabel ("Flipped Ipixel" )
260339 # Visually invert the y-axis so the panel appears upside-down (peaks downwards)
261340 ax1 .invert_yaxis ()
@@ -266,16 +345,30 @@ def main():
266345 # Plot a very faint original Ipixel trace in the regression panel for reference
267346 orig_on_xs = np .interp (xs , pixels , intensities )
268347 # Slightly more visible raw trace for reference
269- ax2 .plot (xs , orig_on_xs , color = "gray" , lw = 0.8 , alpha = 0.28 , label = "raw Ipixel (faint)" )
348+ ax2 .plot (
349+ xs , orig_on_xs , color = "gray" , lw = 0.8 , alpha = 0.28 , label = "raw Ipixel (faint)"
350+ )
270351
271352 # Use only the requested distinct colors (no green/yellow): blue, red, purple
272353 palette = ["blue" , "red" , "purple" ]
273354 colors = [palette [i % len (palette )] for i in range (len (interp_results ))]
274355 import math
356+
275357 for (s_val , kind , ys_i ), col in zip (interp_results , colors ):
276358 # Force s=0.05 to green for emphasis per request (use isclose for safety)
277- plot_col = "green" if math .isclose (float (s_val ), 0.05 , rel_tol = 1e-6 , abs_tol = 1e-9 ) else col
278- ax2 .plot (xs , ys_i , color = plot_col , lw = max (0.8 , lw ), alpha = 0.95 , label = f"s={ s_val } ({ kind } )" )
359+ plot_col = (
360+ "green"
361+ if math .isclose (float (s_val ), 0.05 , rel_tol = 1e-6 , abs_tol = 1e-9 )
362+ else col
363+ )
364+ ax2 .plot (
365+ xs ,
366+ ys_i ,
367+ color = plot_col ,
368+ lw = max (0.8 , lw ),
369+ alpha = 0.95 ,
370+ label = f"s={ s_val } ({ kind } )" ,
371+ )
279372 ax2 .set_xlabel ("pixel number" )
280373 ax2 .set_ylabel ("Ipixel (interpolated)" )
281374 ax2 .legend (fontsize = "small" )
@@ -302,5 +395,6 @@ def main():
302395 print (f"Saved plot -> { pngfile } " )
303396 print ("Done. All generated files are in:" , out_dir )
304397
398+
305399if __name__ == "__main__" :
306- main ()
400+ main ()
0 commit comments