12 Commits
Author SHA1 Message Date
markusro 7ba5c7d56a added HDF5 read capability. Closes #27
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m37s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m50s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-06 20:42:04 +02:00
markusro c181e15aa8 fixed some python2 to python3 changes as well as dat time formats 2026-07-06 20:40:59 +02:00
markusro 3bfb2670eb initial hdfreader implementation 2026-07-05 19:36:20 +02:00
markusro 83a01f1d83 new release
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m25s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m22s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 14:13:32 +02:00
markusro b46debad69 Fixed #13 and #14
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m2s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m11s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 14:07:57 +02:00
markusro 4250039b3c can now set ADC_Result bit depth in the config window (Bug #16)
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m3s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m4s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 13:59:09 +02:00
markusro d3b1a9e3fd Show message if ADC_Data is clipped (solves #16)
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m38s
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m40s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 13:44:20 +02:00
markusro 06867ec17b Missing search input field fixed (Fixes #25)
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m5s
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m7s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 13:24:20 +02:00
markusro 80d8f1e78d fixed stop button, no can interrupt running experiment
Build Debian Packages / build (trixie, debian13) (push) Successful in 15m23s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 15m46s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-04 21:03:23 +02:00
markusro 58e7846bbe description is plotted better now 2026-07-04 20:50:57 +02:00
markusro 1c4f8f5344 Improved interruptable sleep and warn user when using time.sleep.
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m4s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m9s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-04 20:26:14 +02:00
markusro a3738025b7 fixed autocomplete in the editor windows
Build Debian Packages / build (trixie, debian13) (push) Successful in 12m59s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m9s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-04 20:01:34 +02:00
13 changed files with 739 additions and 89 deletions

No files matched your search

+9
View File
@@ -1,3 +1,12 @@
python3-damaris (0.22) stable; urgency=medium
* fixed autocomplete in the editor tabs
* added clipping warning on ADC overload, ADC bit-depth settable in config
* Accumulation object keeps track of job_ids
* get_result_by_index keeps common description
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 05 Jul 2026 14:09:34 +0200
python3-damaris (0.20) stable; urgency=medium python3-damaris (0.20) stable; urgency=medium
* Unified version management across Python app, GUI and documentation * Unified version management across Python app, GUI and documentation
+1 -1
View File
@@ -15,5 +15,5 @@
## module contents ## module contents
# #
__version__ = "0.20" __version__ = "0.22"
__all__=["experiments", "data", "gui"] __all__=["experiments", "data", "gui"]
+44 -1
View File
@@ -19,6 +19,7 @@ import tables
############################################################################# #############################################################################
class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath): class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
default_bit_depth = 14
""" """
Represents the result of an ADC, encapsulating data and metadata Represents the result of an ADC, encapsulating data and metadata
for processing, visualization, and export. for processing, visualization, and export.
@@ -50,6 +51,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
self.ylabel = "Samples [Digits]" self.ylabel = "Samples [Digits]"
self.lock=threading.RLock() self.lock=threading.RLock()
self.nChannels = 0 self.nChannels = 0
self.is_clipped = False
# using no argument for initialization # 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): 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 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): 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\"" "Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
@@ -143,7 +183,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(self.y[i][start:end+1].copy()) tmp_y.append(self.y[i][start:end+1].copy())
r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description.copy() if self.description is not None else None, job_id = self.job_id, job_date = self.job_date)
self.lock.release() self.lock.release()
return r return r
@@ -536,6 +576,8 @@ def read_from_hdf(hdf_node):
# job id and x,y titles are missing # job id and x,y titles are missing
adc=ADC_Result() adc=ADC_Result()
if hdf_node._v_title:
adc.set_title(hdf_node._v_title)
# populate description dictionary # populate description dictionary
adc.description={} adc.description={}
for attrname in hdf_node._v_attrs._v_attrnamesuser: for attrname in hdf_node._v_attrs._v_attrnamesuser:
@@ -573,4 +615,5 @@ def read_from_hdf(hdf_node):
for ch in range(adc_data.shape[1]): for ch in range(adc_data.shape[1]):
adc.y.append(adc_data[:,ch]) adc.y.append(adc_data[:,ch])
adc.check_clipping()
return adc return adc
+92 -21
View File
@@ -61,6 +61,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.xlabel = "Time / s" self.xlabel = "Time / s"
self.ylabel = "Avg. Samples [Digits]" self.ylabel = "Avg. Samples [Digits]"
self.lock=threading.RLock() self.lock=threading.RLock()
self.is_clipped = False
self.common_descriptions=None self.common_descriptions=None
self.time_period=[] self.time_period=[]
@@ -117,9 +118,23 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(self.y[i][start:end+1]) tmp_y.append(self.y[i][start:end+1])
r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error) r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error)
if self.uses_statistics():
r.y_square = []
for i in range(self.get_number_of_channels()):
r.y_square.append(self.y_square[i][start:end+1])
if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy()
r.time_period = self.time_period[:]
r.job_ids = self.job_ids.copy()
self.lock.release() self.lock.release()
return r return r
def get_result_by_index(self, index):
return self.get_accu_by_index(index)
def get_Result_by_index(self, index):
return self.get_accu_by_index(index)
def get_ysquare(self, channel): def get_ysquare(self, channel):
if self.uses_statistics(): if self.uses_statistics():
try: try:
@@ -616,6 +631,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) 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: 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 = 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_id = self.job_id
r.job_ids = self.job_ids.copy() r.job_ids = self.job_ids.copy()
r.time_period = self.time_period[:] if self.time_period is not None else None r.time_period = self.time_period[:] if self.time_period is not None else None
@@ -645,6 +661,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) 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: 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) 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.time_period=[other.job_date,other.job_date]
r.job_id = other.job_id r.job_id = other.job_id
r.common_descriptions=other.description.copy() r.common_descriptions=other.description.copy()
@@ -678,6 +696,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) 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: 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 = 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), r.time_period=[min(self.time_period[0],other.job_date),
max(self.time_period[1],other.job_date)] max(self.time_period[1],other.job_date)]
r.job_id = other.job_id r.job_id = other.job_id
@@ -744,13 +763,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(self.y[i] + other.y[i]) 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(): 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) 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: 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 = 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]), r.time_period=[min(self.time_period[0],other.time_period[0]),
max(self.time_period[1],other.time_period[1])] max(self.time_period[1],other.time_period[1])]
r.job_id = other.job_id r.job_id = other.job_id
@@ -954,6 +974,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.set_title(self.__title_pattern % self.n) self.set_title(self.__title_pattern % self.n)
self.lock.release() self.lock.release()
if hasattr(other, "is_clipped"):
self.is_clipped = other.is_clipped
self.time_period=[other.job_date,other.job_date] self.time_period=[other.job_date,other.job_date]
self.job_id = other.job_id # added by Oleg Petrov self.job_id = other.job_id # added by Oleg Petrov
self.job_ids[other.job_id] = other.get_job_date() self.job_ids[other.job_id] = other.get_job_date()
@@ -982,6 +1004,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.y_square[i] += numpy.array(other.y[i], dtype="float32") ** 2 self.y_square[i] += numpy.array(other.y[i], dtype="float32") ** 2
self.n += 1 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), self.time_period=[min(self.time_period[0],other.job_date),
max(self.time_period[1],other.job_date)] max(self.time_period[1],other.job_date)]
@@ -1020,10 +1044,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.y_square.append(self.y[i] ** 2) self.y_square.append(self.y[i] ** 2)
self.set_title(self.__title_pattern % self.n) self.set_title(self.__title_pattern % self.n)
self.common_descriptions=other.common_desriptions.copy() if hasattr(other, "is_clipped"):
self.is_clipped = other.is_clipped
self.common_descriptions=other.common_descriptions.copy()
self.time_period=other.time_period[:] self.time_period=other.time_period[:]
self.job_id = other.job_id # added by Oleg Petrov self.job_id = other.job_id # added by Oleg Petrov
self.job_ids += other.job_ids self.job_ids.update(other.job_ids)
self.lock.release() self.lock.release()
return self return self
@@ -1049,9 +1075,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.y_square[i] += other.y_square[i] self.y_square[i] += other.y_square[i]
self.n += other.n 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]), self.time_period=[min(self.time_period[0],other.time_period[0]),
max(self.time_period[1],other.time_period[1])] max(self.time_period[1],other.time_period[1])]
self.job_id = other.job_id self.job_id = other.job_id
self.job_ids.update(other.job_ids)
# Removes mismatched common description keys # Removes mismatched common description keys
if self.common_descriptions is not None and other.common_descriptions is not None: if self.common_descriptions is not None and other.common_descriptions is not None:
# Get all keys that exist in both dictionaries # Get all keys that exist in both dictionaries
@@ -1131,26 +1160,66 @@ def read_from_hdf(hdf_node):
earliest_time=None earliest_time=None
if "earliest_time" in dir(hdf_node._v_attrs): if "earliest_time" in dir(hdf_node._v_attrs):
timestring=hdf_node._v_attrs.__getattr__("earliest_time") timestring=hdf_node._v_attrs.__getattr__("earliest_time")
earliest_time=datetime.datetime(int(timestring[:4]), # year # Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
int(timestring[4:6]), # month if '-' in timestring:
int(timestring[6:8]), # day # New format: "2026-07-05 19:13:59.162"
int(timestring[9:11]), # hour try:
int(timestring[12:14]), # minute earliest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
int(timestring[15:17]), # second except ValueError:
int(timestring[18:21])*1000 # microsecond # Fallback to manual parsing
) parts = timestring.split()
if len(parts) >= 2:
date_parts = parts[0].split('-')
time_parts = parts[1].split(':')
if len(date_parts) == 3 and len(time_parts) >= 2:
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
hour, minute = int(time_parts[0]), int(time_parts[1])
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
second = int(second_parts[0])
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
earliest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
else:
# Old format: "20260705 19:13:59.162"
earliest_time=datetime.datetime(int(timestring[:4]), # year
int(timestring[4:6]), # month
int(timestring[6:8]), # day
int(timestring[9:11]), # hour
int(timestring[12:14]), # minute
int(timestring[15:17]), # second
int(timestring[18:21])*1000 # microsecond
)
oldest_time=None oldest_time=None
if "oldest_time" in dir(hdf_node._v_attrs): if "oldest_time" in dir(hdf_node._v_attrs):
timestring=hdf_node._v_attrs.__getattr__("oldest_time") timestring=hdf_node._v_attrs.__getattr__("oldest_time")
oldest_time=datetime.datetime(int(timestring[:4]), # year # Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
int(timestring[4:6]), # month if '-' in timestring:
int(timestring[6:8]), # day # New format: "2026-07-05 19:13:59.162"
int(timestring[9:11]), # hour try:
int(timestring[12:14]), # minute oldest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
int(timestring[15:17]), # second except ValueError:
int(timestring[18:21])*1000 # microsecond # Fallback to manual parsing
) parts = timestring.split()
if len(parts) >= 2:
date_parts = parts[0].split('-')
time_parts = parts[1].split(':')
if len(date_parts) == 3 and len(time_parts) >= 2:
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
hour, minute = int(time_parts[0]), int(time_parts[1])
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
second = int(second_parts[0])
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
oldest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
else:
# Old format: "20260705 19:13:59.162"
oldest_time=datetime.datetime(int(timestring[:4]), # year
int(timestring[4:6]), # month
int(timestring[6:8]), # day
int(timestring[9:11]), # hour
int(timestring[12:14]), # minute
int(timestring[15:17]), # second
int(timestring[18:21])*1000 # microsecond
)
if oldest_time is None or earliest_time is None: if oldest_time is None or earliest_time is None:
accu.time_period=None accu.time_period=None
@@ -1167,6 +1236,8 @@ def read_from_hdf(hdf_node):
accu.index.append((r["start"],r["start"]+r["length"]-1)) accu.index.append((r["start"],r["start"]+r["length"]-1))
accu.n=r["number"] accu.n=r["number"]
accu.sampling_rate=1.0/r["dwelltime"] accu.sampling_rate=1.0/r["dwelltime"]
accu.set_title("Accumulation: n = %d" % accu.n)
# now really belief there are no data # now really belief there are no data
if len(accu.index)==0 or accu.n==0: if len(accu.index)==0 or accu.n==0:
@@ -1181,7 +1252,7 @@ def read_from_hdf(hdf_node):
accu.y_square=[] accu.y_square=[]
accu.use_error=False accu.use_error=False
for ch in range(accu_data.shape[1]/2): for ch in range(accu_data.shape[1] // 2):
accu.y.append(accu_data[:,ch*2]*accu.n) accu.y.append(accu_data[:,ch*2]*accu.n)
if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0): if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0):
accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32")) accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32"))
+244 -4
View File
@@ -7,6 +7,7 @@ import collections
import threading import threading
import traceback import traceback
import io import io
import numpy
class DataPool(collections.abc.MutableMapping): class DataPool(collections.abc.MutableMapping):
""" """
@@ -107,7 +108,7 @@ class DataPool(collections.abc.MutableMapping):
self.__registered_listeners=None self.__registered_listeners=None
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None): def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
if type(hdffile) is bytes: if isinstance(hdffile, (bytes, str)):
dump_file=tables.open_file(hdffile, mode="a") dump_file=tables.open_file(hdffile, mode="a")
elif isinstance(hdffile,tables.File): elif isinstance(hdffile,tables.File):
dump_file=hdffile dump_file=hdffile
@@ -156,7 +157,7 @@ class DataPool(collections.abc.MutableMapping):
value=self.__mydict[key] value=self.__mydict[key]
self.__dictlock.release() self.__dictlock.release()
# now write data, assuming, the object is constant during write operation # now write data, assuming, the object is constant during write operation
if "write_to_hdf" in dir(value): if hasattr(value, "write_to_hdf"):
try: try:
value.write_to_hdf(hdffile=dump_file, value.write_to_hdf(hdffile=dump_file,
where=dump_dir, where=dump_dir,
@@ -170,16 +171,255 @@ class DataPool(collections.abc.MutableMapping):
traceback.print_tb(sys.exc_info()[2], None, traceback_file) traceback.print_tb(sys.exc_info()[2], None, traceback_file)
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue()) print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
traceback_file=None traceback_file=None
elif isinstance(value, (numpy.ndarray, int, float, str, bytes)):
try:
obj_to_save = value
if isinstance(value, str):
obj_to_save = value.encode('utf-8')
dump_file.create_array(dump_dir,
name=group_keyname,
obj=obj_to_save,
title=key)
except Exception as e:
print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
else: else:
print("don't know how to store data_pool[\"%s\"]"%key) print("don't know how to store data_pool[\"%s\"] (type: %s)"%(key, type(value)))
value=None value=None
finally: finally:
dump_group=None dump_group=None
if type(hdffile) is bytes: if isinstance(hdffile, (bytes, str)):
dump_file.close() dump_file.close()
dump_file=None dump_file=None
def read_from_hdf(self, hdffile, where="/data_pool"):
"""
Read data from HDF5 file and populate the DataPool.
Parameters:
- hdffile: HDF5 file object or filename (bytes/str)
- where: Location within HDF5 file to read from (default: "/data_pool")
"""
# Handle both filename and file object
if type(hdffile) is bytes or isinstance(hdffile, str):
hdf_file = tables.open_file(hdffile, mode="r")
close_file = True
elif isinstance(hdffile, tables.File):
hdf_file = hdffile
close_file = False
else:
raise Exception("expecting hdffile or string")
try:
# Navigate to the data pool group
if where not in hdf_file:
raise Exception(f"HDF5 location '{where}' not found")
data_pool_group = hdf_file.get_node(where)
# Recursively process the group structure
self._read_hdf_group(data_pool_group, "")
finally:
if close_file:
hdf_file.close()
def _read_hdf_group(self, group, path_prefix):
"""
Recursively read HDF5 group and populate DataPool.
Parameters:
- group: HDF5 group object
- path_prefix: Current path in DataPool hierarchy
"""
# Process all nodes in this group
for node in group:
# Get the node name (last part of the path)
node_name = node._v_name
# Try to get the original name from title
title = node._v_title
if title:
# For DAMARIS objects, the title is usually the full path
# For intermediate groups, it's just the part name
if "/" in title:
full_path = title
else:
if path_prefix:
full_path = f"{path_prefix}/{title}"
else:
full_path = title
else:
# Strip dir_ and dict_ prefixes from node name
clean_name = self._clean_hdf_name(node_name)
# Build the full path in DataPool hierarchy
if path_prefix:
full_path = f"{path_prefix}/{clean_name}"
else:
full_path = clean_name
full_path = full_path.replace("//", "/")
if isinstance(node, (tables.Group, tables.Table)):
# Check if this is a known DAMARIS object type
if hasattr(node._v_attrs, 'damaris_type'):
damaris_type = node._v_attrs.damaris_type
# Handle different DAMARIS object types
if damaris_type == "Accumulation":
from damaris.data.Accumulation import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
elif damaris_type == "ADC_Result":
from damaris.data.ADC_Result import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
elif damaris_type == "MeasurementResult":
from damaris.data.MeasurementResult import read_from_hdf
obj = read_from_hdf(node)
if obj is not None:
self[full_path] = obj
# Skip further processing of this group since we handled it
continue
# Recursively process sub-groups
if isinstance(node, tables.Group):
self._read_hdf_group(node, full_path)
elif isinstance(node, (tables.Array, tables.CArray, tables.EArray, tables.Table)):
# Handle simple array data
try:
data = node.read()
# If it's a scalar array of bytes, decode it to string
if hasattr(data, 'decode'):
try:
data = data.decode('utf-8')
except:
pass
elif isinstance(data, numpy.ndarray) and data.dtype.kind in ('S', 'V'):
# For numpy byte arrays, try to decode if they are scalar or small
if data.size == 1:
try:
decoded = data.item().decode('utf-8')
data = decoded
except:
pass
self[full_path] = data
except Exception as e:
print(f"Warning: Could not read array {full_path}: {e}")
# Note: Other node types (Table, etc.) would be handled here as needed
def _clean_hdf_name(self, name):
"""
Clean HDF5 node name by removing dir_ and dict_ prefixes.
Parameters:
- name: HDF5 node name (may have dir_ or dict_ prefix)
Returns:
- Cleaned name with prefixes removed
"""
if name.startswith("dir_"):
return name[4:] # Remove "dir_" prefix
elif name.startswith("dict_"):
return name[5:] # Remove "dict_" prefix
else:
return name
@classmethod
def load_hdf5(cls, filename):
"""
Load a DAMARIS HDF5 file and return DataPool and metadata.
Parameters:
- filename: Path to HDF5 file
Returns:
- tuple: (DataPool instance, metadata dict)
metadata dict contains:
- 'scripts': dict with experiment scripts and spool info
- 'log': log data if available
- 'timeline': timeline data if available
"""
# Validate file
if not tables.is_pytables_file(filename):
raise Exception(f"File {filename} is not a valid PyTables HDF5 file")
# Create DataPool and load data
data_pool = cls()
try:
hdf_file = tables.open_file(filename, mode="r")
# Load main data pool
if "/data_pool" in hdf_file:
data_pool.read_from_hdf(hdf_file, where="/data_pool")
# Extract metadata
metadata = {}
# Load scripts
if "/scripts" in hdf_file:
scripts_group = hdf_file.get_node("/scripts")
scripts_data = {}
# Use _v_children to get node names safely
if hasattr(scripts_group, '_v_children'):
for node_name, node in scripts_group._v_children.items():
try:
if hasattr(node, 'read'):
data = node.read()
# Handle numpy string arrays and other types
if hasattr(data, 'decode'):
# bytes or numpy bytes
scripts_data[node_name] = data.decode('utf-8', errors='replace')
elif hasattr(data, 'tolist'):
# numpy array
scripts_data[node_name] = str(data.tolist())
elif isinstance(data, (str, bytes)):
# string or bytes
if isinstance(data, bytes):
scripts_data[node_name] = data.decode('utf-8', errors='replace')
else:
scripts_data[node_name] = data
else:
scripts_data[node_name] = str(data)
except Exception as e:
print(f"Warning: Could not read script {node_name}: {e}")
metadata['scripts'] = scripts_data
# Load log
if "/log" in hdf_file:
try:
log_node = hdf_file.get_node("/log")
metadata['log'] = log_node.read()
except Exception as e:
print(f"Warning: Could not read log: {e}")
# Load timeline
if "/timeline" in hdf_file:
try:
timeline_node = hdf_file.get_node("/timeline")
metadata['timeline'] = timeline_node.read()
except Exception as e:
print(f"Warning: Could not read timeline: {e}")
return data_pool, metadata
except Exception as e:
raise Exception(f"Failed to load HDF5 file {filename}: {e}")
finally:
if 'hdf_file' in locals():
hdf_file.close()
def register_listener(self, listening_function): def register_listener(self, listening_function):
self.__registered_listeners.append(listening_function) self.__registered_listeners.append(listening_function)
+2 -4
View File
@@ -96,12 +96,10 @@ class Experiment:
#for tracking the experiment length: #for tracking the experiment length:
#because loops are possible we need to track the length for each loop level #because loops are possible we need to track the length for each loop level
self.total_time=[] self.total_time=[0.0]
self.total_time.append(0.0)
#and we need to know the number of iterations of the loops to multiply the state. #and we need to know the number of iterations of the loops to multiply the state.
self.loop_iterations=[] self.loop_iterations=[1]
self.loop_iterations.append(1)
# Commands ------------------------------------------------------------------------------------- # Commands -------------------------------------------------------------------------------------
+267 -57
View File
@@ -169,7 +169,7 @@ class DamarisGUI:
self.log = LogWindow( self.xml_gui , self) self.log = LogWindow( self.xml_gui , self)
self.sw = ScriptWidgets( self.xml_gui ) self.sw = ScriptWidgets( self.xml_gui, self )
self.toolbar_init( ) self.toolbar_init( )
@@ -218,6 +218,7 @@ class DamarisGUI:
"on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event,
"on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event,
"on_toolbar_open_file_button_clicked": self.sw.open_file, "on_toolbar_open_file_button_clicked": self.sw.open_file,
"on_toolbar_open_hdf5_button_clicked": self.sw.open_hdf5,
"on_toolbar_new_button_clicked": self.sw.new_file, "on_toolbar_new_button_clicked": self.sw.new_file,
"on_toolbar_save_as_button_clicked": self.sw.save_file_as, "on_toolbar_save_as_button_clicked": self.sw.save_file_as,
"on_toolbar_save_file_button_clicked": self.sw.save_file, "on_toolbar_save_file_button_clicked": self.sw.save_file,
@@ -297,6 +298,14 @@ class DamarisGUI:
self.config = None self.config = None
self.xml_gui = None self.xml_gui = None
def observe_data_pool( self, data_pool ):
"""
Delegate to monitor widgets
"""
self.data = data_pool
if hasattr(self, "monitor"):
self.monitor.observe_data_pool(data_pool)
# event handling: the real acitons in gui programming # event handling: the real acitons in gui programming
# first global events # first global events
@@ -471,9 +480,10 @@ class DamarisGUI:
self.id = self.lockfile.add_experiment() self.id = self.lockfile.add_experiment()
while not self.lockfile.am_i_next(): while not self.lockfile.am_i_next():
self.stop_experiment_flag.wait(interval)
while gtk.events_pending(): while gtk.events_pending():
gtk.main_iteration_do(False) gtk.main_iteration_do(False)
if self.stop_experiment_flag.wait(interval):
break
if loop_run > 1: if loop_run > 1:
self.experiment_script_statusbar_label.set_text("Waiting for other experiment to finish") self.experiment_script_statusbar_label.set_text("Waiting for other experiment to finish")
loop_run = 0 loop_run = 0
@@ -524,7 +534,10 @@ class DamarisGUI:
print("waiting for threads stoping...", end=' ') print("waiting for threads stoping...", end=' ')
still_running = [x for x in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if x is not None and x.is_alive( )] still_running = [x for x in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if x is not None and x.is_alive( )]
for t in still_running: for t in still_running:
t.join( ) while t.is_alive():
while gtk.events_pending():
gtk.main_iteration_do(False)
t.join( 0.05 )
print("done") print("done")
# cleanup # cleanup
@@ -1099,11 +1112,12 @@ class LogWindow:
class ScriptWidgets: class ScriptWidgets:
def __init__( self, xml_gui ): def __init__( self, xml_gui, outer_space ):
""" """
initialize text widgets with text initialize text widgets with text
""" """
self.xml_gui = xml_gui self.xml_gui = xml_gui
self.outer_space = outer_space
# my states # my states
# editing enabled/disabled # editing enabled/disabled
@@ -1186,7 +1200,6 @@ get_ylabel set_ylabel"""
compexp.add_provider(self.compwexp) compexp.add_provider(self.compwexp)
compres.add_provider(self.compwres) compres.add_provider(self.compwres)
# script fonts are atlered by configuration
# clipboard # clipboard
self.main_clipboard = gtk.Clipboard( ) self.main_clipboard = gtk.Clipboard( )
@@ -1194,13 +1207,12 @@ get_ylabel set_ylabel"""
self.experiment_script_statusbar_label = self.xml_gui.get_object( "statusbar_experiment_script_label" ) self.experiment_script_statusbar_label = self.xml_gui.get_object( "statusbar_experiment_script_label" )
self.data_handling_statusbar_label = self.xml_gui.get_object( "statusbar_data_handling_label" ) self.data_handling_statusbar_label = self.xml_gui.get_object( "statusbar_data_handling_label" )
# footline with line and col indicators # footer with line and col indicators
self.experiment_script_line_indicator = self.xml_gui.get_object( "experiment_script_line_textfield" ) self.experiment_script_line_indicator = self.xml_gui.get_object( "experiment_script_line_textfield" )
self.experiment_script_column_indicator = self.xml_gui.get_object( "experiment_script_column_textfield" ) self.experiment_script_column_indicator = self.xml_gui.get_object( "experiment_script_column_textfield" )
self.data_handling_line_indicator = self.xml_gui.get_object( "data_handling_line_textfield" ) self.data_handling_line_indicator = self.xml_gui.get_object( "data_handling_line_textfield" )
self.data_handling_column_indicator = self.xml_gui.get_object( "data_handling_column_textfield" ) self.data_handling_column_indicator = self.xml_gui.get_object( "data_handling_column_textfield" )
# some event handlers # some event handlers
self.experiment_script_textbuffer.connect( "modified-changed", self.textviews_modified ) self.experiment_script_textbuffer.connect( "modified-changed", self.textviews_modified )
self.experiment_script_textview.connect_after( "move-cursor", self.textviews_moved ) self.experiment_script_textview.connect_after( "move-cursor", self.textviews_moved )
@@ -1211,8 +1223,7 @@ get_ylabel set_ylabel"""
self.data_handling_textview.connect( "key-press-event", self.textviews_keypress ) self.data_handling_textview.connect( "key-press-event", self.textviews_keypress )
self.data_handling_textview.connect( "button-release-event", self.textviews_clicked ) self.data_handling_textview.connect( "button-release-event", self.textviews_clicked )
# and the editing toolbar part # and the editing toolbar buttons
# buttons
self.toolbar_new_button = self.xml_gui.get_object( "toolbar_new_button" ) self.toolbar_new_button = self.xml_gui.get_object( "toolbar_new_button" )
self.toolbar_open_button = self.xml_gui.get_object( "toolbar_open_file_button" ) self.toolbar_open_button = self.xml_gui.get_object( "toolbar_open_file_button" )
self.toolbar_save_button = self.xml_gui.get_object( "toolbar_save_file_button" ) self.toolbar_save_button = self.xml_gui.get_object( "toolbar_save_file_button" )
@@ -1223,8 +1234,9 @@ get_ylabel set_ylabel"""
self.toolbar_redo_button = self.xml_gui.get_object( "toolbar_redo_button" ) self.toolbar_redo_button = self.xml_gui.get_object( "toolbar_redo_button" )
self.toolbar_search_button = self.xml_gui.get_object("toolbar_search_button") self.toolbar_search_button = self.xml_gui.get_object("toolbar_search_button")
# my notebook # main notebook
self.main_notebook = self.xml_gui.get_object( "main_notebook" ) self.main_notebook = self.xml_gui.get_object( "main_notebook" )
# config toolbar # config toolbar
self.main_notebook.connect_after( "switch_page", self.notebook_page_switched ) self.main_notebook.connect_after( "switch_page", self.notebook_page_switched )
@@ -1570,7 +1582,7 @@ get_ylabel set_ylabel"""
elif response == gtk.ResponseType.CANCEL: elif response == gtk.ResponseType.CANCEL:
return 0 return 0
def response( self, response_id, script_widget ): def response( dialog, response_id, script_widget ):
if response_id == gtk.ResponseType.OK: if response_id == gtk.ResponseType.OK:
file_name = dialog.get_filename( ) file_name = dialog.get_filename( )
if file_name is None: if file_name is None:
@@ -1578,7 +1590,13 @@ get_ylabel set_ylabel"""
script_filename = os.path.abspath( file_name ) script_filename = os.path.abspath( file_name )
if not os.access( script_filename, os.R_OK ): if not os.access( script_filename, os.R_OK ):
self.outer_space.show_error_dialog( "File I/O Error", "Cannot read from file %s" % script_filename ) error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
error_dialog.set_markup("Cannot read from file %s" % script_filename)
error_dialog.run()
error_dialog.destroy()
return True return True
script_string = script_widget.load_file_as_unicode( script_filename ) script_string = script_widget.load_file_as_unicode( script_filename )
@@ -1611,6 +1629,103 @@ get_ylabel set_ylabel"""
return True return True
def open_hdf5( self, widget, Data=None ):
"""
Open and load an HDF5 file written by DAMARIS.
"""
# ignore
if not self.editing_state:
return 0
parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title="Open HDF5 File...",
parent=parent_window,
action=gtk.FileChooserAction.OPEN,
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
)
dialog.set_default_response( gtk.ResponseType.OK )
dialog.set_select_multiple( False )
# Add filter for HDF5 files
filter_hdf5 = gtk.FileFilter()
filter_hdf5.set_name("HDF5 files")
filter_hdf5.add_pattern("*.h5")
filter_hdf5.add_pattern("*.hdf5")
filter_hdf5.add_pattern("*.HDF5")
filter_hdf5.add_pattern("*.H5")
dialog.add_filter(filter_hdf5)
# Also allow all files
filter_all = gtk.FileFilter()
filter_all.set_name("All files")
filter_all.add_pattern("*")
dialog.add_filter(filter_all)
def response( dialog, response_id, script_widget ):
if response_id == gtk.ResponseType.OK:
file_name = dialog.get_filename( )
if file_name is None:
return
file_name = os.path.abspath( file_name )
if not os.access( file_name, os.R_OK ):
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
error_dialog.set_markup("Cannot read from file %s" % file_name)
error_dialog.run()
error_dialog.destroy()
return True
try:
# Load DataPool and metadata from HDF5
data_pool, metadata = DataPool.load_hdf5(file_name)
# Set the loaded data pool in the outer space (main application)
if hasattr(script_widget.outer_space, 'observe_data_pool'):
script_widget.outer_space.observe_data_pool(data_pool)
elif hasattr(script_widget.outer_space, 'monitor'):
# Fallback if delegate not present
script_widget.outer_space.data = data_pool
script_widget.outer_space.monitor.observe_data_pool(data_pool)
# If there are scripts in metadata, load them
if 'scripts' in metadata:
scripts = metadata['scripts']
exp_script = scripts.get('experiment_script', None)
res_script = scripts.get('result_script', None)
if exp_script or res_script:
script_widget.set_scripts(exp_script, res_script)
# Show success message
success_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.INFO,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
success_dialog.set_markup("Successfully loaded data from %s" % os.path.basename(file_name))
success_dialog.run()
success_dialog.destroy()
except Exception as e:
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
type=gtk.MessageType.ERROR,
flags=gtk.DialogFlags.MODAL,
buttons=("_OK", gtk.ResponseType.OK))
error_dialog.set_markup("Failed to load HDF5 file: %s" % str(e))
error_dialog.run()
error_dialog.destroy()
return True
# Event-Handler for response-signal (when one of the button is pressed)
dialog.connect( "response", response, self )
dialog.run( )
dialog.destroy( )
return True
def save_file( self, widget=None, Data=None ): def save_file( self, widget=None, Data=None ):
""" """
save file to associated filename save file to associated filename
@@ -1798,9 +1913,9 @@ get_ylabel set_ylabel"""
try: try:
if response_id == -1: if response_id == -1:
match_start, match_end = startiter.backward_search(searchstring, gtk.TEXT_SEARCH_TEXT_ONLY) match_start, match_end = startiter.backward_search(searchstring, gtk.TextSearchFlags.TEXT_ONLY)
elif response_id == 1: elif response_id == 1:
match_start, match_end = enditer.forward_search(searchstring, gtk.TEXT_SEARCH_TEXT_ONLY) match_start, match_end = enditer.forward_search(searchstring, gtk.TextSearchFlags.TEXT_ONLY)
except: except:
pass pass
@@ -1857,7 +1972,7 @@ get_ylabel set_ylabel"""
dialog.connect("response", response, self, textbox) dialog.connect("response", response, self, textbox)
dialog.set_resizable(False) dialog.set_resizable(False)
dialog.show() dialog.show_all()
if startit is not None and endit is not None: if startit is not None and endit is not None:
if current_page == 0: if current_page == 0:
@@ -1951,6 +2066,20 @@ class ConfigTab:
self.config_info_textview = self.xml_gui.get_object( "info_textview" ) self.config_info_textview = self.xml_gui.get_object( "info_textview" )
self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" ) self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" )
# ADC bit depth setting
self.config_adc_bit_depth_hbox = gtk.HBox(homogeneous=False, spacing=6)
self.config_adc_bit_depth_label = gtk.Label(label="ADC bit depth:")
self.config_adc_bit_depth_spinbutton = gtk.SpinButton.new_with_range(8, 32, 1)
self.config_adc_bit_depth_spinbutton.set_value(ADC_Result.default_bit_depth)
self.config_adc_bit_depth_spinbutton.set_tooltip_text("Set the default bit depth for clipping detection (e.g., 14 for 14-bit ADCs).")
self.config_adc_bit_depth_spinbutton.connect("value-changed", self.on_adc_bit_depth_changed)
self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_label, False, False, 0)
self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_spinbutton, False, False, 0)
backend_vbox = self.xml_gui.get_object("vbox4")
if backend_vbox:
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
self.config_adc_bit_depth_hbox.show_all()
# insert version informations # insert version informations
components_text = """ components_text = """
operating system %(os)s operating system %(os)s
@@ -2044,7 +2173,7 @@ pygobject version %(pygobject)s
""" """
complib_iter = self.config_data_pool_complib.get_active_iter( ) complib_iter = self.config_data_pool_complib.get_active_iter( )
complib = self.config_data_pool_complib.get_model( ).get_value( complib_iter, 0 ) complib = self.config_data_pool_complib.get_model( ).get_value( complib_iter, 0 )
return { actual_config = {
"start_backend": self.config_start_backend_checkbutton.get_active( ), "start_backend": self.config_start_backend_checkbutton.get_active( ),
"start_result_script": self.config_start_result_script_checkbutton.get_active( ), "start_result_script": self.config_start_result_script_checkbutton.get_active( ),
"start_experiment_script": self.config_start_experiment_script_checkbutton.get_active( ), "start_experiment_script": self.config_start_experiment_script_checkbutton.get_active( ),
@@ -2056,8 +2185,11 @@ pygobject version %(pygobject)s
"data_pool_write_interval": self.config_data_pool_write_interval_entry.get_text( ), "data_pool_write_interval": self.config_data_pool_write_interval_entry.get_text( ),
"data_pool_complib": complib, "data_pool_complib": complib,
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ), "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": self.config_adc_bit_depth_spinbutton.get_value_as_int()
} }
ADC_Result.default_bit_depth = actual_config["adc_bit_depth"]
return actual_config
def set( self, config ): def set( self, config ):
if "start_backend" in config: if "start_backend" in config:
@@ -2083,6 +2215,12 @@ pygobject version %(pygobject)s
if "script_font" in config: if "script_font" in config:
self.config_script_font_button.set_font_name( config[ "script_font" ] ) self.config_script_font_button.set_font_name( config[ "script_font" ] )
self.set_script_font_handler( None ) self.set_script_font_handler( None )
if "adc_bit_depth" in config:
try:
self.config_adc_bit_depth_spinbutton.set_value(float(config["adc_bit_depth"]))
ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
except (ValueError, TypeError):
pass
if "data_pool_complib" in config: if "data_pool_complib" in config:
# find combo-box entry and make it active... # find combo-box entry and make it active...
model = self.config_data_pool_complib.get_model( ) model = self.config_data_pool_complib.get_model( )
@@ -2096,6 +2234,9 @@ pygobject version %(pygobject)s
if iter is None: if iter is None:
print("compression method %s is not supported" % config[ "data_pool_complib" ]) print("compression method %s is not supported" % config[ "data_pool_complib" ])
def on_adc_bit_depth_changed(self, widget):
ADC_Result.default_bit_depth = widget.get_value_as_int()
def load_config_handler( self, widget ): def load_config_handler( self, widget ):
if self.system_default_filename: if self.system_default_filename:
self.load_config( self.system_default_filename ) self.load_config( self.system_default_filename )
@@ -2270,6 +2411,13 @@ class MonitorWidgets:
self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" ) self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" )
self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" ) self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" )
self.display_descriptions_checkbutton = gtk.CheckButton(label="Show Desc.")
self.display_descriptions_checkbutton.set_active(True)
self.display_descriptions_checkbutton.show()
self.display_settings_frame.set_property("n-columns", 5)
self.display_settings_frame.attach(self.display_descriptions_checkbutton, 4, 5, 0, 1, gtk.AttachOptions.FILL, 0, 0, 0)
self.display_descriptions_checkbutton.connect("toggled", self.display_descriptions_toggled)
# Matplot (Display_Table, 1st Row) -------------------------------------------------------- # Matplot (Display_Table, 1st Row) --------------------------------------------------------
# create new plot # create new plot
@@ -2363,6 +2511,8 @@ class MonitorWidgets:
self.__rescale = True self.__rescale = True
self.update_counter = 0 self.update_counter = 0
self.update_counter_lock = threading.Lock( ) self.update_counter_lock = threading.Lock( )
self.description_text = None
self.clipping_marker = None
def source_list_reset( self ): def source_list_reset( self ):
self.display_source_treestore.clear( ) self.display_source_treestore.clear( )
@@ -2374,7 +2524,7 @@ class MonitorWidgets:
def source_list_find_one( self, model, iter, what ): def source_list_find_one( self, model, iter, what ):
"""find node in subcategory""" """find node in subcategory"""
while iter is not None: while iter is not None:
if model.get( iter, 0 )[ 0 ] == what: if str( model.get( iter, 0 )[ 0 ] ) == what:
return iter return iter
iter = model.iter_next( iter ) iter = model.iter_next( iter )
return iter return iter
@@ -2445,31 +2595,50 @@ class MonitorWidgets:
register a listener and save reference to data register a listener and save reference to data
assume to be in gtk/gdk lock assume to be in gtk/gdk lock
""" """
if self.data_pool is not None: gdk.threads_enter( )
# maybe some extra cleanup needed try:
print("ToDo: cleanup widgets") if self.data_pool is not None:
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ): # maybe some extra cleanup needed
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener ) print("ToDo: cleanup widgets")
self.displayed_data[ 1 ] = None if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
self.displayed_data = [ None, None ] self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
self.data_pool.unregister_listener( self.datapool_listener ) self.displayed_data[ 1 ] = None
self.data_pool = None self.displayed_data = [ None, None ]
self.data_pool.unregister_listener( self.datapool_listener )
self.data_pool = None
self.update_counter_lock.acquire( )
self.update_counter = 0
self.update_counter_lock.release( )
# display states
self.source_list_reset( ) self.source_list_reset( )
self.update_counter_lock.acquire( ) self.__rescale = True
self.update_counter = 0 self.displayed_data = [ None, None ]
self.update_counter_lock.release( ) self.display_source_path_label.set_label( "" )
self.clear_display( )
# display states if data_pool is not None:
self.__rescale = True # keep track of data
self.displayed_data = [ None, None ] self.data_pool = data_pool
self.display_source_path_label.set_label( "" ) self.data_pool.register_listener( self.datapool_listener )
self.clear_display( )
# Populate source list with existing data since the listener
if data_pool is not None: # was registered after the data was loaded
# keep track of data for key in sorted(data_pool.keys()):
self.data_pool = data_pool if key != "__builtins__": # Skip internal keys
self.data_pool.register_listener( self.datapool_listener ) self.source_list_add(key)
# Select first data item if available
keys = [k for k in data_pool.keys() if not k.startswith("__")]
if keys:
keys.sort()
first_key = keys[0]
first_iter = self.source_list_find(first_key.split("/"))
if first_iter:
self.display_source_combobox.set_active_iter(first_iter)
finally:
gdk.threads_leave( )
#################### observing data structures and produce idle events #################### observing data structures and produce idle events
@@ -2606,7 +2775,7 @@ class MonitorWidgets:
gdk.threads_leave( ) gdk.threads_leave( )
elif event.what == DataPool.Event.destroy: elif event.what == DataPool.Event.destroy:
gdk.threads_enter( ) gdk.threads_enter( )
self.source_list_reset( 'None' ) self.source_list_reset( )
self.displayed_data = [ None, None ] self.displayed_data = [ None, None ]
self.clear_display( ) self.clear_display( )
gdk.threads_leave( ) gdk.threads_leave( )
@@ -2685,6 +2854,10 @@ class MonitorWidgets:
if self.displayed_data[ 0 ] is not None: if self.displayed_data[ 0 ] is not None:
self.update_display( self.displayed_data[ 0 ][ : ] ) self.update_display( self.displayed_data[ 0 ][ : ] )
def display_descriptions_toggled( self, widget, data=None ):
if self.displayed_data[ 0 ] is not None:
self.update_display( self.displayed_data[ 0 ][ : ] )
def save_display_data_as_text( self, widget, data=None ): def save_display_data_as_text( self, widget, data=None ):
""" """
copy data to tmp file and show save dialog copy data to tmp file and show save dialog
@@ -2935,6 +3108,18 @@ class MonitorWidgets:
self.graphen = [ ] self.graphen = [ ]
self.matplot_axes.clear( ) self.matplot_axes.clear( )
self.matplot_axes.grid( True ) self.matplot_axes.grid( True )
if self.description_text is not None:
try:
self.description_text.remove()
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( ) self.matplot_canvas.draw_idle( )
def update_display( self, subject=None ): def update_display( self, subject=None ):
@@ -3093,25 +3278,48 @@ class MonitorWidgets:
self.matplot_axes.set_ylabel( "" ) self.matplot_axes.set_ylabel( "" )
# Any variables to be drawn on plot # Any variables to be drawn on plot
if False: description_lines = []
descriptions = None if self.display_descriptions_checkbutton.get_active():
if isinstance(in_result, Accumulation): if isinstance(in_result, Accumulation):
descriptions = in_result.common_descriptions description_lines.append("n = %d" % in_result.n)
if in_result.common_descriptions is not None:
description_lines.extend(["%s = %s" % (key, in_result.common_descriptions[key]) for key in in_result.common_descriptions.keys()])
elif isinstance(in_result, ADC_Result): elif isinstance(in_result, ADC_Result):
descriptions = in_result.description if hasattr(in_result, "job_id") and in_result.job_id is not None:
description_lines.append("job_id = %s" % str(in_result.job_id))
if hasattr(in_result, "description") and in_result.description is not None:
description_lines.extend(["%s = %s" % (key, in_result.description[key]) for key in in_result.description.keys()])
if description_lines:
description_string = "\n".join(description_lines)
if self.description_text is None:
self.description_text = self.matplot_axes.text(0.98, 0.95, description_string,
size=10,
transform=self.matplot_axes.transAxes,
va='top', ha='right', ma='left',
bbox=dict(facecolor='lightgray', alpha=0.5, boxstyle='round,pad=0.2'))
else: else:
pass self.description_text.set_text(description_string)
#actual_config = self.config.get() elif self.description_text is not None:
if (descriptions is not None): try:
#print(actual_config['pretty_descriptions']) self.description_text.remove()
description_string = "" except:
for key in descriptions.keys(): pass
description_string += "%s = %s\n" % (key,descriptions[key]) self.description_text = None
self.matplot_axes.text(0.8,0.95, description_string[:-1],
size=10, if getattr(in_result, "is_clipped", False):
transform=self.matplot_axes.transAxes, if self.clipping_marker is None:
va='top', self.clipping_marker = self.matplot_axes.text(0.5, 0.95, "ADC OVERFLOW",
backgroundcolor='white') 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! # Draw it!
self.matplot_canvas.draw_idle( ) self.matplot_canvas.draw_idle( )
@@ -3287,6 +3495,8 @@ class ScriptInterface:
self.back_driver.start( ) self.back_driver.start( )
while (not self.back_driver.quit_flag.isSet( ) and \ while (not self.back_driver.quit_flag.isSet( ) and \
(self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)): (self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)):
while gtk.events_pending():
gtk.main_iteration_do(False)
self.back_driver.quit_flag.wait( 0.1 ) self.back_driver.quit_flag.wait( 0.1 )
if self.exp_handling: if self.exp_handling:
self.exp_handling.start( ) self.exp_handling.start( )
+27
View File
@@ -3,6 +3,7 @@ import io
import traceback import traceback
import sys import sys
import time import time
import ast
from damaris.experiments.Experiment import Quit from damaris.experiments.Experiment import Quit
from damaris.experiments import Experiment from damaris.experiments import Experiment
@@ -48,8 +49,32 @@ class ExperimentHandling(threading.Thread):
self.raised_exception = None self.raised_exception = None
self.location = None self.location = None
exp_iterator=None exp_iterator=None
# check for time.sleep()
try:
tree = ast.parse(self.script)
for node in ast.walk(tree):
found_blocking_sleep = False
if isinstance(node, ast.Call):
func = node.func
if isinstance(func, ast.Attribute) and func.attr == 'sleep':
if isinstance(func.value, ast.Name) and func.value.id == 'time':
found_blocking_sleep = True
elif isinstance(node, ast.ImportFrom) and node.module == 'time':
for alias in node.names:
if alias.name == 'sleep':
found_blocking_sleep = True
if found_blocking_sleep:
print("Warning: time.sleep() detected in experiment script. This is blocking and not interruptible. Please use sleep() instead.")
break
except Exception:
pass
try: try:
exec(self.script, dataspace) exec(self.script, dataspace)
except StopExperiment:
return
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -61,6 +86,8 @@ class ExperimentHandling(threading.Thread):
if "experiment" in dataspace: if "experiment" in dataspace:
try: try:
exp_iterator=dataspace["experiment"]() exp_iterator=dataspace["experiment"]()
except StopExperiment:
return
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
+34
View File
@@ -4,7 +4,9 @@ import sys
import os import os
import os.path import os.path
import traceback import traceback
import ast
from damaris.data import Resultable from damaris.data import Resultable
from damaris.gui.ExperimentHandling import StopExperiment
class ResultHandling(threading.Thread): class ResultHandling(threading.Thread):
""" """
@@ -30,10 +32,35 @@ class ResultHandling(threading.Thread):
del data_classes del data_classes
dataspace["results"]=self dataspace["results"]=self
dataspace["data"]=self.data_space dataspace["data"]=self.data_space
dataspace["sleep"]=self.sleep
self.raised_exception=None self.raised_exception=None
self.location = None self.location = None
# check for time.sleep()
try:
tree = ast.parse(self.script)
for node in ast.walk(tree):
found_blocking_sleep = False
if isinstance(node, ast.Call):
func = node.func
if isinstance(func, ast.Attribute) and func.attr == 'sleep':
if isinstance(func.value, ast.Name) and func.value.id == 'time':
found_blocking_sleep = True
elif isinstance(node, ast.ImportFrom) and node.module == 'time':
for alias in node.names:
if alias.name == 'sleep':
found_blocking_sleep = True
if found_blocking_sleep:
print("Warning: time.sleep() detected in result script. This is blocking and not interruptible. Please use sleep() instead.")
break
except Exception:
pass
try: try:
exec(self.script, dataspace) exec(self.script, dataspace)
except StopExperiment:
return
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -47,6 +74,8 @@ class ResultHandling(threading.Thread):
return return
try: try:
dataspace["result"]() dataspace["result"]()
except StopExperiment:
pass
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -56,6 +85,11 @@ class ResultHandling(threading.Thread):
traceback_file=None traceback_file=None
dataspace=None dataspace=None
def sleep(self, seconds):
self.quit_flag.wait(seconds)
if self.quit_flag.isSet():
raise StopExperiment
def __iter__(self): def __iter__(self):
if self.quit_flag.isSet(): if self.quit_flag.isSet():
self.results=None self.results=None
+2
View File
@@ -239,6 +239,7 @@ class ResultReader:
self.result.index = [(0,tmp_size-1)] self.result.index = [(0,tmp_size-1)]
tmp_index += tmp_size tmp_index += tmp_size
self.result.cont_data=True self.result.cont_data=True
self.result.check_clipping()
tmp_part = None tmp_part = None
def __parseFile_expat(self, in_file): 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)) self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1))
tmp_sample_counter+=tmp_size tmp_sample_counter+=tmp_size
self.result.cont_data=True self.result.cont_data=True
self.result.check_clipping()
# Callback when a xml start tag is found # Callback when a xml start tag is found
def __xmlStartTagFound(self, in_name, in_attribute): def __xmlStartTagFound(self, in_name, in_attribute):
+15
View File
@@ -491,6 +491,21 @@ Public License instead of this License.
<property name="homogeneous">True</property> <property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkToolButton" id="toolbar_open_hdf5_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Open DAMARIS HDF5 file</property>
<property name="label">Open HDF5</property>
<property name="use-underline">True</property>
<property name="icon-name">document-open</property>
<signal name="clicked" handler="on_toolbar_open_hdf5_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child> <child>
<object class="GtkToolButton" id="toolbar_save_file_button"> <object class="GtkToolButton" id="toolbar_save_file_button">
<property name="visible">True</property> <property name="visible">True</property>
+1
View File
@@ -1,4 +1,5 @@
import tables import tables
import numpy
r = MeasurementResult("test") r = MeasurementResult("test")
+1 -1
View File
@@ -105,7 +105,7 @@ class TestAccumulation(unittest.TestCase):
self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30]))) self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30])))
self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35]))) self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35])))
self.assertEqual(sub_result.index, [(0, 1)]) self.assertEqual(sub_result.index, [(0, 1)])
# TODO Fix #13: self.assertEqual(sub_result.common_descriptions, {"key": "value"}) self.assertEqual(sub_result.common_descriptions, {"key": "value"})
def test_write_to_csv_without_error(self): def test_write_to_csv_without_error(self):
"""Test the functionality of writing to CSV.""" """Test the functionality of writing to CSV."""