From d3b1a9e3fd559b18ac0bb06916e956060441699b Mon Sep 17 00:00:00 2001 From: Markus Rosenstihl Date: Sun, 5 Jul 2026 13:44:20 +0200 Subject: [PATCH] Show message if ADC_Data is clipped (solves #16) --- src/damaris/data/ADC_Result.py | 41 ++++++++++++++++++++++++++++++++ src/damaris/data/Accumulation.py | 18 ++++++++++++-- src/damaris/gui/DamarisGUI.py | 29 +++++++++++++++++++++- src/damaris/gui/ResultReader.py | 2 ++ tests/res_test.py | 7 ++++++ 5 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/damaris/data/ADC_Result.py b/src/damaris/data/ADC_Result.py index a5837fe..e325daf 100644 --- a/src/damaris/data/ADC_Result.py +++ b/src/damaris/data/ADC_Result.py @@ -19,6 +19,7 @@ import tables ############################################################################# class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath): + default_bit_depth = 14 """ Represents the result of an ADC, encapsulating data and metadata for processing, visualization, and export. @@ -50,6 +51,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath): self.ylabel = "Samples [Digits]" self.lock=threading.RLock() self.nChannels = 0 + self.is_clipped = False # using no argument for initialization if (x is None) and (y is None) and (index is None) and (sampl_freq is None) and (desc is None) and (job_id is None) and (job_date is None): @@ -107,6 +109,44 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath): return self.cont_data + def check_clipping(self, bit_depth=None): + """ + Check if any data in y is clipped. + For a 14-bit ADC, clipping occurs at 2**13 (8192) or 2**13-1 (8191). + In general, for a bit_depth-bit signed ADC, limits are 2**(bit_depth-1) and -(2**(bit_depth-1)). + """ + if not self.contains_data(): + return False + + if bit_depth is None: + # try to get bit_depth from description + if hasattr(self, "description") and self.description is not None: + if "adc_bit_depth" in self.description: + try: + bit_depth = int(self.description["adc_bit_depth"]) + except (ValueError, TypeError): + bit_depth = ADC_Result.default_bit_depth + else: + bit_depth = ADC_Result.default_bit_depth + else: + bit_depth = ADC_Result.default_bit_depth + + limit_pos = 2**(bit_depth-1) - 1 + limit_neg = -2**(bit_depth-1) + + self.lock.acquire() + try: + self.is_clipped = False + for ch_data in self.y: + if numpy.any(ch_data >= limit_pos) or numpy.any(ch_data <= limit_neg): + self.is_clipped = True + break + finally: + self.lock.release() + + return self.is_clipped + + def add_sample_space(self, samples): "Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\"" @@ -575,4 +615,5 @@ def read_from_hdf(hdf_node): for ch in range(adc_data.shape[1]): adc.y.append(adc_data[:,ch]) + adc.check_clipping() return adc diff --git a/src/damaris/data/Accumulation.py b/src/damaris/data/Accumulation.py index 3787de1..623447b 100644 --- a/src/damaris/data/Accumulation.py +++ b/src/damaris/data/Accumulation.py @@ -61,6 +61,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): self.xlabel = "Time / s" self.ylabel = "Avg. Samples [Digits]" self.lock=threading.RLock() + self.is_clipped = False self.common_descriptions=None self.time_period=[] @@ -616,6 +617,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error) else: r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error) + r.is_clipped = self.is_clipped r.job_id = self.job_id r.job_ids = self.job_ids.copy() r.time_period = self.time_period[:] if self.time_period is not None else None @@ -645,6 +647,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = 1, index = other.index, sampl_freq = other.sampling_rate, error = True) else: r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False) + if hasattr(other, "is_clipped"): + r.is_clipped = other.is_clipped r.time_period=[other.job_date,other.job_date] r.job_id = other.job_id r.common_descriptions=other.description.copy() @@ -678,6 +682,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = True) else: r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False) + r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped) r.time_period=[min(self.time_period[0],other.job_date), max(self.time_period[1],other.job_date)] r.job_id = other.job_id @@ -744,13 +749,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): for i in range(self.get_number_of_channels()): tmp_y.append(self.y[i] + other.y[i]) - tmp_ysquare.append(self.y_square[i] + other.y_square[i]) + if self.uses_statistics(): + tmp_ysquare.append(self.y_square[i] + other.y_square[i]) if self.uses_statistics(): r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = True) else: r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False) - + r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped) r.time_period=[min(self.time_period[0],other.time_period[0]), max(self.time_period[1],other.time_period[1])] r.job_id = other.job_id @@ -954,6 +960,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): self.set_title(self.__title_pattern % self.n) self.lock.release() + if hasattr(other, "is_clipped"): + self.is_clipped = other.is_clipped self.time_period=[other.job_date,other.job_date] self.job_id = other.job_id # added by Oleg Petrov self.job_ids[other.job_id] = other.get_job_date() @@ -982,6 +990,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): self.y_square[i] += numpy.array(other.y[i], dtype="float32") ** 2 self.n += 1 + if hasattr(other, "is_clipped"): + self.is_clipped = self.is_clipped or other.is_clipped self.time_period=[min(self.time_period[0],other.job_date), max(self.time_period[1],other.job_date)] @@ -1020,6 +1030,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): self.y_square.append(self.y[i] ** 2) self.set_title(self.__title_pattern % self.n) + if hasattr(other, "is_clipped"): + self.is_clipped = other.is_clipped self.common_descriptions=other.common_desriptions.copy() self.time_period=other.time_period[:] self.job_id = other.job_id # added by Oleg Petrov @@ -1049,6 +1061,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath): self.y_square[i] += other.y_square[i] self.n += other.n + if hasattr(other, "is_clipped"): + self.is_clipped = self.is_clipped or other.is_clipped self.time_period=[min(self.time_period[0],other.time_period[0]), max(self.time_period[1],other.time_period[1])] self.job_id = other.job_id diff --git a/src/damaris/gui/DamarisGUI.py b/src/damaris/gui/DamarisGUI.py index a23685a..bbae2e7 100644 --- a/src/damaris/gui/DamarisGUI.py +++ b/src/damaris/gui/DamarisGUI.py @@ -2058,7 +2058,8 @@ pygobject version %(pygobject)s "data_pool_write_interval": self.config_data_pool_write_interval_entry.get_text( ), "data_pool_complib": complib, "data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ), - "script_font": self.config_script_font_button.get_font_name( ) + "script_font": self.config_script_font_button.get_font_name( ), + "adc_bit_depth": ADC_Result.default_bit_depth } def set( self, config ): @@ -2085,6 +2086,11 @@ pygobject version %(pygobject)s if "script_font" in config: self.config_script_font_button.set_font_name( config[ "script_font" ] ) self.set_script_font_handler( None ) + if "adc_bit_depth" in config: + try: + ADC_Result.default_bit_depth = int(config["adc_bit_depth"]) + except (ValueError, TypeError): + pass if "data_pool_complib" in config: # find combo-box entry and make it active... model = self.config_data_pool_complib.get_model( ) @@ -2373,6 +2379,7 @@ class MonitorWidgets: self.update_counter = 0 self.update_counter_lock = threading.Lock( ) self.description_text = None + self.clipping_marker = None def source_list_reset( self ): self.display_source_treestore.clear( ) @@ -2955,6 +2962,12 @@ class MonitorWidgets: except: pass self.description_text = None + if self.clipping_marker is not None: + try: + self.clipping_marker.remove() + except: + pass + self.clipping_marker = None self.matplot_canvas.draw_idle( ) def update_display( self, subject=None ): @@ -3142,6 +3155,20 @@ class MonitorWidgets: pass self.description_text = None + if getattr(in_result, "is_clipped", False): + if self.clipping_marker is None: + self.clipping_marker = self.matplot_axes.text(0.5, 0.95, "ADC OVERFLOW", + transform=self.matplot_axes.transAxes, + color="red", fontsize=20, fontweight="bold", + ha="center", va="top", + bbox=dict(facecolor='white', alpha=0.7, edgecolor='red')) + elif self.clipping_marker is not None: + try: + self.clipping_marker.remove() + except: + pass + self.clipping_marker = None + # Draw it! self.matplot_canvas.draw_idle( ) del in_result diff --git a/src/damaris/gui/ResultReader.py b/src/damaris/gui/ResultReader.py index 781af34..a7ed868 100644 --- a/src/damaris/gui/ResultReader.py +++ b/src/damaris/gui/ResultReader.py @@ -239,6 +239,7 @@ class ResultReader: self.result.index = [(0,tmp_size-1)] tmp_index += tmp_size self.result.cont_data=True + self.result.check_clipping() tmp_part = None def __parseFile_expat(self, in_file): @@ -310,6 +311,7 @@ class ResultReader: self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1)) tmp_sample_counter+=tmp_size self.result.cont_data=True + self.result.check_clipping() # Callback when a xml start tag is found def __xmlStartTagFound(self, in_name, in_attribute): diff --git a/tests/res_test.py b/tests/res_test.py index 7d5555c..fbffda9 100644 --- a/tests/res_test.py +++ b/tests/res_test.py @@ -1,4 +1,5 @@ import tables +import numpy r = MeasurementResult("test") @@ -9,6 +10,12 @@ def result(): print("h5 already opened") accu = Accumulation(error=False) for num,ts in enumerate(results): + x = numpy.linspace(0, 1, 10) + y1 = [numpy.array([0, 4000, 8190, 8191, 8192, 0, -8191, -8192, -8193, 0], dtype="int16")] + + # Test with default (14-bit) + adc1 = ADC_Result(x=x, y=y1, index=[(0,9)], sampl_freq=1000, desc={}, job_id=1, job_date=0) + accu.is_clipped = True data["accu"] = accu data["data/ts %i"%num] = ts+0 ts.write_to_hdf(h5, "/", f"adc_{num}", f"adc_{num}")