fix return-value/deprecated API bugs, accumulation lock leaks, code cleanup
Build Debian Packages / build (bookworm, debian12) (push) Successful in 8m56s
Build Debian Packages / build (trixie, debian13) (push) Successful in 9m3s

- raise instead of silent None for unsupported operands and empty negation
- validate before acquiring lock in Accumulation operators (no RLock leak)
- Accumulation._copy_provenance helper, ADC_Result._copy_metadata for all result paths
- shared _sandbox_stdout_redirect contextmanager for sandboxed scripts
- TemperatureMonitor dead code removal (gtk helper, traceback, unused properties)
- new unit tests incl. lock-freedom regression test
- gitignore sphinx generated doc/_autosummary/ and doc/damaris/
This commit is contained in:
markusro committed 2026-10-17 19:17:43 +02:00
1 parent 91ade4376c
commit f9d8de7697
26 files changed
+587 -233

No files matched your search

+2
View File
@@ -14,6 +14,8 @@ debian/.debhelper
# Sphinx documentation # Sphinx documentation
doc/_build/ doc/_build/
doc/_autosummary/
doc/damaris/
# Debian build artifacts # Debian build artifacts
debian/*.debhelper.log debian/*.debhelper.log
+15
View File
@@ -26,6 +26,7 @@ The data module contains result types, data handling, and signal processing clas
damaris.data.ADC_Result damaris.data.ADC_Result
damaris.data.Accumulation damaris.data.Accumulation
damaris.data.Config_Result
damaris.data.DamarisFFT damaris.data.DamarisFFT
damaris.data.DataPool damaris.data.DataPool
damaris.data.Drawable damaris.data.Drawable
@@ -71,6 +72,20 @@ The GUI module contains the graphical interface, backend driver, and experiment/
damaris.gui.TemperatureMonitor damaris.gui.TemperatureMonitor
damaris.gui.logging_utils damaris.gui.logging_utils
A short usage example for the font helper of the GUI module (the non-deprecated
replacement for the removed ``Widget.modify_font()``):
.. code-block:: python
from damaris.gui.DamarisGUI import apply_font_css
apply_font_css(textview, "Monospace 10") # pango style font description
apply_font_css(label, "sans bold 16")
The font string is converted into a CSS rule (``font-family``/``font-size``/
``font-weight``) which is applied to the widget's style context with a
``Gtk.CssProvider``.
Tools Module Tools Module
============ ============
+10
View File
@@ -313,6 +313,10 @@ The change is *in-place*, which means if the timesignal is needed later on, a co
- **clip(start=**\ *None* **, stop=**\ *None* **)** - **clip(start=**\ *None* **, stop=**\ *None* **)**
Only the data between start and stop is returned. Only the data between start and stop is returned.
The start and stop parameters can be either in time or frequency domain. The start and stop parameters can be either in time or frequency domain.
Calling **clip()** without any argument is a placeholder that leaves the
object unchanged (useful in scripted processing chains). If only one bound
is given, the missing one defaults to the first resp. last data point and
reversed bounds (start > stop) are swapped automatically.
- **baseline(lastpart=**\ *0.1* **)** - **baseline(lastpart=**\ *0.1* **)**
This method should be applied first as it corrects the baseline of the signal, taking the last part of the signal for determining the correction. This method should be applied first as it corrects the baseline of the signal, taking the last part of the signal for determining the correction.
@@ -374,6 +378,12 @@ To use this methods one has to apply consecutively the methods to an *timesignal
This applies baseline correction using the last 40% of the timesignal for determination of the signal. Then, an exponential window is applied. From this timesignal an FFT is calculated which is further clipped to view to the data between -1 kHz with 1 kHz width. This applies baseline correction using the last 40% of the timesignal for determination of the signal. Then, an exponential window is applied. From this timesignal an FFT is calculated which is further clipped to view to the data between -1 kHz with 1 kHz width.
Operators and Copies of Data Objects
====================================
The arithmetic operators (``+``, ``-``, ``*``, ``/``, ``//``, ``**``, unary ``-``) of **ADC_Result** and **Accumulation** objects return *new* objects: the operands are never modified and the data arrays of the result are independent copies, so later in-place operations (``+=``, ``*=``, ...) on the result cannot corrupt the operands. The channel metadata is carried over to the result: an **ADC_Result** result keeps the same ``get_nChannels()`` and ``is_clipped`` flag as its operand, and an **Accumulation** result keeps (and merges) the ``job_ids`` provenance of both operands. Adding an unsupported type or negating an empty accumulation raises a ``ValueError`` instead of silently returning *None*. In the same spirit, ``Config_Result(config=..., desc=...)`` stores the given dictionaries in the constructor, and ``measurementresult + 0`` returns an independent deep copy of the result object.
Good Practices Good Practices
============== ==============
+18 -4
View File
@@ -232,8 +232,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
job_id=self.job_id, job_id=self.job_id,
job_date=self.job_date, job_date=self.job_date,
) )
r.nChannels = self.nChannels self._copy_metadata(r)
r.is_clipped = self.is_clipped
finally: finally:
self.lock.release() self.lock.release()
@@ -277,8 +276,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
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.copy() if self.description is not None else None, 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)
r.nChannels = self.nChannels self._copy_metadata(r)
r.is_clipped = self.is_clipped
self.lock.release() self.lock.release()
return r return r
@@ -488,6 +486,11 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
return tmp_string return tmp_string
def _copy_metadata(self, r):
"Copy channel metadata (nChannels, is_clipped) from self to the new result r"
r.nChannels = self.nChannels
r.is_clipped = self.is_clipped
def __add__(self, other): def __add__(self, other):
"Redefining self + other (scalar)" "Redefining self + other (scalar)"
if isinstance(other, int) or isinstance(other, float): if isinstance(other, int) or isinstance(other, float):
@@ -498,6 +501,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") + other) tmp_y.append(numpy.array(self.y[i], dtype="float32") + other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -520,6 +524,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") - other) tmp_y.append(numpy.array(self.y[i], dtype="float32") - other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -537,6 +542,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(other - numpy.array(self.y[i], dtype="float32")) tmp_y.append(other - numpy.array(self.y[i], dtype="float32"))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -554,6 +560,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") * other) tmp_y.append(numpy.array(self.y[i], dtype="float32") * other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -575,6 +582,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") ** other) tmp_y.append(numpy.array(self.y[i], dtype="float32") ** other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -589,6 +597,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(self.y[i], dtype="float32") / other) tmp_y.append(numpy.array(self.y[i], dtype="float32") / other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -602,6 +611,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(other / numpy.array(self.y[i], dtype="float32")) tmp_y.append(other / numpy.array(self.y[i], dtype="float32"))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -618,6 +628,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(numpy.array(self.y[i], dtype="float32") // other) tmp_y.append(numpy.array(self.y[i], dtype="float32") // other)
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -634,6 +645,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(other // numpy.array(self.y[i], dtype="float32")) tmp_y.append(other // numpy.array(self.y[i], dtype="float32"))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
else: else:
@@ -649,6 +661,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(numpy.array(-self.y[i])) tmp_y.append(numpy.array(-self.y[i]))
r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = self.x[:], y = tmp_y, index = self.index[:], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
self._copy_metadata(r)
self.lock.release() self.lock.release()
return r return r
@@ -703,6 +716,7 @@ def read_from_hdf(hdf_node):
adc.cont_data=True adc.cont_data=True
# now do the real data # now do the real data
adc_data=hdf_node.adc_data.read() adc_data=hdf_node.adc_data.read()
adc.set_nChannels(adc_data.shape[1])
adc.x=numpy.arange(adc_data.shape[0], dtype="float32")/adc.sampling_rate adc.x=numpy.arange(adc_data.shape[0], dtype="float32")/adc.sampling_rate
+36 -26
View File
@@ -102,6 +102,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
else: else:
raise ValueError("Wrong usage of __init__!") raise ValueError("Wrong usage of __init__!")
def _copy_provenance(self, r):
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
if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy()
def get_accu_by_index(self, index: int): def get_accu_by_index(self, index: int):
self.lock.acquire() self.lock.acquire()
try: try:
@@ -637,12 +645,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 self._copy_provenance(r)
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
if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy()
self.lock.release() self.lock.release()
return r return r
@@ -678,8 +681,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Other and self not empty (self + other ADC_Result) # Other and self not empty (self + other ADC_Result)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add ADC_Results with different sampling-rates") raise ValueError("Accumulation: You cant add ADC_Results with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -690,6 +691,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.index[i] != other.get_index_bounds(i): if self.index[i] != other.get_index_bounds(i):
raise ValueError("Accumulation: You cant add ADC_Results with different indexing") raise ValueError("Accumulation: You cant add ADC_Results with different indexing")
self.lock.acquire()
tmp_y = [] tmp_y = []
tmp_ysquare = [] tmp_ysquare = []
@@ -706,7 +709,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
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
r.job_ids = self.job_ids r.job_ids = self.job_ids.copy()
r.job_ids[other.job_id] = other.get_job_date() r.job_ids[other.job_id] = other.get_job_date()
if self.common_descriptions is not None: if self.common_descriptions is not None:
@@ -725,33 +728,29 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Self empty (copy) # Self empty (copy)
if not self.contains_data(): if not self.contains_data():
if self.uses_statistics() and not other.uses_statistics():
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
self.lock.acquire()
tmp_y = [] tmp_y = []
tmp_ysquare = [] tmp_ysquare = []
self.lock.acquire() for i in range(other.get_number_of_channels()):
tmp_y.append(numpy.array(other.y[i], dtype="float32"))
if other.uses_statistics():
tmp_ysquare.append(numpy.array(other.y_square[i], dtype="float32"))
if self.uses_statistics(): if self.uses_statistics():
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n, 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 = other.n, index = other.index, sampl_freq = other.sampling_rate, error = True)
else: else:
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = False) r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, n = other.n, index = other.index, sampl_freq = other.sampling_rate, error = False)
for i in range(other.get_number_of_channels()): other._copy_provenance(r)
tmp_y.append(other.y[i])
tmp_ysquare.append(other.y_square[i])
r.time_period=other.time_period[:]
r.job_id = other.job_id
r.job_ids = other.job_ids
if other.common_descriptions is not None:
r.common_descriptions=other.common_descriptions.copy()
else:
r.common_descriptions=None
self.lock.release() self.lock.release()
return r return r
# Other and self not empty (self + other) # Other and self not empty (self + other)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add accumulations with different sampling-rates") raise ValueError("Accumulation: You cant add accumulations with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -764,6 +763,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.uses_statistics() and not other.uses_statistics(): if self.uses_statistics() and not other.uses_statistics():
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error") raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
self.lock.acquire()
tmp_y = [] tmp_y = []
tmp_ysquare = [] tmp_ysquare = []
@@ -780,6 +781,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
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
r.job_ids = self.job_ids.copy()
for i in other.job_ids: for i in other.job_ids:
r.job_ids[i] = other.job_ids[i] r.job_ids[i] = other.job_ids[i]
r.common_descriptions={} r.common_descriptions={}
@@ -793,6 +795,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
return r return r
elif other is None:
# Convenience: ignore add of None
return self + 0
else:
raise ValueError("can not add "+repr(type(other))+" to Accumulation")
def __radd__(self, other): def __radd__(self, other):
"""Redefining other + self""" """Redefining other + self"""
return self.__add__(other) return self.__add__(other)
@@ -992,8 +1000,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Other and self not empty (self + other/ADC_Result) # Other and self not empty (self + other/ADC_Result)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates") raise ValueError("Accumulation: You can't add ADC-Results with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -1004,6 +1010,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.index[i] != other.get_index_bounds(i): if self.index[i] != other.get_index_bounds(i):
raise ValueError("Accumulation: You can't add ADC-Results with different indexing") raise ValueError("Accumulation: You can't add ADC-Results with different indexing")
self.lock.acquire()
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
self.y[i] += other.y[i] self.y[i] += other.y[i]
if self.uses_statistics(): if self.uses_statistics():
@@ -1062,7 +1070,6 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
# Other and self not empty (self + other) # Other and self not empty (self + other)
else: else:
self.lock.acquire()
if self.sampling_rate != other.get_sampling_rate(): if self.sampling_rate != other.get_sampling_rate():
raise ValueError("Accumulation: You cant add accumulations with different sampling-rates") raise ValueError("Accumulation: You cant add accumulations with different sampling-rates")
if len(self.y[0]) != len(other): if len(self.y[0]) != len(other):
@@ -1075,6 +1082,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
if self.uses_statistics() and not other.uses_statistics(): if self.uses_statistics() and not other.uses_statistics():
raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error") raise ValueError("Accumulation: You cant add non-error accumulations to accumulations with error")
self.lock.acquire()
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
self.y[i] += other.y[i] self.y[i] += other.y[i]
if self.uses_statistics(): if self.uses_statistics():
@@ -1123,7 +1132,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
"""Redefining -self""" """Redefining -self"""
if not self.contains_data(): if not self.contains_data():
return raise ValueError("Accumulation: You cant negate an empty accumulation")
tmp_y = [] tmp_y = []
@@ -1135,6 +1144,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = numpy.array(self.y_square), 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 = numpy.array(self.y_square), 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 = 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 = self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
self._copy_provenance(r)
self.lock.release() self.lock.release()
return r return r
+4
View File
@@ -17,8 +17,12 @@ class Config_Result(Resultable):
if config is None: if config is None:
self.config = { } self.config = { }
else:
self.config = config
if desc is None: if desc is None:
self.description = { } self.description = { }
else:
self.description = desc
self.job_id = job_id self.job_id = job_id
self.job_date = job_date self.job_date = job_date
+4 -4
View File
@@ -19,10 +19,6 @@ class DamarisFFT:
:param float stop: end of clipping in s :param float stop: end of clipping in s
""" """
# check if start/stop order is properly
if start > stop:
start, stop = stop, start
# do nothing if one uses clip as a "placeholder" # do nothing if one uses clip as a "placeholder"
if start is None and stop is None: if start is None and stop is None:
return self return self
@@ -32,6 +28,10 @@ class DamarisFFT:
if stop is None: if stop is None:
stop = self.x[ -1 ] stop = self.x[ -1 ]
# check if start/stop order is properly
if start > stop:
start, stop = stop, start
# check if data is fft which changes the start/stop units # check if data is fft which changes the start/stop units
if self.xlabel == "Frequency / Hz": if self.xlabel == "Frequency / Hz":
start = self.x.size * (0.5 + start / self.sampling_rate) start = self.x.size * (0.5 + start / self.sampling_rate)
+4 -1
View File
@@ -155,7 +155,10 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
def __add__(self, right_value): def __add__(self, right_value):
if right_value==0: if right_value==0:
return self.copy() new_one=MeasurementResult(self.quantity_name)
for key in self.data:
new_one[key]=self.data[key].copy()
return new_one
else: else:
raise Exception("not implemented") raise Exception("not implemented")
+3 -3
View File
@@ -21,16 +21,16 @@ class Signalpath:
if abs(reduced_angle[1]) < 1e-6: if abs(reduced_angle[1]) < 1e-6:
reduced_angle = reduced_angle[0] % 4 reduced_angle = reduced_angle[0] % 4
if reduced_angle == 0: if reduced_angle == 0:
rotated.y = list(self.y) rotated.y = [a.copy() for a in self.y]
elif reduced_angle == 1: elif reduced_angle == 1:
rotated.y = [-self.y[1], self.y[0]] rotated.y = [-self.y[1], self.y[0].copy()]
elif reduced_angle == 2: elif reduced_angle == 2:
rotated.y = [-self.y[0], -self.y[1]] rotated.y = [-self.y[0], -self.y[1]]
elif reduced_angle == 3: elif reduced_angle == 3:
rotated.y = [self.y[1], -self.y[0]] rotated.y = [self.y[1].copy(), -self.y[0]]
else: else:
rad_angle = N.deg2rad(degrees) rad_angle = N.deg2rad(degrees)
sin_angle = N.sin(rad_angle) sin_angle = N.sin(rad_angle)
+3 -3
View File
@@ -120,7 +120,7 @@ class BackendDriver(threading.Thread):
self.statefilename=None self.statefilename=None
state_files=glob.glob(os.path.join(self.spool_dir,"*.state")) state_files=glob.glob(os.path.join(self.spool_dir,"*.state"))
while len(state_files)==0: while len(state_files)==0:
if timeout<0 or self.core_input is None or self.core_input.poll() is not None or self.quit_flag.isSet(): if timeout<0 or self.core_input is None or self.core_input.poll() is not None or self.quit_flag.is_set():
# look into core log file and include contents # look into core log file and include contents
logger.error("Backend failed to start: timeout=%s poll=%s", timeout, self.core_input.poll()) logger.error("Backend failed to start: timeout=%s poll=%s", timeout, self.core_input.poll())
log_message='' log_message=''
@@ -156,10 +156,10 @@ class BackendDriver(threading.Thread):
break break
# wait on flag and look after backend # wait on flag and look after backend
while not self.quit_flag.isSet() and self.is_busy(): while not self.quit_flag.is_set() and self.is_busy():
self.quit_flag.wait(0.1) self.quit_flag.wait(0.1)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.stop_queue() self.stop_queue()
while self.is_busy(): while self.is_busy():
time.sleep(0.1) time.sleep(0.1)
+74 -90
View File
@@ -52,6 +52,31 @@ from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCan
max_points_to_display = 0 max_points_to_display = 0
def apply_font_css(widget, font_string):
"""
Non-deprecated replacement for the deprecated widget.modify_font():
converts a pango font description string (family, optional size in pt,
optional weight) into a CSS rule applied to the widget's style context.
Examples:
>>> apply_font_css(textview, "Monospace 10")
>>> apply_font_css(label, "sans bold 16")
"""
fd = pango.FontDescription(font_string)
decl = "font-family: %s;" % fd.get_family()
if fd.get_size() > 0 and not fd.get_size_is_absolute():
decl += " font-size: %.6gpt;" % (fd.get_size() / float(pango.SCALE))
if fd.get_weight() != pango.Weight.NORMAL:
decl += " font-weight: %d;" % fd.get_weight()
provider = gtk.CssProvider()
provider.load_from_data(("%s { %s }" % (widget.get_css_name(), decl)).encode("utf-8"))
context = widget.get_style_context()
context.add_provider(provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
# keep a reference so the provider is not garbage collected
setattr(widget, "_damaris_font_css_provider", provider)
import matplotlib.axes import matplotlib.axes
import matplotlib.figure import matplotlib.figure
@@ -267,12 +292,10 @@ class DamarisGUI:
def run( self ): def run( self ):
# prolong lifetime of clipboard till the very end (avoid error message) # prolong lifetime of clipboard till the very end (avoid error message)
self.main_clipboard = self.sw.main_clipboard self.main_clipboard = self.sw.main_clipboard
gdk.threads_enter( )
if self.start_immediately: if self.start_immediately:
self.toolbar_run_button.emit("clicked") self.toolbar_run_button.emit("clicked")
gtk.main( ) gtk.main( )
gdk.threads_leave( )
self.si = None self.si = None
self.sw = None self.sw = None
@@ -822,14 +845,12 @@ class DamarisGUI:
dump_size = os.stat( self.dump_filename ).st_size / 1e6 dump_size = os.stat( self.dump_filename ).st_size / 1e6
logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size) logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size)
gdk.threads_enter( )
if e_text: if e_text:
self.experiment_script_statusbar_label.set_text( e_text ) self.experiment_script_statusbar_label.set_text( e_text )
if r_text: if r_text:
self.data_handling_statusbar_label.set_text( r_text ) self.data_handling_statusbar_label.set_text( r_text )
if b_text: if b_text:
self.backend_statusbar_label.set_text( b_text ) self.backend_statusbar_label.set_text( b_text )
gdk.threads_leave( )
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f] still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f]
if len( still_running ) == 0: if len( still_running ) == 0:
@@ -838,9 +859,7 @@ class DamarisGUI:
if self.save_thread is None and self.dump_filename != "": if self.save_thread is None and self.dump_filename != "":
logger.info("All subprocesses ended, saving data pool") logger.info("All subprocesses ended, saving data pool")
# Disable run button during data saving to prevent UI confusion # Disable run button during data saving to prevent UI confusion
gdk.threads_enter()
self.toolbar_run_button.set_sensitive(False) self.toolbar_run_button.set_sensitive(False)
gdk.threads_leave()
# thread to save data... # thread to save data...
self.save_thread = threading.Thread( target=self.dump_states, name="dump states" ) self.save_thread = threading.Thread( target=self.dump_states, name="dump states" )
@@ -850,10 +869,8 @@ class DamarisGUI:
self.state = DamarisGUI.Stop_State self.state = DamarisGUI.Stop_State
if self.state == DamarisGUI.Stop_State: if self.state == DamarisGUI.Stop_State:
gdk.threads_enter( )
self.toolbar_pause_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False )
self.toolbar_stop_button.set_sensitive( False ) self.toolbar_stop_button.set_sensitive( False )
gdk.threads_leave( )
if len( still_running ) != 0: if len( still_running ) != 0:
logger.warning("Subprocess(es) still running: %s", ', '.join( [s.getName( ) for s in still_running] )) logger.warning("Subprocess(es) still running: %s", ', '.join( [s.getName( ) for s in still_running] ))
return True return True
@@ -878,12 +895,10 @@ class DamarisGUI:
# now everything is stopped # now everything is stopped
self.state = DamarisGUI.Edit_State self.state = DamarisGUI.Edit_State
gdk.threads_enter( )
self.sw.enable_editing( ) self.sw.enable_editing( )
self.toolbar_run_button.set_sensitive( True ) self.toolbar_run_button.set_sensitive( True )
self.toolbar_stop_button.set_sensitive( False ) self.toolbar_stop_button.set_sensitive( False )
self.toolbar_pause_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False )
gdk.threads_leave( )
return False return False
# dump states? # dump states?
@@ -1334,8 +1349,7 @@ class LogWindow:
self.damaris_gui = damaris_gui self.damaris_gui = damaris_gui
self.textview = self.xml_gui.get_object( "messages_textview" ) self.textview = self.xml_gui.get_object( "messages_textview" )
self.textview.connect( "key-press-event", self.textview_keypress) self.textview.connect( "key-press-event", self.textview_keypress)
font_desc = pango.FontDescription("Monospace 10") apply_font_css(self.textview, "Monospace 10")
self.textview.modify_font(font_desc)
self.textbuffer = self.textview.get_buffer( ) self.textbuffer = self.textview.get_buffer( )
self.last_timetag = None self.last_timetag = None
self( "Started in directory %s\n" % os.getcwd( ) ) self( "Started in directory %s\n" % os.getcwd( ) )
@@ -1351,11 +1365,9 @@ class LogWindow:
timetag_int = math.floor( timetag ) timetag_int = math.floor( timetag )
timetag_ms = int( (timetag - timetag_int) * 1000 ) timetag_ms = int( (timetag - timetag_int) * 1000 )
date_tag = time.strftime( "%Y%m%d %H:%M:%S.%%03d:\n", time.localtime( timetag_int ) ) % timetag_ms date_tag = time.strftime( "%Y%m%d %H:%M:%S.%%03d:\n", time.localtime( timetag_int ) ) % timetag_ms
gdk.threads_enter( )
self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) ) self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) )
self.textbuffer.insert_at_cursor( date_tag + str( message ) ) self.textbuffer.insert_at_cursor( date_tag + str( message ) )
self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5) self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5)
gdk.threads_leave( )
def textview_keypress( self, widget, event, data=None ): def textview_keypress( self, widget, event, data=None ):
if event.state & gdk.ModifierType.CONTROL_MASK != 0: if event.state & gdk.ModifierType.CONTROL_MASK != 0:
@@ -1449,22 +1461,20 @@ class ScriptWidgets:
self.data_handling_textbuffer.set_language(langpython) self.data_handling_textbuffer.set_language(langpython)
self.data_handling_textbuffer.set_highlight_syntax(True) self.data_handling_textbuffer.set_highlight_syntax(True)
fontdesc = pango.FontDescription("monospace")
self.experiment_script_textview.set_buffer( self.experiment_script_textbuffer ) self.experiment_script_textview.set_buffer( self.experiment_script_textbuffer )
self.experiment_script_textview.set_show_line_numbers(True) self.experiment_script_textview.set_show_line_numbers(True)
self.experiment_script_textview.set_auto_indent(True) self.experiment_script_textview.set_auto_indent(True)
self.experiment_script_textview.set_insert_spaces_instead_of_tabs(True) self.experiment_script_textview.set_insert_spaces_instead_of_tabs(True)
self.experiment_script_textview.set_tab_width(4) self.experiment_script_textview.set_tab_width(4)
self.experiment_script_textview.set_smart_home_end(True) self.experiment_script_textview.set_smart_home_end(True)
self.experiment_script_textview.modify_font(fontdesc) apply_font_css(self.experiment_script_textview, "monospace")
self.data_handling_textview.set_buffer( self.data_handling_textbuffer ) self.data_handling_textview.set_buffer( self.data_handling_textbuffer )
self.data_handling_textview.set_show_line_numbers(True) self.data_handling_textview.set_show_line_numbers(True)
self.data_handling_textview.set_auto_indent(True) self.data_handling_textview.set_auto_indent(True)
self.data_handling_textview.set_insert_spaces_instead_of_tabs(True) self.data_handling_textview.set_insert_spaces_instead_of_tabs(True)
self.data_handling_textview.set_tab_width(4) self.data_handling_textview.set_tab_width(4)
self.data_handling_textview.set_smart_home_end(True) self.data_handling_textview.set_smart_home_end(True)
self.data_handling_textview.modify_font(fontdesc) apply_font_css(self.data_handling_textview, "monospace")
keywords = """for if else elif in print try finally except global lambda not or pass def keywords = """for if else elif in print try finally except global lambda not or pass def
class import from as return yield while continue break assert None True False AccumulatedValue class import from as return yield while continue break assert None True False AccumulatedValue
@@ -1918,7 +1928,6 @@ get_ylabel set_ylabel"""
parent_window = self.xml_gui.get_object( "main_window" ) parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title=open_dialog_title, dialog = gtk.FileChooserDialog( title=open_dialog_title,
parent=parent_window, parent=parent_window,
action=gtk.FileChooserAction.OPEN,
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
) )
dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_default_response( gtk.ResponseType.OK )
@@ -1944,7 +1953,6 @@ get_ylabel set_ylabel"""
parent_window = self.xml_gui.get_object( "main_window" ) parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title="Open HDF5 File...", dialog = gtk.FileChooserDialog( title="Open HDF5 File...",
parent=parent_window, parent=parent_window,
action=gtk.FileChooserAction.OPEN,
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
) )
dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_default_response( gtk.ResponseType.OK )
@@ -2106,8 +2114,8 @@ get_ylabel set_ylabel"""
parent_window = self.xml_gui.get_object( "main_window" ) parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title=dialog_title, dialog = gtk.FileChooserDialog( title=dialog_title,
parent=parent_window, parent=parent_window,
action=gtk.FileChooserAction.SAVE,
buttons=("_Save", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) ) buttons=("_Save", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) )
dialog.set_property("action", gtk.FileChooserAction.SAVE)
dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_default_response( gtk.ResponseType.OK )
dialog.set_select_multiple( False ) dialog.set_select_multiple( False )
@@ -2671,11 +2679,9 @@ pygobject version %(pygobject)s
title="Select Temperature Controller Device", title="Select Temperature Controller Device",
parent=None, parent=None,
action=gtk.FileChooserAction.OPEN, action=gtk.FileChooserAction.OPEN,
buttons=(
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
gtk.STOCK_OPEN, gtk.ResponseType.OK
)
) )
dialog.add_button("Cancel", gtk.ResponseType.CANCEL)
dialog.add_button("Open", gtk.ResponseType.OK)
# Set default folder to /dev/serial/by-id/ if it exists # Set default folder to /dev/serial/by-id/ if it exists
default_folder = "/dev/serial/by-id/" default_folder = "/dev/serial/by-id/"
@@ -2767,10 +2773,10 @@ pygobject version %(pygobject)s
font = self.config_script_font_button.get_font_name( ) font = self.config_script_font_button.get_font_name( )
experiment_script_textview = self.xml_gui.get_object( "experiment_script_textview" ) experiment_script_textview = self.xml_gui.get_object( "experiment_script_textview" )
if experiment_script_textview: if experiment_script_textview:
experiment_script_textview.modify_font( pango.FontDescription( font ) ) apply_font_css(experiment_script_textview, font)
data_handling_textview = self.xml_gui.get_object( "data_handling_textview" ) data_handling_textview = self.xml_gui.get_object( "data_handling_textview" )
if data_handling_textview: if data_handling_textview:
data_handling_textview.modify_font( pango.FontDescription( font ) ) apply_font_css(data_handling_textview, font)
def browse_backend_executable_dialog( self, widget ): def browse_backend_executable_dialog( self, widget ):
""" """
@@ -3031,7 +3037,7 @@ class MonitorWidgets:
try: try:
# The coordinate label is stored in the toolbar's message attribute # The coordinate label is stored in the toolbar's message attribute
if hasattr(self.matplot_toolbar, 'message'): if hasattr(self.matplot_toolbar, 'message'):
self.matplot_toolbar.message.modify_font(pango.FontDescription("sans bold 16")) apply_font_css(self.matplot_toolbar.message, "sans bold 16")
except: except:
pass pass
@@ -3214,50 +3220,46 @@ 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
""" """
gdk.threads_enter( ) if self.data_pool is not None:
try: # maybe some extra cleanup needed
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 )
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ): self.displayed_data[ 1 ] = None
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
self.displayed_data[ 1 ] = 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.__rescale = True
self.displayed_data = [ None, None ] self.displayed_data = [ None, None ]
self.display_source_path_label.set_label( "" )
self.clear_display( )
if data_pool is not None: self.data_pool.unregister_listener( self.datapool_listener )
# keep track of data self.data_pool = None
self.data_pool = data_pool
self.data_pool.register_listener( self.datapool_listener )
# Populate source list with existing data since the listener self.update_counter_lock.acquire( )
# was registered after the data was loaded self.update_counter = 0
keys = sorted([k for k in data_pool if k != "__builtins__"]) self.update_counter_lock.release( )
for key in keys:
self.source_list_add(key)
# Select first data item if available # display states
keys = [k for k in data_pool if not k.startswith("__")] self.source_list_reset( )
if keys: self.__rescale = True
keys.sort() self.displayed_data = [ None, None ]
first_key = keys[0] self.display_source_path_label.set_label( "" )
first_iter = self.source_list_find(first_key.split("/")) self.clear_display( )
if first_iter:
self.display_source_combobox.set_active_iter(first_iter) if data_pool is not None:
finally: # keep track of data
gdk.threads_leave( ) self.data_pool = data_pool
self.data_pool.register_listener( self.datapool_listener )
# Populate source list with existing data since the listener
# was registered after the data was loaded
keys = sorted([k for k in data_pool if k != "__builtins__"])
for key in keys:
self.source_list_add(key)
# Select first data item if available
keys = [k for k in data_pool 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)
#################### observing data structures and produce idle events #################### observing data structures and produce idle events
@@ -3348,11 +3350,7 @@ class MonitorWidgets:
if self.update_counter > 10: if self.update_counter > 10:
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter) logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
else: else:
gdk.threads_enter( ) self.update_display( )
try:
self.update_display( )
finally:
gdk.threads_leave( )
else: else:
# unregister old one # unregister old one
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ],
@@ -3366,20 +3364,13 @@ class MonitorWidgets:
if self.update_counter > 10: if self.update_counter > 10:
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter) logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
else: else:
gdk.threads_enter( ) self.renew_display( )
try:
self.renew_display( )
finally:
gdk.threads_leave( )
new_data_struct = None new_data_struct = None
elif event.what == DataPool.Event.new_key: elif event.what == DataPool.Event.new_key:
# update combo-box by inserting and rely on consistent information # update combo-box by inserting and rely on consistent information
gdk.threads_enter( )
self.source_list_add( event.subject ) self.source_list_add( event.subject )
gdk.threads_leave( )
elif event.what == DataPool.Event.deleted_key: elif event.what == DataPool.Event.deleted_key:
# update combo-box by removing and rely on consistent information # update combo-box by removing and rely on consistent information
gdk.threads_enter( )
if (self.displayed_data[ 0 ] is not None and if (self.displayed_data[ 0 ] is not None and
self.displayed_data[ 0 ] == event.subject): self.displayed_data[ 0 ] == event.subject):
self.displayed_data = [ None, None ] self.displayed_data = [ None, None ]
@@ -3389,13 +3380,10 @@ class MonitorWidgets:
# not necessary, because event will be submitted # not necessary, because event will be submitted
#self.clear_display() #self.clear_display()
self.source_list_remove( event.subject ) self.source_list_remove( event.subject )
gdk.threads_leave( )
elif event.what == DataPool.Event.destroy: elif event.what == DataPool.Event.destroy:
gdk.threads_enter( )
self.source_list_reset( ) self.source_list_reset( )
self.displayed_data = [ None, None ] self.displayed_data = [ None, None ]
self.clear_display( ) self.clear_display( )
gdk.threads_leave( )
return return
def update_display_idle_event( self, subject=None ): def update_display_idle_event( self, subject=None ):
@@ -3409,11 +3397,7 @@ class MonitorWidgets:
return return
if self.displayed_data[ 0 ] is None or subject != self.displayed_data[ 0 ]: if self.displayed_data[ 0 ] is None or subject != self.displayed_data[ 0 ]:
return return
gdk.threads_enter( ) self.update_display( )
try:
self.update_display( )
finally:
gdk.threads_leave( )
######################## events from buttons ######################## events from buttons
@@ -4159,7 +4143,7 @@ class ScriptInterface:
# start them # start them
if self.back_driver is not None: if self.back_driver is not None:
self.back_driver.start( ) self.back_driver.start( )
while (not self.back_driver.quit_flag.isSet( ) and \ while (not self.back_driver.quit_flag.is_set( ) 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(): while gtk.events_pending():
gtk.main_iteration_do(False) gtk.main_iteration_do(False)
+27 -16
View File
@@ -5,6 +5,7 @@ import sys
import time import time
import logging import logging
import ast import ast
import contextlib
from damaris.experiments.Experiment import Quit from damaris.experiments.Experiment import Quit
from damaris.experiments import Experiment from damaris.experiments import Experiment
@@ -36,6 +37,19 @@ class _SandboxStdout:
return False return False
@contextlib.contextmanager
def _sandbox_stdout_redirect(log_callback):
"""Temporarily redirect process-global stdout/stderr to the sandbox log."""
old_stdout, old_stderr = sys.stdout, sys.stderr
sandbox = _SandboxStdout(log_callback)
try:
sys.stdout = sandbox
sys.stderr = sandbox
yield sandbox
finally:
sys.stdout, sys.stderr = old_stdout, old_stderr
class ExperimentHandling(threading.Thread): class ExperimentHandling(threading.Thread):
""" """
runs the experiment script in sandbox runs the experiment script in sandbox
@@ -52,17 +66,25 @@ class ExperimentHandling(threading.Thread):
self.data["__recentexperiment"]=-1 self.data["__recentexperiment"]=-1
def synchronize(self, before=0, waitsteps=0.1): def synchronize(self, before=0, waitsteps=0.1):
while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.isSet(): while (self.data["__recentexperiment"]>self.data["__recentresult"]+before) and not self.quit_flag.is_set():
self.quit_flag.wait(waitsteps) self.quit_flag.wait(waitsteps)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
raise StopExperiment raise StopExperiment
def sleep(self, seconds): def sleep(self, seconds):
self.quit_flag.wait(seconds) self.quit_flag.wait(seconds)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
raise StopExperiment raise StopExperiment
def run(self): def run(self):
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
if self._log_callback is not None:
with _sandbox_stdout_redirect(self._log_callback):
self._run_script()
else:
self._run_script()
def _run_script(self):
dataspace={} dataspace={}
exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment']) exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment'])
for name in dir(exp_classes): for name in dir(exp_classes):
@@ -77,13 +99,6 @@ class ExperimentHandling(threading.Thread):
self.location = None self.location = None
exp_iterator=None exp_iterator=None
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
_orig_stdout = sys.stdout
_orig_stderr = sys.stderr
if self._log_callback is not None:
sys.stdout = _SandboxStdout(self._log_callback)
sys.stderr = _SandboxStdout(self._log_callback)
# check for time.sleep() # check for time.sleep()
try: try:
tree = ast.parse(self.script) tree = ast.parse(self.script)
@@ -131,7 +146,7 @@ class ExperimentHandling(threading.Thread):
traceback_file=None traceback_file=None
return return
while exp_iterator is not None and not self.quit_flag.isSet(): while exp_iterator is not None and not self.quit_flag.is_set():
# get next experiment from script # get next experiment from script
try: try:
job=next(exp_iterator) job=next(exp_iterator)
@@ -171,7 +186,7 @@ class ExperimentHandling(threading.Thread):
# relax for a short time # relax for a short time
if "__resultsinadvance" in self.data and self.data["__resultsinadvance"]+100<job.job_id: if "__resultsinadvance" in self.data and self.data["__resultsinadvance"]+100<job.job_id:
self.quit_flag.wait(0.05) self.quit_flag.wait(0.05)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
dataspace=None dataspace=None
exp_iterator=None exp_iterator=None
break break
@@ -181,7 +196,3 @@ class ExperimentHandling(threading.Thread):
dataspace=None dataspace=None
self.exp_iterator=None self.exp_iterator=None
self.writer=None self.writer=None
# Restore stdout/stderr
sys.stdout = _orig_stdout
sys.stderr = _orig_stderr
+10 -13
View File
@@ -7,7 +7,7 @@ import traceback
import logging import logging
import ast import ast
from damaris.data import Resultable from damaris.data import Resultable
from damaris.gui.ExperimentHandling import StopExperiment, _SandboxStdout from damaris.gui.ExperimentHandling import StopExperiment, _sandbox_stdout_redirect
logger = logging.getLogger("damaris.result_handling") logger = logging.getLogger("damaris.result_handling")
@@ -28,12 +28,13 @@ class ResultHandling(threading.Thread):
def run(self): def run(self):
# Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI # Redirect stdout/stderr so print() in sandboxed scripts goes to the GUI
_orig_stdout = sys.stdout
_orig_stderr = sys.stderr
if self._log_callback is not None: if self._log_callback is not None:
sys.stdout = _SandboxStdout(self._log_callback) with _sandbox_stdout_redirect(self._log_callback):
sys.stderr = _SandboxStdout(self._log_callback) self._run_script()
else:
self._run_script()
def _run_script(self):
# execute it # execute it
dataspace={} dataspace={}
data_classes = __import__('damaris.data', dataspace, dataspace, ['*']) data_classes = __import__('damaris.data', dataspace, dataspace, ['*'])
@@ -96,30 +97,26 @@ class ResultHandling(threading.Thread):
traceback_file=None traceback_file=None
dataspace=None dataspace=None
# Restore stdout/stderr
sys.stdout = _orig_stdout
sys.stderr = _orig_stderr
def sleep(self, seconds): def sleep(self, seconds):
self.quit_flag.wait(seconds) self.quit_flag.wait(seconds)
if self.quit_flag.isSet(): if self.quit_flag.is_set():
raise StopExperiment raise StopExperiment
def __iter__(self): def __iter__(self):
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.results=None self.results=None
return return
for i in self.results: for i in self.results:
if hasattr(self.results, "in_advance"): if hasattr(self.results, "in_advance"):
self.data_space["__resultsinadvance"]=self.results.in_advance self.data_space["__resultsinadvance"]=self.results.in_advance
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.results=None self.results=None
return return
if isinstance(i, Resultable.Resultable): if isinstance(i, Resultable.Resultable):
if self.data_space is not None: if self.data_space is not None:
self.data_space["__recentresult"]=i.job_id+0 self.data_space["__recentresult"]=i.job_id+0
yield i yield i
if self.quit_flag.isSet(): if self.quit_flag.is_set():
self.results=None self.results=None
return return
+6 -6
View File
@@ -15,7 +15,7 @@ import base64
import numpy import numpy
try: try:
import xml.etree.cElementTree import xml.etree.ElementTree
ELEMENT_TREE = True ELEMENT_TREE = True
except: except:
import xml.parsers.expat import xml.parsers.expat
@@ -109,7 +109,7 @@ class ResultReader:
self.result_job_number = None self.result_job_number = None
# Job Date is set in __read_file() # Job Date is set in __read_file()
self.__filetype = None self.__filetype = None
for elem in xml.etree.cElementTree.ElementTree(file=in_file).iter(): for elem in xml.etree.ElementTree.ElementTree(file=in_file).iter():
if elem.tag == 'result': if elem.tag == 'result':
self.result_job_number = int(elem.get("job")) self.result_job_number = int(elem.get("job"))
pass pass
@@ -542,7 +542,7 @@ class BlockingResultReader(ResultReader):
block until result is available block until result is available
""" """
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no)) expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
while (not self.quit_flag.isSet()) and (self.stop_no is None or self.stop_no>self.no): while (not self.quit_flag.is_set()) and (self.stop_no is None or self.stop_no>self.no):
if not os.access(expected_filename,os.R_OK): if not os.access(expected_filename,os.R_OK):
# stop polling, if required # stop polling, if required
if self.poll_time<0: break if self.poll_time<0: break
@@ -558,11 +558,11 @@ class BlockingResultReader(ResultReader):
self.in_advance+=1 self.in_advance+=1
in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1)) in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1))
if self.quit_flag.isSet(): break if self.quit_flag.is_set(): break
r=self.get_result_object(expected_filename) r=self.get_result_object(expected_filename)
if self.quit_flag.isSet(): break if self.quit_flag.is_set(): break
if self.quit_flag.isSet(): break if self.quit_flag.is_set(): break
yield r yield r
if self.clear_results: if self.clear_results:
+25 -32
View File
@@ -13,25 +13,6 @@ import datetime
from damaris.data import TemperatureResult from damaris.data import TemperatureResult
from damaris.tools.temperature import TemperatureController from damaris.tools.temperature import TemperatureController
# GTK imports are done lazily to avoid initialization issues
_HAS_GTK = None
_gdk = None
def _get_gdk():
"""Lazily import gdk to avoid GTK initialization issues."""
global _HAS_GTK, _gdk
if _HAS_GTK is None:
try:
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk as gdk
_gdk = gdk
_HAS_GTK = True
except (ImportError, ValueError):
_HAS_GTK = False
return _gdk if _HAS_GTK else None
class TemperatureMonitor(threading.Thread): class TemperatureMonitor(threading.Thread):
""" """
@@ -72,6 +53,10 @@ class TemperatureMonitor(threading.Thread):
self.result = TemperatureResult() self.result = TemperatureResult()
self._running = False self._running = False
self._lock = threading.RLock() self._lock = threading.RLock()
self._consecutive_failures = 0
self._max_consecutive_failures = 10
self._last_error = None
self._connection_ok = True
# Initialize the result with metadata # Initialize the result with metadata
self.result.xlabel = "Time (s)" self.result.xlabel = "Time (s)"
@@ -124,21 +109,29 @@ class TemperatureMonitor(threading.Thread):
data_pool_result.job_id = self.result.job_id if hasattr(self.result, 'job_id') else None data_pool_result.job_id = self.result.job_id if hasattr(self.result, 'job_id') else None
data_pool_result.job_date = self.result.job_date if hasattr(self.result, 'job_date') else None data_pool_result.job_date = self.result.job_date if hasattr(self.result, 'job_date') else None
# Store in data_pool in GTK thread (if GTK available) # Store in data_pool (dict item assignment is atomic under the GIL)
gdk = _get_gdk() self.data_pool[self.data_key] = data_pool_result
if gdk is not None:
gdk.threads_enter() # Reset consecutive failure counter on successful read
try: self._consecutive_failures = 0
self.data_pool[self.data_key] = data_pool_result self._last_error = None
finally: self._connection_ok = True
gdk.threads_leave()
else:
self.data_pool[self.data_key] = data_pool_result
except Exception as e: except Exception as e:
print(f"TemperatureMonitor error: {e}") self._consecutive_failures += 1
import traceback self._last_error = str(e)
traceback.print_exc() if self._consecutive_failures <= 3:
print(f"TemperatureMonitor read error (attempt {self._consecutive_failures}): {e}")
elif self._consecutive_failures == 5:
print(f"TemperatureMonitor: repeated read errors, checking connection...")
if hasattr(self.controller, 'check_connection'):
self._connection_ok = self.controller.check_connection()
if not self._connection_ok:
print(f"TemperatureMonitor: WARNING - controller appears disconnected after {self._consecutive_failures} failures")
elif self._consecutive_failures >= self._max_consecutive_failures:
print(f"TemperatureMonitor: ERROR - {self._consecutive_failures} consecutive failures, controller may be disconnected")
else:
print(f"TemperatureMonitor error (failure #{self._consecutive_failures}): {e}")
# Sleep in small increments for responsive shutdown # Sleep in small increments for responsive shutdown
sleep_remaining = self.interval sleep_remaining = self.interval
+24 -14
View File
@@ -1,5 +1,6 @@
import serial import serial
import re import re
import time
import operator import operator
from functools import reduce from functools import reduce
@@ -57,21 +58,22 @@ class Eurotherm(TemperatureController):
def send_read_param(self, param): def send_read_param(self, param):
self.s.write(EOT + self.device + param + ENQ) self.s.write(EOT + self.device + param + ENQ)
def read_param(self, param): def read_param(self, param, retries=3, retry_delay=0.05):
self.s.flushInput() for attempt in range(retries):
self.send_read_param(param) self.s.flushInput()
answer = self.s.read(self._expect_len) self.send_read_param(param)
m = reply_pattern.search(answer) answer = self.s.read(self._expect_len)
if m is None:
# Reading _expect_len bytes was not enough...
answer += self.s.read(200)
m = reply_pattern.search(answer) m = reply_pattern.search(answer)
if m is not None: if m is None:
self._expect_len = len(answer) answer += self.s.read(200)
return m.group(1) m = reply_pattern.search(answer)
else: if m is not None:
print("received:", repr(answer)) self._expect_len = len(answer)
return None return m.group(1)
if attempt < retries - 1:
time.sleep(retry_delay)
print("received:", repr(answer))
return None
def write_param(self, mnemonic, data): def write_param(self, mnemonic, data):
if len(mnemonic) > 2: if len(mnemonic) > 2:
@@ -128,6 +130,14 @@ class Eurotherm(TemperatureController):
"""Set target temperature in Celsius.""" """Set target temperature in Celsius."""
return self.write_param('SL', str(temperature)) return self.write_param('SL', str(temperature))
def check_connection(self) -> bool:
"""Check if the Eurotherm device is connected and responsive."""
try:
result = self.read_param('PV')
return result is not None
except Exception:
return False
def close(self): def close(self):
"""Close the serial connection.""" """Close the serial connection."""
if hasattr(self, 's') and self.s is not None: if hasattr(self, 's') and self.s is not None:
+20 -7
View File
@@ -1,5 +1,6 @@
import serial import serial
import re import re
import time
import numpy as np import numpy as np
from .temperature import TemperatureController, register_controller from .temperature import TemperatureController, register_controller
@@ -91,13 +92,17 @@ class Lakeshore(TemperatureController):
else: else:
return 0.0 return 0.0
def read_param(self, param): def read_param(self, param, retries=3, retry_delay=0.05):
"""Read a parameter using simple ASCII commands.""" """Read a parameter using simple ASCII commands."""
self.s.write(f"{param}?\n") for attempt in range(retries):
answer = self.s.readline().strip() self.s.flushInput()
if answer and len(answer) > 1: self.s.write(f"{param}?\n")
# Remove the first character (command echo) and return the value answer = self.s.readline().strip()
return answer[1:] if answer and len(answer) > 1:
# Remove the first character (command echo) and return the value
return answer[1:]
if attempt < retries - 1:
time.sleep(retry_delay)
return None return None
def get_value(self, name): def get_value(self, name):
@@ -134,9 +139,17 @@ class Lakeshore(TemperatureController):
def set_setpoint(self, temperature: float) -> bool: def set_setpoint(self, temperature: float) -> bool:
"""Set target temperature in Celsius.""" """Set target temperature in Celsius."""
self.s.write(f"SETP {temperature}\n") self.s.write(f"SETP 1,{temperature}\n")
return True return True
def check_connection(self) -> bool:
"""Check if the Lakeshore device is connected and responsive."""
try:
result = self.read_param('KRDG')
return result is not None
except Exception:
return False
def close(self): def close(self):
"""Close the serial connection.""" """Close the serial connection."""
if hasattr(self, 's') and self.s is not None: if hasattr(self, 's') and self.s is not None:
+10
View File
@@ -149,3 +149,13 @@ class TemperatureController(ABC):
if they have resources to clean up. if they have resources to clean up.
""" """
pass pass
def check_connection(self) -> bool:
"""
Check if the controller is connected and responsive.
Returns:
bool: True if the controller is connected and responsive,
False otherwise. Subclasses should override this method.
"""
return False
+3 -2
View File
@@ -11,6 +11,7 @@ def test(i):
return e return e
def experiment(): def experiment():
for i in range(1400): for i in range(100):
sleep(0.1) #sleep(0.1)
yield test(i) yield test(i)
synchronize()
+2
View File
@@ -9,6 +9,7 @@ def result():
except: except:
print("h5 already opened") print("h5 already opened")
accu = Accumulation(error=False) accu = Accumulation(error=False)
for num,ts in enumerate(results): for num,ts in enumerate(results):
data["accu"] = accu data["accu"] = accu
data["data/ts %i"%num] = ts+0 data["data/ts %i"%num] = ts+0
@@ -17,6 +18,7 @@ def result():
r[num] = ts.y[0][0] r[num] = ts.y[0][0]
data["test"] = r data["test"] = r
accu+=ts accu+=ts
print(accu)
accu.write_to_hdf(h5, "/", "name", "title") accu.write_to_hdf(h5, "/", "name", "title")
try: try:
h5.close() h5.close()
+38 -11
View File
@@ -195,14 +195,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]+10, y[i] + 10, strict=True) np.testing.assert_array_equal(adc.y[i]+10, y[i] + 10, strict=True)
adc += 10 adc += 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] + 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] + 10).astype("float32"), strict=True)
# test float addition # test float addition
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] + 10., y[i] + 10., strict=True) np.testing.assert_array_equal(adc.y[i] + 10., (y[i] + 10 + 10.).astype("float32"), strict=True)
adc += 10. adc += 10.
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] + 10 + 10., strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] + 10 + 10.).astype("float32"), strict=True)
def test_operator_sub_scalar(self): def test_operator_sub_scalar(self):
""" """
@@ -215,14 +215,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i] - 10, y[i] - 10, strict=True) np.testing.assert_array_equal(adc.y[i] - 10, y[i] - 10, strict=True)
adc -= 10 adc -= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] - 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] - 10).astype("float32"), strict=True)
# test float subtraction # test float subtraction
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] - 10., y[i] - 10., strict=True) np.testing.assert_array_equal(adc.y[i] - 10., (y[i] - 10 - 10.).astype("float32"), strict=True)
adc -= 10. adc -= 10.
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] - 10 - 10., strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] - 10 - 10.).astype("float32"), strict=True)
def test_operator_mul_scalar(self): def test_operator_mul_scalar(self):
""" """
@@ -236,14 +236,14 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i] * 10, y[i] * 10, strict=True) np.testing.assert_array_equal(adc.y[i] * 10, y[i] * 10, strict=True)
adc *= 10 adc *= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] * 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] * 10).astype("float32"), strict=True)
# test float multiplication # test float multiplication
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i] * 10.0, y[i] * 10.0, strict=True) np.testing.assert_array_equal(adc.y[i] * 10.0, (y[i] * 10 * 10.0).astype("float32"), strict=True)
adc *= 10.0 adc *= 10.0
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] * 10 * 10.0, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] * 10 * 10.0).astype("float32"), strict=True)
def test_operator_truediv_scalar(self): def test_operator_truediv_scalar(self):
""" """
@@ -255,7 +255,7 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]/10, y[i] / 10, strict=True) np.testing.assert_array_equal(adc.y[i]/10, y[i] / 10, strict=True)
adc /= 10 adc /= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] / 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] / 10).astype("float32"), strict=True)
def test_operator_floordiv_scalar(self): def test_operator_floordiv_scalar(self):
""" """
@@ -267,10 +267,37 @@ class TestADCResult(unittest.TestCase):
np.testing.assert_array_equal(adc.y[i]//10, y[i] // 10, strict=True) np.testing.assert_array_equal(adc.y[i]//10, y[i] // 10, strict=True)
adc //= 10 adc //= 10
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(adc.y[i], y[i] // 10, strict=True) np.testing.assert_array_equal(adc.y[i], (y[i] // 10).astype("float32"), strict=True)
self.assertRaises(ValueError, adc.__floordiv__, 1.0) self.assertRaises(ValueError, adc.__floordiv__, 1.0)
self.assertRaises(ValueError, adc.__rfloordiv__, 1.0) self.assertRaises(ValueError, adc.__rfloordiv__, 1.0)
def test_operators_keep_channel_metadata(self):
"""operator results keep nChannels/is_clipped identical to the operand"""
adc = self.create_adc_result()
adc.is_clipped = True
results = [adc + 1, adc - 1, adc * 2, adc ** 2, adc / 2., adc // 2,
10 + adc, 10 - adc, 2 * adc, 100. / adc, 100 // adc, -adc]
for r in results:
self.assertEqual(r.get_nChannels(), adc.get_nChannels())
self.assertEqual(r.is_clipped, adc.is_clipped)
def test_hdf_roundtrip_keeps_nchannels(self):
"""read_from_hdf must restore the number of channels"""
import tempfile
import tables
from damaris.data.ADC_Result import read_from_hdf
adc = self.create_adc_result()
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "adc.h5")
with tables.open_file(path, "w") as h5:
adc.write_to_hdf(h5, h5.root, "adc", "test")
with tables.open_file(path, "r") as h5:
adc2 = read_from_hdf(h5.root.adc)
self.assertIsNotNone(adc2)
self.assertTrue(adc2.contains_data())
self.assertEqual(adc2.get_nChannels(), adc.get_nChannels())
def test_operator_overload_adc_result_exception(self): def test_operator_overload_adc_result_exception(self):
""" """
Test the ValueError raised when ADC_Result is used as an operand in an arithmetic operation Test the ValueError raised when ADC_Result is used as an operand in an arithmetic operation
+71 -1
View File
@@ -1,5 +1,6 @@
import os import os
import subprocess import subprocess
import threading
import unittest import unittest
from datetime import datetime from datetime import datetime
@@ -258,7 +259,7 @@ class TestAccumulation(unittest.TestCase):
np.testing.assert_array_equal(accu.y[i], y[i] - 10) np.testing.assert_array_equal(accu.y[i], y[i] - 10)
# test float subtraction # test float subtraction
adc -= 10. accu -= 10.
for i in range(adc.get_nChannels()): for i in range(adc.get_nChannels()):
np.testing.assert_array_equal(accu.y[i], y[i] - 10 - 10.) np.testing.assert_array_equal(accu.y[i], y[i] - 10 - 10.)
@@ -312,5 +313,74 @@ class TestAccumulation(unittest.TestCase):
self.assertRaises(ValueError, accu.__floordiv__, 1.0) self.assertRaises(ValueError, accu.__floordiv__, 1.0)
self.assertRaises(ValueError, accu.__ifloordiv__, 1.0) self.assertRaises(ValueError, accu.__ifloordiv__, 1.0)
def test_add_invalid_operand_raises(self):
"""accu + bogus must raise ValueError instead of returning None."""
adc = self.create_adc_result()
accu = Accumulation()
accu += adc
self.assertRaises(ValueError, accu.__add__, "foo")
self.assertRaises(ValueError, accu.__iadd__, "foo")
def test_neg_empty_raises(self):
"""-empty accumulation must raise ValueError."""
self.assertRaises(ValueError, Accumulation().__neg__)
def test_add_adc_result_does_not_modify_job_ids(self):
"""accu + adc must not insert the adc job id into accu.job_ids."""
accu = Accumulation()
accu += self.create_adc_result()
n_before = len(accu.job_ids)
adc2 = self.create_adc_result()
adc2.job_id = 12
r = accu + adc2
self.assertIn(12, r.job_ids)
self.assertNotIn(12, accu.job_ids)
self.assertEqual(len(accu.job_ids), n_before)
def test_empty_plus_accumulation_copies_data(self):
"""empty_accu + accu must copy arrays and metadata, not alias them."""
adc = self.create_adc_result()
accu = Accumulation()
accu += adc
accu.is_clipped = True
r = Accumulation() + accu
self.assertIsNot(r.y[0], accu.y[0])
self.assertIsNot(r.job_ids, accu.job_ids)
self.assertTrue(r.is_clipped)
# in-place operation on the result must leave the operand unchanged
r *= 2
np.testing.assert_array_equal(accu.y[0], np.array([10, 20, 30], dtype="float32"))
def test_accumulation_plus_accumulation_keeps_both_job_ids(self):
"""accu1 + accu2 must keep the provenance of both operands."""
adc1 = self.create_adc_result()
adc2 = self.create_adc_result()
adc2.job_id = 12
accu1 = Accumulation()
accu1 += adc1
accu2 = Accumulation()
accu2 += adc2
r = accu1 + accu2
self.assertEqual(set(r.job_ids.keys()), {11, 12})
self.assertEqual(set(accu1.job_ids.keys()), {11})
self.assertEqual(set(accu2.job_ids.keys()), {12})
def test_empty_error_plus_non_error_raises(self):
"""error accumulation + non-error accumulation must raise ValueError."""
accu = Accumulation()
accu += self.create_adc_result()
empty_err = Accumulation(error=True)
self.assertRaises(ValueError, empty_err.__add__, accu)
free = []
def check_lock():
if empty_err.lock.acquire(timeout=1):
free.append(True)
empty_err.lock.release()
t = threading.Thread(target=check_lock)
t.start()
t.join(3)
self.assertTrue(free, "lock leaked after ValueError in __add__")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+29
View File
@@ -0,0 +1,29 @@
import unittest
from damaris.data.Config_Result import Config_Result
class TestConfigResult(unittest.TestCase):
def test_constructor_with_no_arguments(self):
"""Test initializer with no arguments."""
cr = Config_Result()
self.assertEqual(cr.get_config_dictionary(), {})
self.assertEqual(cr.description, {})
def test_constructor_with_arguments(self):
"""Test initializer with config and description arguments."""
config = {"a": 1}
desc = {"b": 2}
cr = Config_Result(config=config, desc=desc)
self.assertEqual(cr.get_config_dictionary(), config)
self.assertEqual(cr.description, desc)
def test_get_set_config(self):
cr = Config_Result()
cr.set_config("key", "value")
self.assertEqual(cr.get_config("key"), "value")
self.assertIsNone(cr.get_config("missing"))
if __name__ == "__main__":
unittest.main()
+44
View File
@@ -0,0 +1,44 @@
import unittest
import numpy as np
from damaris.data.DamarisFFT import DamarisFFT
class TestDamarisFFT(unittest.TestCase):
@staticmethod
def make_fft():
fft = DamarisFFT()
fft.sampling_rate = 100.0
fft.x = np.arange(10, dtype="float32") / 100.0
fft.y = [np.arange(10, dtype="float32")]
fft.xlabel = "Time / s"
return fft
def test_clip_without_arguments_is_placeholder(self):
"""clip() without arguments returns self unchanged (placeholder use)."""
fft = self.make_fft()
self.assertIs(fft.clip(), fft)
self.assertEqual(len(fft.x), 10)
np.testing.assert_array_equal(fft.y[0], np.arange(10, dtype="float32"))
def test_clip_swaps_reversed_bounds(self):
fft = self.make_fft()
fft.clip(start=0.05, stop=0.01)
expected_x = (np.arange(10, dtype="float32") / 100.0)[1:5]
np.testing.assert_array_equal(fft.x, expected_x)
np.testing.assert_array_equal(fft.y[0], np.arange(1, 5, dtype="float32"))
def test_clip_single_bound(self):
fft = self.make_fft()
fft.clip(start=0.02)
self.assertEqual(len(fft.x), 7)
self.assertEqual(len(fft.y[0]), 7)
def test_clip_too_close_raises(self):
fft = self.make_fft()
self.assertRaises(ValueError, fft.clip, 0.021, 0.029)
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
import sys
import threading
import unittest
from damaris.gui.ExperimentHandling import ExperimentHandling
from damaris.gui.ResultHandling import ResultHandling
class _FakeWriter:
def __init__(self):
self.sent = []
def send_next(self, job, quit=False):
self.sent.append(job)
class _FakeResultIterator:
def __init__(self):
self.quit_flag = threading.Event()
def __iter__(self):
return iter([])
class TestSandboxStdoutRestore(unittest.TestCase):
"""
sys.stdout/sys.stderr are process-global: the sandbox redirect in
run() must be undone even when the script raises or returns early.
"""
def test_experiment_stdout_restored_after_script_exception(self):
orig_stdout = sys.stdout
orig_stderr = sys.stderr
handled = ExperimentHandling("raise ValueError('boom')", None, None,
log_callback=lambda line: None)
handled.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIs(sys.stderr, orig_stderr)
self.assertIsNotNone(handled.raised_exception)
def test_experiment_stdout_restored_on_success(self):
orig_stdout = sys.stdout
writer = _FakeWriter()
handled = ExperimentHandling("print('hello')\nexperiment = lambda: iter([])",
writer, None, log_callback=lambda line: None)
handled.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIsNone(handled.raised_exception)
def test_result_stdout_restored_after_script_exception(self):
orig_stdout = sys.stdout
orig_stderr = sys.stderr
handling = ResultHandling("raise ValueError('boom')", _FakeResultIterator(),
None, log_callback=lambda line: None)
handling.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIs(sys.stderr, orig_stderr)
self.assertIsNotNone(handling.raised_exception)
def test_result_stdout_restored_without_result_function(self):
orig_stdout = sys.stdout
handling = ResultHandling("x = 1", _FakeResultIterator(), None,
log_callback=lambda line: None)
handling.run()
self.assertIs(sys.stdout, orig_stdout)
self.assertIsNone(handling.raised_exception)
if __name__ == "__main__":
unittest.main()
+35
View File
@@ -0,0 +1,35 @@
import unittest
import numpy as np
from damaris.data.MeasurementResult import MeasurementResult
class TestMeasurementResult(unittest.TestCase):
def test_add_zero_preserves_data(self):
"""mr + 0 must return a MeasurementResult with identical data."""
mr = MeasurementResult("test_quantity")
for i in range(3):
mr[i] = float(i * i)
mr2 = mr + 0
self.assertIsInstance(mr2, MeasurementResult)
self.assertEqual(mr2.quantity_name, "test_quantity")
np.testing.assert_array_equal(mr2.get_xdata(), mr.get_xdata())
np.testing.assert_array_equal(mr2.get_ydata(), mr.get_ydata())
def test_add_zero_returns_independent_copy(self):
"""Changing the copy must not change the original."""
mr = MeasurementResult("q")
mr[1.0] = 5.0
mr2 = mr + 0
self.assertIsNot(mr2[1.0], mr[1.0])
mr2[1.0] = mr2[1.0] + 100.0
self.assertEqual(mr[1.0].mean(), 5.0)
def test_add_nonzero_raises(self):
mr = MeasurementResult("q")
self.assertRaises(Exception, mr.__add__, 1)
if __name__ == "__main__":
unittest.main()