Public Access
fix return-value/deprecated API bugs, accumulation lock leaks, code cleanup
- 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:
1 parent
91ade4376c
commit
f9d8de7697
26 files changed
+590
-236
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
============
|
============
|
||||||
|
|||||||
@@ -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
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -1074,6 +1081,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
raise ValueError("Accumulation: You cant add accumulations with different indexing")
|
raise ValueError("Accumulation: You cant add accumulations with different indexing")
|
||||||
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]
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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( )
|
self.data_pool.unregister_listener( self.datapool_listener )
|
||||||
|
self.data_pool = None
|
||||||
|
|
||||||
if data_pool is not None:
|
self.update_counter_lock.acquire( )
|
||||||
# keep track of data
|
self.update_counter = 0
|
||||||
self.data_pool = data_pool
|
self.update_counter_lock.release( )
|
||||||
self.data_pool.register_listener( self.datapool_listener )
|
|
||||||
|
# display states
|
||||||
# Populate source list with existing data since the listener
|
self.source_list_reset( )
|
||||||
# was registered after the data was loaded
|
self.__rescale = True
|
||||||
keys = sorted([k for k in data_pool if k != "__builtins__"])
|
self.displayed_data = [ None, None ]
|
||||||
for key in keys:
|
self.display_source_path_label.set_label( "" )
|
||||||
self.source_list_add(key)
|
self.clear_display( )
|
||||||
|
|
||||||
# Select first data item if available
|
if data_pool is not None:
|
||||||
keys = [k for k in data_pool if not k.startswith("__")]
|
# keep track of data
|
||||||
if keys:
|
self.data_pool = data_pool
|
||||||
keys.sort()
|
self.data_pool.register_listener( self.datapool_listener )
|
||||||
first_key = keys[0]
|
|
||||||
first_iter = self.source_list_find(first_key.split("/"))
|
# Populate source list with existing data since the listener
|
||||||
if first_iter:
|
# was registered after the data was loaded
|
||||||
self.display_source_combobox.set_active_iter(first_iter)
|
keys = sorted([k for k in data_pool if k != "__builtins__"])
|
||||||
finally:
|
for key in keys:
|
||||||
gdk.threads_leave( )
|
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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
+4
-3
@@ -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()
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user