Public Access
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75863ffc19 | ||
|
|
6f0b090d9d | ||
|
|
d9f2cc3f2c | ||
|
|
e9e3f5a5f5 | ||
|
|
9441b5a4d9 | ||
|
|
7f29f2d173 | ||
|
|
847ab4d89c | ||
|
|
d6da2f2bda | ||
|
|
5338304ee8 | ||
|
|
d48f96e498 | ||
|
|
058716f3c0 | ||
|
|
6d9f55f788 | ||
|
|
e8d0e45bfb | ||
|
|
08e1fe2a1a | ||
|
|
064036241b | ||
|
|
97aa99c239 | ||
|
|
f26d203e79 | ||
|
|
594c0ee8d4 | ||
|
|
68da261ab4 | ||
|
|
00466cb559 | ||
|
|
7ba5c7d56a | ||
|
|
c181e15aa8 | ||
|
|
26970ba201 | ||
|
|
eab49db8d4 | ||
|
|
3bfb2670eb | ||
|
|
83a01f1d83 | ||
|
|
b46debad69 | ||
|
|
4250039b3c | ||
|
|
d3b1a9e3fd | ||
|
|
06867ec17b | ||
|
|
80d8f1e78d | ||
|
|
58e7846bbe | ||
|
|
1c4f8f5344 | ||
|
|
a3738025b7 |
No files matched your search
@@ -6,3 +6,8 @@ __pycache__
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
*.whl
|
*.whl
|
||||||
.idea
|
.idea
|
||||||
|
debian/files
|
||||||
|
.pybuild
|
||||||
|
.junie
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/.debhelper
|
||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
include src/gui/DAMARIS3.png
|
include src/damaris/gui/DAMARIS3.png
|
||||||
include src/gui/DAMARIS3.ico
|
include src/damaris/gui/DAMARIS3.ico
|
||||||
include src/gui/damaris.glade
|
include src/damaris/gui/damaris3.glade
|
||||||
include src/gui/damaris.gladep
|
include src/damaris/gui/python.xml
|
||||||
#recursive-include doc/reference-html *.html *.gif *.png *.css
|
#recursive-include doc/reference-html *.html *.gif *.png *.css
|
||||||
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
|
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
|
||||||
@@ -15,3 +15,12 @@ Alternatively you can install withoout the system-site-packages:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
This will download newer packages and install them in the venv.
|
This will download newer packages and install them in the venv.
|
||||||
|
|
||||||
|
|
||||||
|
Using uv
|
||||||
|
========
|
||||||
|
Dependicies needed:
|
||||||
|
sudo apt install libgirepository1.0-dev pkg-config
|
||||||
|
|
||||||
|
Then you can start DAMARIS3:
|
||||||
|
uv run DAMARIS3
|
||||||
Vendored
+9
@@ -1,3 +1,12 @@
|
|||||||
|
python3-damaris (0.22) stable; urgency=medium
|
||||||
|
|
||||||
|
* fixed autocomplete in the editor tabs
|
||||||
|
* added clipping warning on ADC overload, ADC bit-depth settable in config
|
||||||
|
* Accumulation object keeps track of job_ids
|
||||||
|
* get_result_by_index keeps common description
|
||||||
|
|
||||||
|
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 05 Jul 2026 14:09:34 +0200
|
||||||
|
|
||||||
python3-damaris (0.20) stable; urgency=medium
|
python3-damaris (0.20) stable; urgency=medium
|
||||||
|
|
||||||
* Unified version management across Python app, GUI and documentation
|
* Unified version management across Python app, GUI and documentation
|
||||||
|
|||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
python3-damaris_0.20_all.deb science optional
|
|
||||||
python3-damaris_0.20_amd64.buildinfo science optional
|
|
||||||
+5
-4
@@ -14,12 +14,13 @@ license = {text = "GPL-2.0"}
|
|||||||
keywords = ["NMR", "data-processing"]
|
keywords = ["NMR", "data-processing"]
|
||||||
readme = {file = "README", content-type = "text/plain"}
|
readme = {file = "README", content-type = "text/plain"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"scipy >= 0.14.0",
|
"scipy>=0.14.0",
|
||||||
"numpy >= 1.8.2, < 2.0",
|
"numpy>=1.8.2,<2.0",
|
||||||
"tables >= 3.1.1",
|
"tables >= 3.1.1",
|
||||||
"matplotlib >= 1.4.1, <= 3.6",
|
"matplotlib >= 1.4.1, <= 3.6",
|
||||||
"pyxdg",
|
"pyxdg",
|
||||||
"PyGObject >= 3.14.0, <= 3.51.0"
|
"PyGObject >= 3.14.0, <= 3.51.0",
|
||||||
|
"pyserial>=3.5",
|
||||||
]
|
]
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Achim Gädke"},
|
{name = "Achim Gädke"},
|
||||||
@@ -39,7 +40,7 @@ DAMARIS3 = "damaris.__main__:main"
|
|||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "damaris.xml", "python.xml", "damaris3.glade"]
|
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "python.xml", "damaris3.glade"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "damaris.__version__"}
|
version = {attr = "damaris.__version__"}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
|
|
||||||
## module contents
|
## module contents
|
||||||
#
|
#
|
||||||
__version__ = "0.20"
|
__version__ = "0.22"
|
||||||
__all__=["experiments", "data", "gui"]
|
__all__=["experiments", "data", "gui"]
|
||||||
@@ -19,6 +19,7 @@ import tables
|
|||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||||
|
default_bit_depth = 14
|
||||||
"""
|
"""
|
||||||
Represents the result of an ADC, encapsulating data and metadata
|
Represents the result of an ADC, encapsulating data and metadata
|
||||||
for processing, visualization, and export.
|
for processing, visualization, and export.
|
||||||
@@ -50,6 +51,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.ylabel = "Samples [Digits]"
|
self.ylabel = "Samples [Digits]"
|
||||||
self.lock=threading.RLock()
|
self.lock=threading.RLock()
|
||||||
self.nChannels = 0
|
self.nChannels = 0
|
||||||
|
self.is_clipped = False
|
||||||
|
|
||||||
# using no argument for initialization
|
# using no argument for initialization
|
||||||
if (x is None) and (y is None) and (index is None) and (sampl_freq is None) and (desc is None) and (job_id is None) and (job_date is None):
|
if (x is None) and (y is None) and (index is None) and (sampl_freq is None) and (desc is None) and (job_id is None) and (job_date is None):
|
||||||
@@ -107,6 +109,44 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
return self.cont_data
|
return self.cont_data
|
||||||
|
|
||||||
|
|
||||||
|
def check_clipping(self, bit_depth=None):
|
||||||
|
"""
|
||||||
|
Check if any data in y is clipped.
|
||||||
|
For a 14-bit ADC, clipping occurs at 2**13 (8192) or 2**13-1 (8191).
|
||||||
|
In general, for a bit_depth-bit signed ADC, limits are 2**(bit_depth-1) and -(2**(bit_depth-1)).
|
||||||
|
"""
|
||||||
|
if not self.contains_data():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if bit_depth is None:
|
||||||
|
# try to get bit_depth from description
|
||||||
|
if hasattr(self, "description") and self.description is not None:
|
||||||
|
if "adc_bit_depth" in self.description:
|
||||||
|
try:
|
||||||
|
bit_depth = int(self.description["adc_bit_depth"])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
bit_depth = ADC_Result.default_bit_depth
|
||||||
|
else:
|
||||||
|
bit_depth = ADC_Result.default_bit_depth
|
||||||
|
else:
|
||||||
|
bit_depth = ADC_Result.default_bit_depth
|
||||||
|
|
||||||
|
limit_pos = 2**(bit_depth-1) - 1
|
||||||
|
limit_neg = -2**(bit_depth-1)
|
||||||
|
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
self.is_clipped = False
|
||||||
|
for ch_data in self.y:
|
||||||
|
if numpy.any(ch_data >= limit_pos) or numpy.any(ch_data <= limit_neg):
|
||||||
|
self.is_clipped = True
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
return self.is_clipped
|
||||||
|
|
||||||
|
|
||||||
def add_sample_space(self, samples):
|
def add_sample_space(self, samples):
|
||||||
"Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
|
"Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
|
||||||
|
|
||||||
@@ -143,7 +183,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
for i in range(self.get_number_of_channels()):
|
for i in range(self.get_number_of_channels()):
|
||||||
tmp_y.append(self.y[i][start:end+1].copy())
|
tmp_y.append(self.y[i][start:end+1].copy())
|
||||||
|
|
||||||
r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date)
|
r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description.copy() if self.description is not None else None, job_id = self.job_id, job_date = self.job_date)
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@@ -536,6 +576,8 @@ def read_from_hdf(hdf_node):
|
|||||||
|
|
||||||
# job id and x,y titles are missing
|
# job id and x,y titles are missing
|
||||||
adc=ADC_Result()
|
adc=ADC_Result()
|
||||||
|
if hdf_node._v_title:
|
||||||
|
adc.set_title(hdf_node._v_title)
|
||||||
# populate description dictionary
|
# populate description dictionary
|
||||||
adc.description={}
|
adc.description={}
|
||||||
for attrname in hdf_node._v_attrs._v_attrnamesuser:
|
for attrname in hdf_node._v_attrs._v_attrnamesuser:
|
||||||
@@ -573,4 +615,5 @@ def read_from_hdf(hdf_node):
|
|||||||
for ch in range(adc_data.shape[1]):
|
for ch in range(adc_data.shape[1]):
|
||||||
adc.y.append(adc_data[:,ch])
|
adc.y.append(adc_data[:,ch])
|
||||||
|
|
||||||
|
adc.check_clipping()
|
||||||
return adc
|
return adc
|
||||||
@@ -61,6 +61,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.xlabel = "Time / s"
|
self.xlabel = "Time / s"
|
||||||
self.ylabel = "Avg. Samples [Digits]"
|
self.ylabel = "Avg. Samples [Digits]"
|
||||||
self.lock=threading.RLock()
|
self.lock=threading.RLock()
|
||||||
|
self.is_clipped = False
|
||||||
|
|
||||||
self.common_descriptions=None
|
self.common_descriptions=None
|
||||||
self.time_period=[]
|
self.time_period=[]
|
||||||
@@ -117,9 +118,23 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
tmp_y.append(self.y[i][start:end+1])
|
tmp_y.append(self.y[i][start:end+1])
|
||||||
|
|
||||||
r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error)
|
r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error)
|
||||||
|
if self.uses_statistics():
|
||||||
|
r.y_square = []
|
||||||
|
for i in range(self.get_number_of_channels()):
|
||||||
|
r.y_square.append(self.y_square[i][start:end+1])
|
||||||
|
if self.common_descriptions is not None:
|
||||||
|
r.common_descriptions = self.common_descriptions.copy()
|
||||||
|
r.time_period = self.time_period[:]
|
||||||
|
r.job_ids = self.job_ids.copy()
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def get_result_by_index(self, index):
|
||||||
|
return self.get_accu_by_index(index)
|
||||||
|
|
||||||
|
def get_Result_by_index(self, index):
|
||||||
|
return self.get_accu_by_index(index)
|
||||||
|
|
||||||
def get_ysquare(self, channel):
|
def get_ysquare(self, channel):
|
||||||
if self.uses_statistics():
|
if self.uses_statistics():
|
||||||
try:
|
try:
|
||||||
@@ -616,6 +631,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
|
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
|
||||||
else:
|
else:
|
||||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
|
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
|
||||||
|
r.is_clipped = self.is_clipped
|
||||||
r.job_id = self.job_id
|
r.job_id = self.job_id
|
||||||
r.job_ids = self.job_ids.copy()
|
r.job_ids = self.job_ids.copy()
|
||||||
r.time_period = self.time_period[:] if self.time_period is not None else None
|
r.time_period = self.time_period[:] if self.time_period is not None else None
|
||||||
@@ -645,6 +661,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = 1, index = other.index, sampl_freq = other.sampling_rate, error = True)
|
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = 1, index = other.index, sampl_freq = other.sampling_rate, error = True)
|
||||||
else:
|
else:
|
||||||
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False)
|
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False)
|
||||||
|
if hasattr(other, "is_clipped"):
|
||||||
|
r.is_clipped = other.is_clipped
|
||||||
r.time_period=[other.job_date,other.job_date]
|
r.time_period=[other.job_date,other.job_date]
|
||||||
r.job_id = other.job_id
|
r.job_id = other.job_id
|
||||||
r.common_descriptions=other.description.copy()
|
r.common_descriptions=other.description.copy()
|
||||||
@@ -678,6 +696,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = True)
|
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = True)
|
||||||
else:
|
else:
|
||||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False)
|
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False)
|
||||||
|
r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped)
|
||||||
r.time_period=[min(self.time_period[0],other.job_date),
|
r.time_period=[min(self.time_period[0],other.job_date),
|
||||||
max(self.time_period[1],other.job_date)]
|
max(self.time_period[1],other.job_date)]
|
||||||
r.job_id = other.job_id
|
r.job_id = other.job_id
|
||||||
@@ -744,13 +763,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
|
|
||||||
for i in range(self.get_number_of_channels()):
|
for i in range(self.get_number_of_channels()):
|
||||||
tmp_y.append(self.y[i] + other.y[i])
|
tmp_y.append(self.y[i] + other.y[i])
|
||||||
tmp_ysquare.append(self.y_square[i] + other.y_square[i])
|
if self.uses_statistics():
|
||||||
|
tmp_ysquare.append(self.y_square[i] + other.y_square[i])
|
||||||
|
|
||||||
if self.uses_statistics():
|
if self.uses_statistics():
|
||||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = True)
|
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = True)
|
||||||
else:
|
else:
|
||||||
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
|
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
|
||||||
|
r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped)
|
||||||
r.time_period=[min(self.time_period[0],other.time_period[0]),
|
r.time_period=[min(self.time_period[0],other.time_period[0]),
|
||||||
max(self.time_period[1],other.time_period[1])]
|
max(self.time_period[1],other.time_period[1])]
|
||||||
r.job_id = other.job_id
|
r.job_id = other.job_id
|
||||||
@@ -954,6 +974,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.set_title(self.__title_pattern % self.n)
|
self.set_title(self.__title_pattern % self.n)
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
|
if hasattr(other, "is_clipped"):
|
||||||
|
self.is_clipped = other.is_clipped
|
||||||
self.time_period=[other.job_date,other.job_date]
|
self.time_period=[other.job_date,other.job_date]
|
||||||
self.job_id = other.job_id # added by Oleg Petrov
|
self.job_id = other.job_id # added by Oleg Petrov
|
||||||
self.job_ids[other.job_id] = other.get_job_date()
|
self.job_ids[other.job_id] = other.get_job_date()
|
||||||
@@ -967,14 +989,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.lock.acquire()
|
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 diffrent 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):
|
||||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of samples")
|
raise ValueError("Accumulation: You can't add ADC-Results with different number of samples")
|
||||||
if len(self.y) != other.get_number_of_channels():
|
if len(self.y) != other.get_number_of_channels():
|
||||||
raise ValueError("Accumulation: You cant add ADC-Results with diffrent number of channels")
|
raise ValueError("Accumulation: You can't add ADC-Results with different number of channels")
|
||||||
for i in range(len(self.index)):
|
for i in range(len(self.index)):
|
||||||
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 diffrent indexing")
|
raise ValueError("Accumulation: You can't add ADC-Results with different indexing")
|
||||||
|
|
||||||
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]
|
||||||
@@ -982,6 +1004,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.y_square[i] += numpy.array(other.y[i], dtype="float32") ** 2
|
self.y_square[i] += numpy.array(other.y[i], dtype="float32") ** 2
|
||||||
|
|
||||||
self.n += 1
|
self.n += 1
|
||||||
|
if hasattr(other, "is_clipped"):
|
||||||
|
self.is_clipped = self.is_clipped or other.is_clipped
|
||||||
self.time_period=[min(self.time_period[0],other.job_date),
|
self.time_period=[min(self.time_period[0],other.job_date),
|
||||||
max(self.time_period[1],other.job_date)]
|
max(self.time_period[1],other.job_date)]
|
||||||
|
|
||||||
@@ -1020,10 +1044,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.y_square.append(self.y[i] ** 2)
|
self.y_square.append(self.y[i] ** 2)
|
||||||
|
|
||||||
self.set_title(self.__title_pattern % self.n)
|
self.set_title(self.__title_pattern % self.n)
|
||||||
self.common_descriptions=other.common_desriptions.copy()
|
if hasattr(other, "is_clipped"):
|
||||||
|
self.is_clipped = other.is_clipped
|
||||||
|
self.common_descriptions=other.common_descriptions.copy()
|
||||||
self.time_period=other.time_period[:]
|
self.time_period=other.time_period[:]
|
||||||
self.job_id = other.job_id # added by Oleg Petrov
|
self.job_id = other.job_id # added by Oleg Petrov
|
||||||
self.job_ids += other.job_ids
|
self.job_ids.update(other.job_ids)
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
@@ -1049,9 +1075,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
self.y_square[i] += other.y_square[i]
|
self.y_square[i] += other.y_square[i]
|
||||||
|
|
||||||
self.n += other.n
|
self.n += other.n
|
||||||
|
if hasattr(other, "is_clipped"):
|
||||||
|
self.is_clipped = self.is_clipped or other.is_clipped
|
||||||
self.time_period=[min(self.time_period[0],other.time_period[0]),
|
self.time_period=[min(self.time_period[0],other.time_period[0]),
|
||||||
max(self.time_period[1],other.time_period[1])]
|
max(self.time_period[1],other.time_period[1])]
|
||||||
self.job_id = other.job_id
|
self.job_id = other.job_id
|
||||||
|
self.job_ids.update(other.job_ids)
|
||||||
# Removes mismatched common description keys
|
# Removes mismatched common description keys
|
||||||
if self.common_descriptions is not None and other.common_descriptions is not None:
|
if self.common_descriptions is not None and other.common_descriptions is not None:
|
||||||
# Get all keys that exist in both dictionaries
|
# Get all keys that exist in both dictionaries
|
||||||
@@ -1131,26 +1160,66 @@ def read_from_hdf(hdf_node):
|
|||||||
earliest_time=None
|
earliest_time=None
|
||||||
if "earliest_time" in dir(hdf_node._v_attrs):
|
if "earliest_time" in dir(hdf_node._v_attrs):
|
||||||
timestring=hdf_node._v_attrs.__getattr__("earliest_time")
|
timestring=hdf_node._v_attrs.__getattr__("earliest_time")
|
||||||
earliest_time=datetime.datetime(int(timestring[:4]), # year
|
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
|
||||||
int(timestring[4:6]), # month
|
if '-' in timestring:
|
||||||
int(timestring[6:8]), # day
|
# New format: "2026-07-05 19:13:59.162"
|
||||||
int(timestring[9:11]), # hour
|
try:
|
||||||
int(timestring[12:14]), # minute
|
earliest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
|
||||||
int(timestring[15:17]), # second
|
except ValueError:
|
||||||
int(timestring[18:21])*1000 # microsecond
|
# Fallback to manual parsing
|
||||||
)
|
parts = timestring.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
date_parts = parts[0].split('-')
|
||||||
|
time_parts = parts[1].split(':')
|
||||||
|
if len(date_parts) == 3 and len(time_parts) >= 2:
|
||||||
|
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
|
||||||
|
hour, minute = int(time_parts[0]), int(time_parts[1])
|
||||||
|
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
|
||||||
|
second = int(second_parts[0])
|
||||||
|
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
|
||||||
|
earliest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
|
||||||
|
else:
|
||||||
|
# Old format: "20260705 19:13:59.162"
|
||||||
|
earliest_time=datetime.datetime(int(timestring[:4]), # year
|
||||||
|
int(timestring[4:6]), # month
|
||||||
|
int(timestring[6:8]), # day
|
||||||
|
int(timestring[9:11]), # hour
|
||||||
|
int(timestring[12:14]), # minute
|
||||||
|
int(timestring[15:17]), # second
|
||||||
|
int(timestring[18:21])*1000 # microsecond
|
||||||
|
)
|
||||||
|
|
||||||
oldest_time=None
|
oldest_time=None
|
||||||
if "oldest_time" in dir(hdf_node._v_attrs):
|
if "oldest_time" in dir(hdf_node._v_attrs):
|
||||||
timestring=hdf_node._v_attrs.__getattr__("oldest_time")
|
timestring=hdf_node._v_attrs.__getattr__("oldest_time")
|
||||||
oldest_time=datetime.datetime(int(timestring[:4]), # year
|
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
|
||||||
int(timestring[4:6]), # month
|
if '-' in timestring:
|
||||||
int(timestring[6:8]), # day
|
# New format: "2026-07-05 19:13:59.162"
|
||||||
int(timestring[9:11]), # hour
|
try:
|
||||||
int(timestring[12:14]), # minute
|
oldest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
|
||||||
int(timestring[15:17]), # second
|
except ValueError:
|
||||||
int(timestring[18:21])*1000 # microsecond
|
# Fallback to manual parsing
|
||||||
)
|
parts = timestring.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
date_parts = parts[0].split('-')
|
||||||
|
time_parts = parts[1].split(':')
|
||||||
|
if len(date_parts) == 3 and len(time_parts) >= 2:
|
||||||
|
year, month, day = int(date_parts[0]), int(date_parts[1]), int(date_parts[2])
|
||||||
|
hour, minute = int(time_parts[0]), int(time_parts[1])
|
||||||
|
second_parts = time_parts[2].split('.') if len(time_parts) > 2 else ['0']
|
||||||
|
second = int(second_parts[0])
|
||||||
|
microsecond = int(second_parts[1].ljust(6, '0')[:6]) if len(second_parts) > 1 else 0
|
||||||
|
oldest_time=datetime.datetime(year, month, day, hour, minute, second, microsecond)
|
||||||
|
else:
|
||||||
|
# Old format: "20260705 19:13:59.162"
|
||||||
|
oldest_time=datetime.datetime(int(timestring[:4]), # year
|
||||||
|
int(timestring[4:6]), # month
|
||||||
|
int(timestring[6:8]), # day
|
||||||
|
int(timestring[9:11]), # hour
|
||||||
|
int(timestring[12:14]), # minute
|
||||||
|
int(timestring[15:17]), # second
|
||||||
|
int(timestring[18:21])*1000 # microsecond
|
||||||
|
)
|
||||||
|
|
||||||
if oldest_time is None or earliest_time is None:
|
if oldest_time is None or earliest_time is None:
|
||||||
accu.time_period=None
|
accu.time_period=None
|
||||||
@@ -1167,6 +1236,8 @@ def read_from_hdf(hdf_node):
|
|||||||
accu.index.append((r["start"],r["start"]+r["length"]-1))
|
accu.index.append((r["start"],r["start"]+r["length"]-1))
|
||||||
accu.n=r["number"]
|
accu.n=r["number"]
|
||||||
accu.sampling_rate=1.0/r["dwelltime"]
|
accu.sampling_rate=1.0/r["dwelltime"]
|
||||||
|
|
||||||
|
accu.set_title("Accumulation: n = %d" % accu.n)
|
||||||
|
|
||||||
# now really belief there are no data
|
# now really belief there are no data
|
||||||
if len(accu.index)==0 or accu.n==0:
|
if len(accu.index)==0 or accu.n==0:
|
||||||
@@ -1181,7 +1252,7 @@ def read_from_hdf(hdf_node):
|
|||||||
accu.y_square=[]
|
accu.y_square=[]
|
||||||
accu.use_error=False
|
accu.use_error=False
|
||||||
|
|
||||||
for ch in range(accu_data.shape[1]/2):
|
for ch in range(accu_data.shape[1] // 2):
|
||||||
accu.y.append(accu_data[:,ch*2]*accu.n)
|
accu.y.append(accu_data[:,ch*2]*accu.n)
|
||||||
if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0):
|
if accu.n<2 or numpy.all(accu_data[:,ch*2+1]==0.0):
|
||||||
accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32"))
|
accu.y_square.append(numpy.zeros((accu_data.shape[0]) ,dtype="float32"))
|
||||||
|
|||||||
+339
-13
@@ -7,6 +7,7 @@ import collections
|
|||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import io
|
import io
|
||||||
|
import numpy
|
||||||
|
|
||||||
class DataPool(collections.abc.MutableMapping):
|
class DataPool(collections.abc.MutableMapping):
|
||||||
"""
|
"""
|
||||||
@@ -48,6 +49,7 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
self.__mydict={}
|
self.__mydict={}
|
||||||
self.__dictlock=threading.Lock()
|
self.__dictlock=threading.Lock()
|
||||||
self.__registered_listeners=[]
|
self.__registered_listeners=[]
|
||||||
|
self.__dirty_keys=set()
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
try:
|
try:
|
||||||
@@ -64,6 +66,7 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
else:
|
else:
|
||||||
e=DataPool.Event(DataPool.Event.new_key, name,self)
|
e=DataPool.Event(DataPool.Event.new_key, name,self)
|
||||||
self.__mydict[name]=value
|
self.__mydict[name]=value
|
||||||
|
self.__dirty_keys.add(name)
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self.__dictlock.release()
|
||||||
self.__send_event(e)
|
self.__send_event(e)
|
||||||
@@ -73,6 +76,7 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
try:
|
try:
|
||||||
self.__dictlock.acquire()
|
self.__dictlock.acquire()
|
||||||
del self.__mydict[name]
|
del self.__mydict[name]
|
||||||
|
self.__dirty_keys.add(name)
|
||||||
finally:
|
finally:
|
||||||
self.__dictlock.release()
|
self.__dictlock.release()
|
||||||
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
|
self.__send_event(DataPool.Event(DataPool.Event.deleted_key,name,self))
|
||||||
@@ -106,18 +110,74 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
self.__send_event(DataPool.Event(DataPool.Event.destroy))
|
self.__send_event(DataPool.Event(DataPool.Event.destroy))
|
||||||
self.__registered_listeners=None
|
self.__registered_listeners=None
|
||||||
|
|
||||||
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
|
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None, incremental=False):
|
||||||
if type(hdffile) is bytes:
|
|
||||||
|
"""
|
||||||
|
Writes the content of a DataPool object to an HDF5 file using the `tables` library.
|
||||||
|
|
||||||
|
This method serializes the entire content of the data pool into an HDF5 file, creating
|
||||||
|
appropriate groups and arrays based on the keys and values stored in the data pool.
|
||||||
|
Nested storage paths are supported, and group or array names are
|
||||||
|
converted into valid HDF5 names. Paths are prepended with "dir_", results with "dict_".
|
||||||
|
If duplicate names arise, extensions (like `_001`, `_002`)
|
||||||
|
are appended to ensure uniqueness.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
hdffile : str | bytes | tables.File
|
||||||
|
Path to the HDF5 file or an already open `tables.File` object where the
|
||||||
|
data should be written. Strings or bytes are interpreted as file paths.
|
||||||
|
|
||||||
|
where : str, optional
|
||||||
|
The root HDF5 group path under which the data pool will be stored.
|
||||||
|
Defaults to "/".
|
||||||
|
|
||||||
|
name : str, optional
|
||||||
|
The name of the root HDF5 group under which the data pool is saved.
|
||||||
|
Defaults to "data_pool".
|
||||||
|
|
||||||
|
complib : str, optional
|
||||||
|
Compression library to be used when creating HDF5 arrays. Optional, defaults
|
||||||
|
to None, meaning no compression is applied.
|
||||||
|
|
||||||
|
complevel : int, optional
|
||||||
|
Compression level to use when applying compression. Has no effect if `complib`
|
||||||
|
is set to None.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception
|
||||||
|
If `hdffile` is neither a string nor a valid `tables.File` object.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- This method assumes that the content of the data pool does not change during the
|
||||||
|
write operation.
|
||||||
|
- Keys in the data pool starting with "__" (double underscores) are skipped.
|
||||||
|
- If a key contains nested paths (e.g., "group1/subgroup2/item"), intermediate
|
||||||
|
groups are created as necessary.
|
||||||
|
- Group or array names are converted to valid HDF5 names, and name conflicts
|
||||||
|
are resolved by appending extensions (e.g., "_001").
|
||||||
|
- Errors during the writing process are logged to the console, and traceback
|
||||||
|
information is displayed for debugging purposes.
|
||||||
|
"""
|
||||||
|
if isinstance(hdffile, (bytes, str)):
|
||||||
dump_file=tables.open_file(hdffile, mode="a")
|
dump_file=tables.open_file(hdffile, mode="a")
|
||||||
elif isinstance(hdffile,tables.File):
|
elif isinstance(hdffile,tables.File):
|
||||||
dump_file=hdffile
|
dump_file=hdffile
|
||||||
else:
|
else:
|
||||||
raise Exception("expecting hdffile or string")
|
raise Exception("expecting hdffile or string")
|
||||||
|
|
||||||
dump_group=dump_file.create_group(where, name, "DAMARIS data pool")
|
if where in dump_file and name in dump_file.get_node(where):
|
||||||
|
dump_group = dump_file.get_node(where, name)
|
||||||
|
else:
|
||||||
|
dump_group = dump_file.create_group(where, name, "DAMARIS data pool")
|
||||||
|
|
||||||
self.__dictlock.acquire()
|
self.__dictlock.acquire()
|
||||||
dict_keys=list(self.__mydict.keys())
|
if incremental:
|
||||||
|
dict_keys=list(self.__dirty_keys)
|
||||||
|
else:
|
||||||
|
# Full sync: current keys + any other dirty keys (e.g. deletions)
|
||||||
|
dict_keys=list(set(self.__mydict.keys()) | self.__dirty_keys)
|
||||||
self.__dictlock.release()
|
self.__dictlock.release()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for key in dict_keys:
|
for key in dict_keys:
|
||||||
if key.startswith("__"):
|
if key.startswith("__"):
|
||||||
@@ -142,21 +202,37 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
|
|
||||||
# convert last part of key to a valid name
|
# convert last part of key to a valid name
|
||||||
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table)
|
group_keyname="dict_"+str(namelist[-1]).translate(DataPool.translation_table)
|
||||||
# avoid double names by adding number extension
|
|
||||||
|
# find existing node for this key
|
||||||
if group_keyname in dump_dir:
|
if group_keyname in dump_dir:
|
||||||
extension_count=0
|
if dump_dir._v_children[group_keyname]._v_title == key:
|
||||||
while group_keyname+"_%03d"%extension_count in dump_dir:
|
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
|
||||||
extension_count+=1
|
else:
|
||||||
group_keyname+="_%03d"%extension_count
|
extension_count = 0
|
||||||
|
while group_keyname + "_%03d" % extension_count in dump_dir:
|
||||||
|
suffix_name = group_keyname + "_%03d" % extension_count
|
||||||
|
if dump_dir._v_children[suffix_name]._v_title == key:
|
||||||
|
dump_file.remove_node(dump_dir, suffix_name, recursive=True)
|
||||||
|
group_keyname = suffix_name
|
||||||
|
break
|
||||||
|
extension_count += 1
|
||||||
|
else:
|
||||||
|
# truly new key with collision
|
||||||
|
group_keyname += "_%03d" % extension_count
|
||||||
|
|
||||||
self.__dictlock.acquire()
|
self.__dictlock.acquire()
|
||||||
if key not in self.__mydict:
|
if key not in self.__mydict:
|
||||||
# outdated ...
|
# Key was deleted
|
||||||
|
if key in self.__dirty_keys:
|
||||||
|
self.__dirty_keys.remove(key)
|
||||||
self.__dictlock.release()
|
self.__dictlock.release()
|
||||||
continue
|
continue
|
||||||
value=self.__mydict[key]
|
value=self.__mydict[key]
|
||||||
|
if key in self.__dirty_keys:
|
||||||
|
self.__dirty_keys.remove(key)
|
||||||
self.__dictlock.release()
|
self.__dictlock.release()
|
||||||
# now write data, assuming, the object is constant during write operation
|
# now write data, assuming, the object is constant during write operation
|
||||||
if "write_to_hdf" in dir(value):
|
if hasattr(value, "write_to_hdf"):
|
||||||
try:
|
try:
|
||||||
value.write_to_hdf(hdffile=dump_file,
|
value.write_to_hdf(hdffile=dump_file,
|
||||||
where=dump_dir,
|
where=dump_dir,
|
||||||
@@ -170,16 +246,266 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
|
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
|
||||||
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
|
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
|
||||||
traceback_file=None
|
traceback_file=None
|
||||||
|
elif isinstance(value, (numpy.ndarray, int, float, str, bytes)):
|
||||||
|
try:
|
||||||
|
obj_to_save = value
|
||||||
|
if isinstance(value, str):
|
||||||
|
obj_to_save = value.encode('utf-8')
|
||||||
|
dump_file.create_array(dump_dir,
|
||||||
|
name=group_keyname,
|
||||||
|
obj=obj_to_save,
|
||||||
|
title=key)
|
||||||
|
except Exception as e:
|
||||||
|
print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
|
||||||
else:
|
else:
|
||||||
print("don't know how to store data_pool[\"%s\"]"%key)
|
print("don't know how to store data_pool[\"%s\"] (type: %s)"%(key, type(value)))
|
||||||
value=None
|
value=None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
dump_group=None
|
dump_group=None
|
||||||
if type(hdffile) is bytes:
|
if isinstance(hdffile, (bytes, str)):
|
||||||
dump_file.close()
|
dump_file.close()
|
||||||
dump_file=None
|
dump_file=None
|
||||||
|
|
||||||
|
def read_from_hdf(self, hdffile, where="/data_pool"):
|
||||||
|
"""
|
||||||
|
Read specified data from HDF5 file and populate the DataPool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- hdffile: HDF5 file object or filename (bytes/str)
|
||||||
|
- where: Location within HDF5 file to read from (default: "/data_pool")
|
||||||
|
"""
|
||||||
|
# Handle both filename and file object
|
||||||
|
if type(hdffile) is bytes or isinstance(hdffile, str):
|
||||||
|
hdf_file = tables.open_file(hdffile, mode="r")
|
||||||
|
close_file = True
|
||||||
|
elif isinstance(hdffile, tables.File):
|
||||||
|
hdf_file = hdffile
|
||||||
|
close_file = False
|
||||||
|
else:
|
||||||
|
raise Exception("expecting hdffile or string")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Navigate to the data pool group
|
||||||
|
if where not in hdf_file:
|
||||||
|
raise Exception(f"HDF5 location '{where}' not found")
|
||||||
|
|
||||||
|
data_pool_group = hdf_file.get_node(where)
|
||||||
|
|
||||||
|
# Recursively process the group structure
|
||||||
|
self._read_hdf_group(data_pool_group, "")
|
||||||
|
|
||||||
|
# Clear dirty keys as they are now in sync with HDF5
|
||||||
|
self.__dictlock.acquire()
|
||||||
|
self.__dirty_keys.clear()
|
||||||
|
self.__dictlock.release()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if close_file:
|
||||||
|
hdf_file.close()
|
||||||
|
|
||||||
|
def _read_hdf_group(self, group, path_prefix):
|
||||||
|
"""
|
||||||
|
Recursively read HDF5 group and populate DataPool.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- group: HDF5 group object
|
||||||
|
- path_prefix: Current path in DataPool hierarchy
|
||||||
|
"""
|
||||||
|
# Process all nodes in this group
|
||||||
|
for node in group:
|
||||||
|
# Get the node name (last part of the path)
|
||||||
|
node_name = node._v_name
|
||||||
|
|
||||||
|
# Try to get the original name from title
|
||||||
|
title = node._v_title
|
||||||
|
|
||||||
|
if title:
|
||||||
|
# For DAMARIS objects, the title is usually the full path
|
||||||
|
# For intermediate groups, it's just the part name
|
||||||
|
if "/" in title:
|
||||||
|
full_path = title
|
||||||
|
else:
|
||||||
|
if path_prefix:
|
||||||
|
full_path = f"{path_prefix}/{title}"
|
||||||
|
else:
|
||||||
|
full_path = title
|
||||||
|
else:
|
||||||
|
# Strip dir_ and dict_ prefixes from node name
|
||||||
|
clean_name = self._clean_hdf_name(node_name)
|
||||||
|
|
||||||
|
# Build the full path in DataPool hierarchy
|
||||||
|
if path_prefix:
|
||||||
|
full_path = f"{path_prefix}/{clean_name}"
|
||||||
|
else:
|
||||||
|
full_path = clean_name
|
||||||
|
|
||||||
|
full_path = full_path.replace("//", "/")
|
||||||
|
|
||||||
|
if isinstance(node, (tables.Group, tables.Table)):
|
||||||
|
# Check if this is a known DAMARIS object type
|
||||||
|
if hasattr(node._v_attrs, 'damaris_type'):
|
||||||
|
damaris_type = node._v_attrs.damaris_type
|
||||||
|
|
||||||
|
# Handle different DAMARIS object types
|
||||||
|
if damaris_type == "Accumulation":
|
||||||
|
from damaris.data.Accumulation import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
elif damaris_type == "ADC_Result":
|
||||||
|
from damaris.data.ADC_Result import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
elif damaris_type == "MeasurementResult":
|
||||||
|
from damaris.data.MeasurementResult import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
elif damaris_type == "TemperatureResult":
|
||||||
|
from damaris.data.Temperature import read_from_hdf
|
||||||
|
obj = read_from_hdf(node)
|
||||||
|
if obj is not None:
|
||||||
|
self[full_path] = obj
|
||||||
|
|
||||||
|
# Skip further processing of this group since we handled it
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Recursively process sub-groups
|
||||||
|
if isinstance(node, tables.Group):
|
||||||
|
self._read_hdf_group(node, full_path)
|
||||||
|
|
||||||
|
elif isinstance(node, (tables.Array, tables.CArray, tables.EArray, tables.Table)):
|
||||||
|
# Handle simple array data
|
||||||
|
try:
|
||||||
|
data = node.read()
|
||||||
|
# If it's a scalar array of bytes, decode it to string
|
||||||
|
if hasattr(data, 'decode'):
|
||||||
|
try:
|
||||||
|
data = data.decode('utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif isinstance(data, numpy.ndarray) and data.dtype.kind in ('S', 'V'):
|
||||||
|
# For numpy byte arrays, try to decode if they are scalar or small
|
||||||
|
if data.size == 1:
|
||||||
|
try:
|
||||||
|
decoded = data.item().decode('utf-8')
|
||||||
|
data = decoded
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self[full_path] = data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read array {full_path}: {e}")
|
||||||
|
|
||||||
|
# Note: Other node types (Table, etc.) would be handled here as needed
|
||||||
|
|
||||||
|
def _clean_hdf_name(self, name):
|
||||||
|
"""
|
||||||
|
Clean HDF5 node name by removing dir_ and dict_ prefixes.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- name: HDF5 node name (may have dir_ or dict_ prefix)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Cleaned name with prefixes removed
|
||||||
|
"""
|
||||||
|
if name.startswith("dir_"):
|
||||||
|
return name[4:] # Remove "dir_" prefix
|
||||||
|
elif name.startswith("dict_"):
|
||||||
|
return name[5:] # Remove "dict_" prefix
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_hdf5(cls, filename):
|
||||||
|
"""
|
||||||
|
Load a complete DAMARIS HDF5 file and return DataPool and metadata, including scripts.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- filename: Path to HDF5 file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- tuple: (DataPool instance, metadata dict)
|
||||||
|
|
||||||
|
metadata dict contains:
|
||||||
|
- 'scripts': dict with experiment scripts and spool info
|
||||||
|
- 'log': log data if available
|
||||||
|
- 'timeline': timeline data if available
|
||||||
|
"""
|
||||||
|
# Validate file
|
||||||
|
if not tables.is_pytables_file(filename):
|
||||||
|
raise Exception(f"File {filename} is not a valid PyTables HDF5 file")
|
||||||
|
|
||||||
|
# Create DataPool and load data
|
||||||
|
data_pool = cls()
|
||||||
|
|
||||||
|
try:
|
||||||
|
hdf_file = tables.open_file(filename, mode="r")
|
||||||
|
|
||||||
|
# Load main data pool
|
||||||
|
if "/data_pool" in hdf_file:
|
||||||
|
data_pool.read_from_hdf(hdf_file, where="/data_pool")
|
||||||
|
|
||||||
|
# Extract metadata
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
# Load scripts
|
||||||
|
if "/scripts" in hdf_file:
|
||||||
|
scripts_group = hdf_file.get_node("/scripts")
|
||||||
|
scripts_data = {}
|
||||||
|
# Use _v_children to get node names safely
|
||||||
|
if hasattr(scripts_group, '_v_children'):
|
||||||
|
for node_name, node in scripts_group._v_children.items():
|
||||||
|
try:
|
||||||
|
if hasattr(node, 'read'):
|
||||||
|
data = node.read()
|
||||||
|
# Handle numpy string arrays and other types
|
||||||
|
if hasattr(data, 'decode'):
|
||||||
|
# bytes or numpy bytes
|
||||||
|
scripts_data[node_name] = data.decode('utf-8', errors='replace')
|
||||||
|
elif hasattr(data, 'tolist'):
|
||||||
|
# numpy array
|
||||||
|
scripts_data[node_name] = str(data.tolist())
|
||||||
|
elif isinstance(data, (str, bytes)):
|
||||||
|
# string or bytes
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
scripts_data[node_name] = data.decode('utf-8', errors='replace')
|
||||||
|
else:
|
||||||
|
scripts_data[node_name] = data
|
||||||
|
else:
|
||||||
|
scripts_data[node_name] = str(data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read script {node_name}: {e}")
|
||||||
|
metadata['scripts'] = scripts_data
|
||||||
|
|
||||||
|
# Load log
|
||||||
|
if "/log" in hdf_file:
|
||||||
|
try:
|
||||||
|
log_node = hdf_file.get_node("/log")
|
||||||
|
metadata['log'] = log_node.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read log: {e}")
|
||||||
|
|
||||||
|
# Load timeline
|
||||||
|
if "/timeline" in hdf_file:
|
||||||
|
try:
|
||||||
|
timeline_node = hdf_file.get_node("/timeline")
|
||||||
|
metadata['timeline'] = timeline_node.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read timeline: {e}")
|
||||||
|
|
||||||
|
return data_pool, metadata
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Failed to load HDF5 file {filename}: {e}")
|
||||||
|
finally:
|
||||||
|
if 'hdf_file' in locals():
|
||||||
|
hdf_file.close()
|
||||||
|
|
||||||
def register_listener(self, listening_function):
|
def register_listener(self, listening_function):
|
||||||
self.__registered_listeners.append(listening_function)
|
self.__registered_listeners.append(listening_function)
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
|||||||
|
|
||||||
def uses_statistics(self):
|
def uses_statistics(self):
|
||||||
"""
|
"""
|
||||||
drawable interface method, returns True
|
drawable interface method returns True
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -203,10 +203,11 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
|||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
suitable for further processing
|
suitable for further processing
|
||||||
"""
|
"""
|
||||||
# write sorted
|
|
||||||
the_destination=destination
|
the_destination=destination
|
||||||
|
file_opened = False
|
||||||
if type(destination) in (str,):
|
if type(destination) in (str,):
|
||||||
the_destination=open(destination, "w")
|
the_destination=open(destination, "w")
|
||||||
|
file_opened = True
|
||||||
|
|
||||||
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
|
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
|
||||||
the_destination.write("# x y ysigma n\n")
|
the_destination.write("# x y ysigma n\n")
|
||||||
@@ -222,7 +223,8 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
|||||||
y.mean_error(),
|
y.mean_error(),
|
||||||
delimiter,
|
delimiter,
|
||||||
y.n))
|
y.n))
|
||||||
the_destination=None
|
if file_opened:
|
||||||
|
the_destination.close()
|
||||||
|
|
||||||
|
|
||||||
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from .Resultable import Resultable
|
from .Resultable import Resultable
|
||||||
from .Drawable import Drawable
|
from .Drawable import Drawable
|
||||||
|
import numpy
|
||||||
|
import threading
|
||||||
|
import tables
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# #
|
# #
|
||||||
@@ -15,17 +18,224 @@ from .Drawable import Drawable
|
|||||||
class TemperatureResult(Resultable, Drawable):
|
class TemperatureResult(Resultable, Drawable):
|
||||||
"""
|
"""
|
||||||
Specialised class of Resultable and Drawable
|
Specialised class of Resultable and Drawable
|
||||||
Contains recorded temperature data
|
Contains recorded temperature data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
x: List of timestamps in seconds.
|
||||||
|
y: List of temperature readings in Celsius.
|
||||||
|
setpoint: Optional list of setpoint temperatures in Celsius.
|
||||||
|
xlabel: Label for x-axis (default: "Time (s)").
|
||||||
|
ylabel: Label for y-axis (default: "Temperature (C)").
|
||||||
"""
|
"""
|
||||||
def __init__(self, x = None, y = None, desc = None, job_id = None, job_date = None):
|
def __init__(self, x = None, y = None, setpoint = None, desc = None, job_id = None, job_date = None):
|
||||||
Resultable.__init__(self)
|
Resultable.__init__(self)
|
||||||
Drawable.__init__(self)
|
Drawable.__init__(self)
|
||||||
|
|
||||||
|
# Set default labels
|
||||||
|
self.xlabel = "Time (s)"
|
||||||
|
self.ylabel = "Temperature (C)"
|
||||||
|
|
||||||
|
# Initialize data lists
|
||||||
|
self.x = [] if x is None else x
|
||||||
|
self.y = [] if y is None else y
|
||||||
|
self.setpoint = [] if setpoint is None else setpoint
|
||||||
|
|
||||||
|
# Set metadata if provided
|
||||||
|
if desc is not None:
|
||||||
|
self.description = desc
|
||||||
|
if job_id is not None:
|
||||||
|
self.job_id = job_id
|
||||||
|
if job_date is not None:
|
||||||
|
self.job_date = job_date
|
||||||
|
|
||||||
|
# Listener management for GUI updates
|
||||||
|
self.__listeners = []
|
||||||
|
self.__listener_lock = threading.Lock()
|
||||||
|
|
||||||
|
def get_number_of_channels(self):
|
||||||
|
"""Returns 1 for temperature data."""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def register_listener(self, listener):
|
||||||
|
"""Register a listener to be notified when data changes."""
|
||||||
|
with self.__listener_lock:
|
||||||
|
if listener not in self.__listeners:
|
||||||
|
self.__listeners.append(listener)
|
||||||
|
|
||||||
|
def unregister_listener(self, listener):
|
||||||
|
"""Unregister a listener."""
|
||||||
|
with self.__listener_lock:
|
||||||
|
if listener in self.__listeners:
|
||||||
|
self.__listeners.remove(listener)
|
||||||
|
|
||||||
|
def get_xdata(self):
|
||||||
|
"""Returns the x data (timestamps) as numpy array."""
|
||||||
|
if isinstance(self.x, numpy.ndarray):
|
||||||
|
return self.x
|
||||||
|
return numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
|
||||||
|
|
||||||
|
def get_ydata(self, channel=0):
|
||||||
|
"""Returns the y data (temperature readings) as numpy array."""
|
||||||
|
if channel == 0:
|
||||||
|
if isinstance(self.y, numpy.ndarray):
|
||||||
|
return self.y
|
||||||
|
return numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
|
||||||
|
return numpy.array([], dtype=float)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Returns the number of data points."""
|
||||||
|
return len(self.y) if self.y else 0
|
||||||
|
|
||||||
|
def uses_statistics(self):
|
||||||
|
"""Returns False as temperature data doesn't use statistics."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ready_for_drawing_error(self):
|
||||||
|
"""Returns False as temperature data doesn't have error bars."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_yerr(self, channel=0):
|
||||||
|
"""Returns empty error data."""
|
||||||
|
return numpy.array([], dtype=float)
|
||||||
|
|
||||||
|
def get_errorplotdata(self):
|
||||||
|
"""Returns empty error plot data."""
|
||||||
|
return [[], [], []]
|
||||||
|
|
||||||
|
def get_lineplotdata(self):
|
||||||
|
"""Returns line plot data (not used for temperature)."""
|
||||||
|
return [[], []]
|
||||||
|
|
||||||
|
def get_xmin(self):
|
||||||
|
"""Returns minimum of x."""
|
||||||
|
xdata = self.get_xdata()
|
||||||
|
return xdata.min() if len(xdata) > 0 else 0
|
||||||
|
|
||||||
|
def get_xmax(self):
|
||||||
|
"""Returns maximum of x."""
|
||||||
|
xdata = self.get_xdata()
|
||||||
|
return xdata.max() if len(xdata) > 0 else 0
|
||||||
|
|
||||||
|
def get_ymin(self):
|
||||||
|
"""Returns minimum of y."""
|
||||||
|
ydata = self.get_ydata(0)
|
||||||
|
return ydata.min() if len(ydata) > 0 else 0
|
||||||
|
|
||||||
|
def get_ymax(self):
|
||||||
|
"""Returns maximum of y."""
|
||||||
|
ydata = self.get_ydata(0)
|
||||||
|
return ydata.max() if len(ydata) > 0 else 0
|
||||||
|
|
||||||
|
def get_xminpos(self):
|
||||||
|
"""Returns smallest positive value of x."""
|
||||||
|
xdata = self.get_xdata()
|
||||||
|
mask = xdata > 0
|
||||||
|
if numpy.any(mask):
|
||||||
|
return xdata[mask].min()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_yminpos(self):
|
||||||
|
"""Returns smallest positive value of y."""
|
||||||
|
ydata = self.get_ydata(0)
|
||||||
|
mask = ydata > 0
|
||||||
|
if numpy.any(mask):
|
||||||
|
return ydata[mask].min()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||||
|
"""
|
||||||
|
Write the temperature result to an HDF5 group.
|
||||||
|
|
||||||
if (x is None) and (y is None) and (desc is None) and (job_id is None) and (job_date is None):
|
Stores x (timestamps), y (temperature readings), and setpoint arrays
|
||||||
pass
|
as an HDF5 table, with metadata saved as group attributes.
|
||||||
|
"""
|
||||||
|
h5_table_format = {
|
||||||
|
"x": tables.Float64Col(),
|
||||||
|
"y": tables.Float64Col(),
|
||||||
|
"setpoint": tables.Float64Col(),
|
||||||
|
}
|
||||||
|
filter = None
|
||||||
|
if complib is not None:
|
||||||
|
if complevel is None:
|
||||||
|
complevel = 9
|
||||||
|
filter = tables.Filters(complevel=complevel, complib=complib, shuffle=1)
|
||||||
|
|
||||||
elif (x is not None) and (y is not None) and (desc is not None) and (job_id is not None) and (job_date is not None):
|
with self.__listener_lock:
|
||||||
pass
|
xdata = numpy.array(self.x, dtype=float) if self.x else numpy.array([], dtype=float)
|
||||||
|
ydata = numpy.array(self.y, dtype=float) if self.y else numpy.array([], dtype=float)
|
||||||
|
spdata = numpy.array(self.setpoint, dtype=float) if self.setpoint else numpy.array([], dtype=float)
|
||||||
|
|
||||||
else:
|
# Use the length of the longest array; shorter ones are padded with 0
|
||||||
raise ValueError("Wrong usage of __init__!")
|
n = max(len(xdata), len(ydata), 1)
|
||||||
|
if len(xdata) < n:
|
||||||
|
xdata = numpy.pad(xdata, (0, n - len(xdata)), constant_values=0.0)
|
||||||
|
if len(ydata) < n:
|
||||||
|
ydata = numpy.pad(ydata, (0, n - len(ydata)), constant_values=0.0)
|
||||||
|
if len(spdata) < n:
|
||||||
|
spdata = numpy.pad(spdata, (0, n - len(spdata)), constant_values=0.0)
|
||||||
|
|
||||||
|
temp_table = hdffile.create_table(
|
||||||
|
where=where, name=name,
|
||||||
|
description=h5_table_format,
|
||||||
|
title=title,
|
||||||
|
filters=filter,
|
||||||
|
expectedrows=n,
|
||||||
|
)
|
||||||
|
temp_table.flavor = "numpy"
|
||||||
|
temp_table.attrs.damaris_type = "TemperatureResult"
|
||||||
|
temp_table.attrs.xlabel = self.xlabel
|
||||||
|
temp_table.attrs.ylabel = self.ylabel
|
||||||
|
|
||||||
|
if hasattr(self, "job_id") and self.job_id is not None:
|
||||||
|
temp_table.attrs.job_id = self.job_id
|
||||||
|
if hasattr(self, "job_date") and self.job_date is not None:
|
||||||
|
temp_table.attrs.time = "%04d%02d%02d %02d:%02d:%02d.%03d" % (
|
||||||
|
self.job_date.year,
|
||||||
|
self.job_date.month,
|
||||||
|
self.job_date.day,
|
||||||
|
self.job_date.hour,
|
||||||
|
self.job_date.minute,
|
||||||
|
self.job_date.second,
|
||||||
|
self.job_date.microsecond // 1000,
|
||||||
|
)
|
||||||
|
if hasattr(self, "description") and self.description is not None:
|
||||||
|
for (key, value) in self.description.items():
|
||||||
|
temp_table.attrs.__setattr__("description_" + key, str(value))
|
||||||
|
|
||||||
|
row = temp_table.row
|
||||||
|
for i in range(n):
|
||||||
|
row["x"] = xdata[i]
|
||||||
|
row["y"] = ydata[i]
|
||||||
|
row["setpoint"] = spdata[i]
|
||||||
|
row.append()
|
||||||
|
|
||||||
|
temp_table.flush()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""String representation of the temperature result."""
|
||||||
|
return f"TemperatureResult(points={len(self)}, job_id={self.job_id})"
|
||||||
|
|
||||||
|
|
||||||
|
def read_from_hdf(hdf_node):
|
||||||
|
"""
|
||||||
|
Read a TemperatureResult object from an HDF5 table node.
|
||||||
|
Returns None if the node is not suitable.
|
||||||
|
"""
|
||||||
|
if not isinstance(hdf_node, tables.Table):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hdf_node._v_attrs.damaris_type != "TemperatureResult":
|
||||||
|
return None
|
||||||
|
|
||||||
|
tr = TemperatureResult()
|
||||||
|
tr.xlabel = getattr(hdf_node._v_attrs, "xlabel", "Time (s)")
|
||||||
|
tr.ylabel = getattr(hdf_node._v_attrs, "ylabel", "Temperature (C)")
|
||||||
|
|
||||||
|
for r in hdf_node.iterrows():
|
||||||
|
tr.x.append(r["x"])
|
||||||
|
tr.y.append(r["y"])
|
||||||
|
sp = r["setpoint"]
|
||||||
|
if sp != 0.0 or len(tr.setpoint) > 0:
|
||||||
|
tr.setpoint.append(sp)
|
||||||
|
|
||||||
|
return tr
|
||||||
@@ -6,5 +6,5 @@ from damaris.data.Error_Result import Error_Result
|
|||||||
from damaris.data.Config_Result import Config_Result
|
from damaris.data.Config_Result import Config_Result
|
||||||
from damaris.data.Temperature import TemperatureResult
|
from damaris.data.Temperature import TemperatureResult
|
||||||
|
|
||||||
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "FFT", "Error_Result", "Config_Result", "TemperatureResult" ]
|
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ]
|
||||||
|
|
||||||
@@ -96,12 +96,10 @@ class Experiment:
|
|||||||
|
|
||||||
#for tracking the experiment length:
|
#for tracking the experiment length:
|
||||||
#because loops are possible we need to track the length for each loop level
|
#because loops are possible we need to track the length for each loop level
|
||||||
self.total_time=[]
|
self.total_time=[0.0]
|
||||||
self.total_time.append(0.0)
|
|
||||||
|
|
||||||
#and we need to know the number of iterations of the loops to multiply the state.
|
#and we need to know the number of iterations of the loops to multiply the state.
|
||||||
self.loop_iterations=[]
|
self.loop_iterations=[1]
|
||||||
self.loop_iterations.append(1)
|
|
||||||
|
|
||||||
# Commands -------------------------------------------------------------------------------------
|
# Commands -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+566
-76
@@ -61,7 +61,8 @@ from damaris.gui import ResultReader, ResultHandling
|
|||||||
from damaris.gui import BackendDriver
|
from damaris.gui import BackendDriver
|
||||||
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader
|
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader
|
||||||
#from damaris.data import Drawable # this is a base class, it should be used...
|
#from damaris.data import Drawable # this is a base class, it should be used...
|
||||||
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult
|
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult, TemperatureResult
|
||||||
|
# TemperatureMonitor imported lazily to avoid circular import issues
|
||||||
|
|
||||||
# default, can be set to true from start script
|
# default, can be set to true from start script
|
||||||
debug = False
|
debug = False
|
||||||
@@ -88,7 +89,6 @@ class logstream:
|
|||||||
def __del__( self ):
|
def __del__( self ):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
global log
|
global log
|
||||||
log = logstream( )
|
log = logstream( )
|
||||||
sys.stdout = log
|
sys.stdout = log
|
||||||
@@ -134,9 +134,6 @@ class LockFile:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DamarisGUI:
|
class DamarisGUI:
|
||||||
ExpScript_Display = 0
|
ExpScript_Display = 0
|
||||||
ResScript_Display = 1
|
ResScript_Display = 1
|
||||||
@@ -169,7 +166,7 @@ class DamarisGUI:
|
|||||||
|
|
||||||
self.log = LogWindow( self.xml_gui , self)
|
self.log = LogWindow( self.xml_gui , self)
|
||||||
|
|
||||||
self.sw = ScriptWidgets( self.xml_gui )
|
self.sw = ScriptWidgets( self.xml_gui, self )
|
||||||
|
|
||||||
self.toolbar_init( )
|
self.toolbar_init( )
|
||||||
|
|
||||||
@@ -184,6 +181,10 @@ class DamarisGUI:
|
|||||||
|
|
||||||
#to stop queued experiments
|
#to stop queued experiments
|
||||||
self.stop_experiment_flag = threading.Event()
|
self.stop_experiment_flag = threading.Event()
|
||||||
|
|
||||||
|
# Temperature monitoring
|
||||||
|
self.temp_monitor = None
|
||||||
|
self.temp_controller = None
|
||||||
|
|
||||||
exp_script = ""
|
exp_script = ""
|
||||||
if exp_script_filename is not None and exp_script_filename != "":
|
if exp_script_filename is not None and exp_script_filename != "":
|
||||||
@@ -218,6 +219,7 @@ class DamarisGUI:
|
|||||||
"on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event,
|
"on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event,
|
||||||
"on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event,
|
"on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event,
|
||||||
"on_toolbar_open_file_button_clicked": self.sw.open_file,
|
"on_toolbar_open_file_button_clicked": self.sw.open_file,
|
||||||
|
"on_toolbar_open_hdf5_button_clicked": self.sw.open_hdf5,
|
||||||
"on_toolbar_new_button_clicked": self.sw.new_file,
|
"on_toolbar_new_button_clicked": self.sw.new_file,
|
||||||
"on_toolbar_save_as_button_clicked": self.sw.save_file_as,
|
"on_toolbar_save_as_button_clicked": self.sw.save_file_as,
|
||||||
"on_toolbar_save_file_button_clicked": self.sw.save_file,
|
"on_toolbar_save_file_button_clicked": self.sw.save_file,
|
||||||
@@ -297,6 +299,14 @@ class DamarisGUI:
|
|||||||
self.config = None
|
self.config = None
|
||||||
self.xml_gui = None
|
self.xml_gui = None
|
||||||
|
|
||||||
|
def observe_data_pool( self, data_pool ):
|
||||||
|
"""
|
||||||
|
Delegate to monitor widgets
|
||||||
|
"""
|
||||||
|
self.data = data_pool
|
||||||
|
if hasattr(self, "monitor"):
|
||||||
|
self.monitor.observe_data_pool(data_pool)
|
||||||
|
|
||||||
# event handling: the real acitons in gui programming
|
# event handling: the real acitons in gui programming
|
||||||
|
|
||||||
# first global events
|
# first global events
|
||||||
@@ -471,9 +481,10 @@ class DamarisGUI:
|
|||||||
|
|
||||||
self.id = self.lockfile.add_experiment()
|
self.id = self.lockfile.add_experiment()
|
||||||
while not self.lockfile.am_i_next():
|
while not self.lockfile.am_i_next():
|
||||||
self.stop_experiment_flag.wait(interval)
|
|
||||||
while gtk.events_pending():
|
while gtk.events_pending():
|
||||||
gtk.main_iteration_do(False)
|
gtk.main_iteration_do(False)
|
||||||
|
if self.stop_experiment_flag.wait(interval):
|
||||||
|
break
|
||||||
if loop_run > 1:
|
if loop_run > 1:
|
||||||
self.experiment_script_statusbar_label.set_text("Waiting for other experiment to finish")
|
self.experiment_script_statusbar_label.set_text("Waiting for other experiment to finish")
|
||||||
loop_run = 0
|
loop_run = 0
|
||||||
@@ -506,6 +517,8 @@ class DamarisGUI:
|
|||||||
self.data = self.si.data
|
self.data = self.si.data
|
||||||
# run frontend and script engines
|
# run frontend and script engines
|
||||||
self.monitor.observe_data_pool( self.data )
|
self.monitor.observe_data_pool( self.data )
|
||||||
|
# Start temperature monitoring if configured (before scripts start)
|
||||||
|
self.start_temperature_monitoring(actual_config)
|
||||||
self.si.runScripts( )
|
self.si.runScripts( )
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||||
@@ -524,7 +537,10 @@ class DamarisGUI:
|
|||||||
print("waiting for threads stoping...", end=' ')
|
print("waiting for threads stoping...", end=' ')
|
||||||
still_running = [x for x in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if x is not None and x.is_alive( )]
|
still_running = [x for x in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if x is not None and x.is_alive( )]
|
||||||
for t in still_running:
|
for t in still_running:
|
||||||
t.join( )
|
while t.is_alive():
|
||||||
|
while gtk.events_pending():
|
||||||
|
gtk.main_iteration_do(False)
|
||||||
|
t.join( 0.05 )
|
||||||
print("done")
|
print("done")
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
@@ -563,6 +579,7 @@ class DamarisGUI:
|
|||||||
self.dump_filename = ""
|
self.dump_filename = ""
|
||||||
if actual_config[ "data_pool_name" ] != "":
|
if actual_config[ "data_pool_name" ] != "":
|
||||||
self.dump_states( init=True )
|
self.dump_states( init=True )
|
||||||
|
|
||||||
gobject.timeout_add( 200, self.observe_running_experiment )
|
gobject.timeout_add( 200, self.observe_running_experiment )
|
||||||
|
|
||||||
def observe_running_experiment( self ):
|
def observe_running_experiment( self ):
|
||||||
@@ -675,6 +692,8 @@ class DamarisGUI:
|
|||||||
|
|
||||||
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:
|
||||||
|
# Stop temperature monitoring when experiment finishes
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
if self.save_thread is None and self.dump_filename != "":
|
if self.save_thread is None and self.dump_filename != "":
|
||||||
print("all subprocesses ended, saving data pool")
|
print("all subprocesses ended, saving data pool")
|
||||||
# thread to save data...
|
# thread to save data...
|
||||||
@@ -865,15 +884,6 @@ class DamarisGUI:
|
|||||||
filters=tables.Filters( complevel=9, complib='zlib' ) )
|
filters=tables.Filters( complevel=9, complib='zlib' ) )
|
||||||
|
|
||||||
if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ):
|
if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ):
|
||||||
# take some data from dump file and repack
|
|
||||||
os.rename( self.dump_filename, self.dump_filename + ".bak" )
|
|
||||||
old_dump_file = tables.open_file( self.dump_filename + ".bak", mode="r+" )
|
|
||||||
if "data_pool" in old_dump_file.root:
|
|
||||||
old_dump_file.remove_node( where="/", name="data_pool", recursive=True )
|
|
||||||
old_dump_file.copy_file( self.dump_filename )
|
|
||||||
old_dump_file.close( )
|
|
||||||
del old_dump_file
|
|
||||||
os.remove( self.dump_filename + ".bak" )
|
|
||||||
# prepare for update
|
# prepare for update
|
||||||
dump_file = tables.open_file( self.dump_filename, mode="r+" )
|
dump_file = tables.open_file( self.dump_filename, mode="r+" )
|
||||||
|
|
||||||
@@ -890,7 +900,8 @@ class DamarisGUI:
|
|||||||
|
|
||||||
# save the data!
|
# save the data!
|
||||||
self.data.write_hdf5( dump_file, where="/", name="data_pool",
|
self.data.write_hdf5( dump_file, where="/", name="data_pool",
|
||||||
complib=self.dump_complib, complevel=self.dump_complevel )
|
complib=self.dump_complib, complevel=self.dump_complevel,
|
||||||
|
incremental=not init )
|
||||||
|
|
||||||
# now save additional information
|
# now save additional information
|
||||||
e = self.si.data.get( "__recentexperiment", -1 ) + 1
|
e = self.si.data.get( "__recentexperiment", -1 ) + 1
|
||||||
@@ -973,8 +984,98 @@ class DamarisGUI:
|
|||||||
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f]
|
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f]
|
||||||
for r in still_running:
|
for r in still_running:
|
||||||
r.quit_flag.set( )
|
r.quit_flag.set( )
|
||||||
|
|
||||||
|
# Stop temperature monitoring
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
|
|
||||||
self.state = DamarisGUI.Stop_State
|
self.state = DamarisGUI.Stop_State
|
||||||
|
|
||||||
|
def start_temperature_monitoring(self, config):
|
||||||
|
"""
|
||||||
|
Start temperature monitoring thread if configured.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary from self.config.get()
|
||||||
|
"""
|
||||||
|
# Check if temperature monitoring is enabled
|
||||||
|
if not config.get("enable_temperature_monitoring", False):
|
||||||
|
# Stop any existing monitor if temperature monitoring is disabled
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stop any existing monitor before starting a new one
|
||||||
|
if self.temp_monitor is not None:
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
|
|
||||||
|
controller_type = config.get("temperature_controller_type", "eurotherm")
|
||||||
|
temp_interval = config.get("temperature_interval", 2.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the factory and TemperatureMonitor (lazy import to avoid circular issues)
|
||||||
|
from damaris.tools.temperature import create_controller
|
||||||
|
from damaris.gui.TemperatureMonitor import TemperatureMonitor
|
||||||
|
|
||||||
|
# Create controller based on type
|
||||||
|
if controller_type == "eurotherm":
|
||||||
|
temp_device = config.get("temperature_device", "")
|
||||||
|
temp_baudrate = config.get("temperature_baudrate", 19200)
|
||||||
|
|
||||||
|
if not temp_device:
|
||||||
|
print("Temperature monitoring: No device configured for Eurotherm")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.temp_controller = create_controller(
|
||||||
|
"eurotherm", temp_device, baudrate=temp_baudrate
|
||||||
|
)
|
||||||
|
print(f"Temperature monitoring started with Eurotherm on {temp_device} (interval: {temp_interval}s)")
|
||||||
|
|
||||||
|
elif controller_type == "simulated":
|
||||||
|
# Simulated controller doesn't need device configuration
|
||||||
|
# Use default parameters: 290K base, 10K amplitude, 60s period, 5% noise, 10% missing
|
||||||
|
self.temp_controller = create_controller("simulated")
|
||||||
|
print(f"Temperature monitoring started with simulated controller (interval: {temp_interval}s)")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Temperature monitoring: Unknown controller type: {controller_type}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create and start monitor thread
|
||||||
|
self.temp_monitor = TemperatureMonitor(
|
||||||
|
controller=self.temp_controller,
|
||||||
|
interval=temp_interval,
|
||||||
|
data_pool=self.data,
|
||||||
|
data_key="temperature"
|
||||||
|
)
|
||||||
|
self.temp_monitor.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to start temperature monitoring: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
self.temp_controller = None
|
||||||
|
self.temp_monitor = None
|
||||||
|
|
||||||
|
def stop_temperature_monitoring(self):
|
||||||
|
"""
|
||||||
|
Stop the temperature monitoring thread if running.
|
||||||
|
"""
|
||||||
|
if self.temp_monitor is not None:
|
||||||
|
try:
|
||||||
|
self.temp_monitor.stop()
|
||||||
|
print("Temperature monitoring stopped")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error stopping temperature monitoring: {e}")
|
||||||
|
finally:
|
||||||
|
self.temp_monitor = None
|
||||||
|
|
||||||
|
if self.temp_controller is not None:
|
||||||
|
try:
|
||||||
|
self.temp_controller.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing temperature controller: {e}")
|
||||||
|
finally:
|
||||||
|
self.temp_controller = None
|
||||||
|
|
||||||
def documentation_init( self ):
|
def documentation_init( self ):
|
||||||
|
|
||||||
self.doc_urls = {
|
self.doc_urls = {
|
||||||
@@ -1099,11 +1200,12 @@ class LogWindow:
|
|||||||
|
|
||||||
|
|
||||||
class ScriptWidgets:
|
class ScriptWidgets:
|
||||||
def __init__( self, xml_gui ):
|
def __init__( self, xml_gui, outer_space ):
|
||||||
"""
|
"""
|
||||||
initialize text widgets with text
|
initialize text widgets with text
|
||||||
"""
|
"""
|
||||||
self.xml_gui = xml_gui
|
self.xml_gui = xml_gui
|
||||||
|
self.outer_space = outer_space
|
||||||
|
|
||||||
# my states
|
# my states
|
||||||
# editing enabled/disabled
|
# editing enabled/disabled
|
||||||
@@ -1170,7 +1272,7 @@ get_ylabel set_ylabel"""
|
|||||||
|
|
||||||
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None)
|
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None)
|
||||||
self.compwgen.register(self.keywordsbuffer)
|
self.compwgen.register(self.keywordsbuffer)
|
||||||
self.compwgen.set_property("minimum-word-size", 1)
|
self.compwgen.set_property("minimum-word-size", 2)
|
||||||
|
|
||||||
self.compwexp = gtksourceview2.CompletionWords.new("Words", None)
|
self.compwexp = gtksourceview2.CompletionWords.new("Words", None)
|
||||||
self.compwexp.register(self.experiment_script_textbuffer)
|
self.compwexp.register(self.experiment_script_textbuffer)
|
||||||
@@ -1186,7 +1288,6 @@ get_ylabel set_ylabel"""
|
|||||||
compexp.add_provider(self.compwexp)
|
compexp.add_provider(self.compwexp)
|
||||||
compres.add_provider(self.compwres)
|
compres.add_provider(self.compwres)
|
||||||
|
|
||||||
# script fonts are atlered by configuration
|
|
||||||
# clipboard
|
# clipboard
|
||||||
self.main_clipboard = gtk.Clipboard( )
|
self.main_clipboard = gtk.Clipboard( )
|
||||||
|
|
||||||
@@ -1194,13 +1295,12 @@ get_ylabel set_ylabel"""
|
|||||||
self.experiment_script_statusbar_label = self.xml_gui.get_object( "statusbar_experiment_script_label" )
|
self.experiment_script_statusbar_label = self.xml_gui.get_object( "statusbar_experiment_script_label" )
|
||||||
self.data_handling_statusbar_label = self.xml_gui.get_object( "statusbar_data_handling_label" )
|
self.data_handling_statusbar_label = self.xml_gui.get_object( "statusbar_data_handling_label" )
|
||||||
|
|
||||||
# footline with line and col indicators
|
# footer with line and col indicators
|
||||||
self.experiment_script_line_indicator = self.xml_gui.get_object( "experiment_script_line_textfield" )
|
self.experiment_script_line_indicator = self.xml_gui.get_object( "experiment_script_line_textfield" )
|
||||||
self.experiment_script_column_indicator = self.xml_gui.get_object( "experiment_script_column_textfield" )
|
self.experiment_script_column_indicator = self.xml_gui.get_object( "experiment_script_column_textfield" )
|
||||||
self.data_handling_line_indicator = self.xml_gui.get_object( "data_handling_line_textfield" )
|
self.data_handling_line_indicator = self.xml_gui.get_object( "data_handling_line_textfield" )
|
||||||
self.data_handling_column_indicator = self.xml_gui.get_object( "data_handling_column_textfield" )
|
self.data_handling_column_indicator = self.xml_gui.get_object( "data_handling_column_textfield" )
|
||||||
|
|
||||||
|
|
||||||
# some event handlers
|
# some event handlers
|
||||||
self.experiment_script_textbuffer.connect( "modified-changed", self.textviews_modified )
|
self.experiment_script_textbuffer.connect( "modified-changed", self.textviews_modified )
|
||||||
self.experiment_script_textview.connect_after( "move-cursor", self.textviews_moved )
|
self.experiment_script_textview.connect_after( "move-cursor", self.textviews_moved )
|
||||||
@@ -1211,8 +1311,7 @@ get_ylabel set_ylabel"""
|
|||||||
self.data_handling_textview.connect( "key-press-event", self.textviews_keypress )
|
self.data_handling_textview.connect( "key-press-event", self.textviews_keypress )
|
||||||
self.data_handling_textview.connect( "button-release-event", self.textviews_clicked )
|
self.data_handling_textview.connect( "button-release-event", self.textviews_clicked )
|
||||||
|
|
||||||
# and the editing toolbar part
|
# and the editing toolbar buttons
|
||||||
# buttons
|
|
||||||
self.toolbar_new_button = self.xml_gui.get_object( "toolbar_new_button" )
|
self.toolbar_new_button = self.xml_gui.get_object( "toolbar_new_button" )
|
||||||
self.toolbar_open_button = self.xml_gui.get_object( "toolbar_open_file_button" )
|
self.toolbar_open_button = self.xml_gui.get_object( "toolbar_open_file_button" )
|
||||||
self.toolbar_save_button = self.xml_gui.get_object( "toolbar_save_file_button" )
|
self.toolbar_save_button = self.xml_gui.get_object( "toolbar_save_file_button" )
|
||||||
@@ -1223,8 +1322,9 @@ get_ylabel set_ylabel"""
|
|||||||
self.toolbar_redo_button = self.xml_gui.get_object( "toolbar_redo_button" )
|
self.toolbar_redo_button = self.xml_gui.get_object( "toolbar_redo_button" )
|
||||||
self.toolbar_search_button = self.xml_gui.get_object("toolbar_search_button")
|
self.toolbar_search_button = self.xml_gui.get_object("toolbar_search_button")
|
||||||
|
|
||||||
# my notebook
|
# main notebook
|
||||||
self.main_notebook = self.xml_gui.get_object( "main_notebook" )
|
self.main_notebook = self.xml_gui.get_object( "main_notebook" )
|
||||||
|
|
||||||
# config toolbar
|
# config toolbar
|
||||||
self.main_notebook.connect_after( "switch_page", self.notebook_page_switched )
|
self.main_notebook.connect_after( "switch_page", self.notebook_page_switched )
|
||||||
|
|
||||||
@@ -1570,7 +1670,7 @@ get_ylabel set_ylabel"""
|
|||||||
elif response == gtk.ResponseType.CANCEL:
|
elif response == gtk.ResponseType.CANCEL:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def response( self, response_id, script_widget ):
|
def response( dialog, response_id, script_widget ):
|
||||||
if response_id == gtk.ResponseType.OK:
|
if response_id == gtk.ResponseType.OK:
|
||||||
file_name = dialog.get_filename( )
|
file_name = dialog.get_filename( )
|
||||||
if file_name is None:
|
if file_name is None:
|
||||||
@@ -1578,7 +1678,13 @@ get_ylabel set_ylabel"""
|
|||||||
|
|
||||||
script_filename = os.path.abspath( file_name )
|
script_filename = os.path.abspath( file_name )
|
||||||
if not os.access( script_filename, os.R_OK ):
|
if not os.access( script_filename, os.R_OK ):
|
||||||
self.outer_space.show_error_dialog( "File I/O Error", "Cannot read from file %s" % script_filename )
|
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
|
||||||
|
type=gtk.MessageType.ERROR,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
buttons=("_OK", gtk.ResponseType.OK))
|
||||||
|
error_dialog.set_markup("Cannot read from file %s" % script_filename)
|
||||||
|
error_dialog.run()
|
||||||
|
error_dialog.destroy()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
script_string = script_widget.load_file_as_unicode( script_filename )
|
script_string = script_widget.load_file_as_unicode( script_filename )
|
||||||
@@ -1611,6 +1717,94 @@ get_ylabel set_ylabel"""
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def open_hdf5( self, widget, Data=None ):
|
||||||
|
"""
|
||||||
|
Open and load an HDF5 file written by DAMARIS.
|
||||||
|
"""
|
||||||
|
# ignore
|
||||||
|
if not self.editing_state:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
parent_window = self.xml_gui.get_object( "main_window" )
|
||||||
|
dialog = gtk.FileChooserDialog( title="Open HDF5 File...",
|
||||||
|
parent=parent_window,
|
||||||
|
action=gtk.FileChooserAction.OPEN,
|
||||||
|
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
|
||||||
|
)
|
||||||
|
dialog.set_default_response( gtk.ResponseType.OK )
|
||||||
|
dialog.set_select_multiple( False )
|
||||||
|
|
||||||
|
# Add filter for HDF5 files
|
||||||
|
filter_hdf5 = gtk.FileFilter()
|
||||||
|
filter_hdf5.set_name("HDF5 files")
|
||||||
|
filter_hdf5.add_pattern("*.h5")
|
||||||
|
filter_hdf5.add_pattern("*.hdf5")
|
||||||
|
filter_hdf5.add_pattern("*.HDF5")
|
||||||
|
filter_hdf5.add_pattern("*.H5")
|
||||||
|
dialog.add_filter(filter_hdf5)
|
||||||
|
|
||||||
|
# Also allow all files
|
||||||
|
filter_all = gtk.FileFilter()
|
||||||
|
filter_all.set_name("All files")
|
||||||
|
filter_all.add_pattern("*")
|
||||||
|
dialog.add_filter(filter_all)
|
||||||
|
|
||||||
|
def response( dialog, response_id, script_widget ):
|
||||||
|
if response_id == gtk.ResponseType.OK:
|
||||||
|
file_name = dialog.get_filename( )
|
||||||
|
if file_name is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
file_name = os.path.abspath( file_name )
|
||||||
|
if not os.access( file_name, os.R_OK ):
|
||||||
|
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
|
||||||
|
type=gtk.MessageType.ERROR,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
buttons=("_OK", gtk.ResponseType.OK))
|
||||||
|
error_dialog.set_markup("Cannot read from file %s" % file_name)
|
||||||
|
error_dialog.run()
|
||||||
|
error_dialog.destroy()
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load DataPool and metadata from HDF5
|
||||||
|
data_pool, metadata = DataPool.load_hdf5(file_name)
|
||||||
|
|
||||||
|
# Set the loaded data pool in the outer space (main application)
|
||||||
|
if hasattr(script_widget.outer_space, 'observe_data_pool'):
|
||||||
|
script_widget.outer_space.observe_data_pool(data_pool)
|
||||||
|
elif hasattr(script_widget.outer_space, 'monitor'):
|
||||||
|
# Fallback if delegate not present
|
||||||
|
script_widget.outer_space.data = data_pool
|
||||||
|
script_widget.outer_space.monitor.observe_data_pool(data_pool)
|
||||||
|
|
||||||
|
# If there are scripts in metadata, load them
|
||||||
|
if 'scripts' in metadata:
|
||||||
|
scripts = metadata['scripts']
|
||||||
|
exp_script = scripts.get('experiment_script', None)
|
||||||
|
res_script = scripts.get('result_script', None)
|
||||||
|
if exp_script or res_script:
|
||||||
|
script_widget.set_scripts(exp_script, res_script)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
|
||||||
|
type=gtk.MessageType.ERROR,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
buttons=("_OK", gtk.ResponseType.OK))
|
||||||
|
error_dialog.set_markup("Failed to load HDF5 file: %s" % str(e))
|
||||||
|
error_dialog.run()
|
||||||
|
error_dialog.destroy()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Event-Handler for response-signal (when one of the button is pressed)
|
||||||
|
dialog.connect( "response", response, self )
|
||||||
|
|
||||||
|
dialog.run( )
|
||||||
|
dialog.destroy( )
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def save_file( self, widget=None, Data=None ):
|
def save_file( self, widget=None, Data=None ):
|
||||||
"""
|
"""
|
||||||
save file to associated filename
|
save file to associated filename
|
||||||
@@ -1798,9 +1992,9 @@ get_ylabel set_ylabel"""
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if response_id == -1:
|
if response_id == -1:
|
||||||
match_start, match_end = startiter.backward_search(searchstring, gtk.TEXT_SEARCH_TEXT_ONLY)
|
match_start, match_end = startiter.backward_search(searchstring, gtk.TextSearchFlags.TEXT_ONLY)
|
||||||
elif response_id == 1:
|
elif response_id == 1:
|
||||||
match_start, match_end = enditer.forward_search(searchstring, gtk.TEXT_SEARCH_TEXT_ONLY)
|
match_start, match_end = enditer.forward_search(searchstring, gtk.TextSearchFlags.TEXT_ONLY)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1857,7 +2051,7 @@ get_ylabel set_ylabel"""
|
|||||||
dialog.connect("response", response, self, textbox)
|
dialog.connect("response", response, self, textbox)
|
||||||
dialog.set_resizable(False)
|
dialog.set_resizable(False)
|
||||||
|
|
||||||
dialog.show()
|
dialog.show_all()
|
||||||
|
|
||||||
if startit is not None and endit is not None:
|
if startit is not None and endit is not None:
|
||||||
if current_page == 0:
|
if current_page == 0:
|
||||||
@@ -1951,6 +2145,87 @@ class ConfigTab:
|
|||||||
self.config_info_textview = self.xml_gui.get_object( "info_textview" )
|
self.config_info_textview = self.xml_gui.get_object( "info_textview" )
|
||||||
self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" )
|
self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" )
|
||||||
|
|
||||||
|
# ADC bit depth setting
|
||||||
|
self.config_adc_bit_depth_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||||
|
self.config_adc_bit_depth_label = gtk.Label(label="ADC bit depth:")
|
||||||
|
self.config_adc_bit_depth_spinbutton = gtk.SpinButton.new_with_range(8, 32, 1)
|
||||||
|
self.config_adc_bit_depth_spinbutton.set_value(ADC_Result.default_bit_depth)
|
||||||
|
self.config_adc_bit_depth_spinbutton.set_tooltip_text("Set the default bit depth for clipping detection (e.g., 14 for 14-bit ADCs).")
|
||||||
|
self.config_adc_bit_depth_spinbutton.connect("value-changed", self.on_adc_bit_depth_changed)
|
||||||
|
self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_label, False, False, 0)
|
||||||
|
self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_spinbutton, False, False, 0)
|
||||||
|
backend_vbox = self.xml_gui.get_object("vbox4")
|
||||||
|
if backend_vbox:
|
||||||
|
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
|
||||||
|
self.config_adc_bit_depth_hbox.show_all()
|
||||||
|
|
||||||
|
# Temperature monitoring settings - single line layout
|
||||||
|
self.config_temp_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||||
|
|
||||||
|
# Controller type selector
|
||||||
|
self.config_temp_controller_type_label = gtk.Label(label="Controller:")
|
||||||
|
self.config_temp_controller_type_combobox = gtk.ComboBoxText()
|
||||||
|
self.config_temp_controller_type_combobox.append_text("eurotherm")
|
||||||
|
self.config_temp_controller_type_combobox.append_text("simulated")
|
||||||
|
self.config_temp_controller_type_combobox.set_active(0) # Default to eurotherm
|
||||||
|
self.config_temp_controller_type_combobox.set_tooltip_text("Select temperature controller type")
|
||||||
|
self.config_temp_controller_type_combobox.connect("changed", self.on_temp_controller_type_changed)
|
||||||
|
|
||||||
|
self.config_temp_controller_type_hbox = gtk.HBox(homogeneous=False, spacing=0)
|
||||||
|
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_label, False, False, 0)
|
||||||
|
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_combobox, False, False, 0)
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_controller_type_hbox, False, False, 0)
|
||||||
|
|
||||||
|
# Enable checkbox
|
||||||
|
self.config_temp_monitoring_checkbutton = gtk.CheckButton(label="Temp:")
|
||||||
|
self.config_temp_monitoring_checkbutton.set_tooltip_text("Enable periodic temperature reading from controller")
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_monitoring_checkbutton, False, False, 0)
|
||||||
|
|
||||||
|
# Device file chooser with entry (shown for eurotherm, hidden for simulated)
|
||||||
|
self.config_temp_device_entry = gtk.Entry()
|
||||||
|
self.config_temp_device_entry.set_tooltip_text("Serial port device (e.g., /dev/serial/by-id/...)")
|
||||||
|
self.config_temp_device_entry.set_width_chars(30)
|
||||||
|
self.config_temp_device_button = gtk.Button(label="...")
|
||||||
|
self.config_temp_device_button.set_tooltip_text("Browse for temperature controller device")
|
||||||
|
self.config_temp_device_button.connect("clicked", self.on_temp_device_browse_clicked)
|
||||||
|
|
||||||
|
self.config_temp_device_hbox = gtk.HBox(homogeneous=False, spacing=0)
|
||||||
|
self.config_temp_device_hbox.pack_start(self.config_temp_device_entry, True, True, 0)
|
||||||
|
self.config_temp_device_hbox.pack_start(self.config_temp_device_button, False, False, 0)
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_device_hbox, True, True, 0)
|
||||||
|
|
||||||
|
# Interval spinbutton
|
||||||
|
self.config_temp_interval_label = gtk.Label(label="Interval (s):")
|
||||||
|
self.config_temp_interval_spinbutton = gtk.SpinButton.new_with_range(0.1, 60, 0.1)
|
||||||
|
self.config_temp_interval_spinbutton.set_value(2.0) # Default to 2 seconds
|
||||||
|
self.config_temp_interval_spinbutton.set_tooltip_text("Time between temperature readings in seconds")
|
||||||
|
|
||||||
|
self.config_temp_interval_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||||
|
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0)
|
||||||
|
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_spinbutton, False, False, 0)
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_interval_hbox, False, False, 0)
|
||||||
|
|
||||||
|
# Baud rate (for eurotherm)
|
||||||
|
self.config_temp_baudrate_combobox = gtk.ComboBoxText()
|
||||||
|
self.config_temp_baudrate_combobox.append_text("19200")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("9600")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("38400")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("57600")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("115200")
|
||||||
|
self.config_temp_baudrate_combobox.set_active(0)
|
||||||
|
self.config_temp_baudrate_combobox.set_tooltip_text("Serial baud rate for temperature controller")
|
||||||
|
self.config_temp_baudrate_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||||
|
self.config_temp_baudrate_hbox.pack_start(self.config_temp_baudrate_combobox, False, False, 0)
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_baudrate_hbox, False, False, 0)
|
||||||
|
# Note: baud rate is shown for eurotherm, hidden for simulated
|
||||||
|
|
||||||
|
# Set initial visibility based on controller type
|
||||||
|
self._update_temp_widgets_visibility()
|
||||||
|
|
||||||
|
if backend_vbox:
|
||||||
|
backend_vbox.pack_start(self.config_temp_hbox, False, False, 0)
|
||||||
|
self.config_temp_hbox.show_all()
|
||||||
|
|
||||||
# insert version informations
|
# insert version informations
|
||||||
components_text = """
|
components_text = """
|
||||||
operating system %(os)s
|
operating system %(os)s
|
||||||
@@ -2044,7 +2319,7 @@ pygobject version %(pygobject)s
|
|||||||
"""
|
"""
|
||||||
complib_iter = self.config_data_pool_complib.get_active_iter( )
|
complib_iter = self.config_data_pool_complib.get_active_iter( )
|
||||||
complib = self.config_data_pool_complib.get_model( ).get_value( complib_iter, 0 )
|
complib = self.config_data_pool_complib.get_model( ).get_value( complib_iter, 0 )
|
||||||
return {
|
actual_config = {
|
||||||
"start_backend": self.config_start_backend_checkbutton.get_active( ),
|
"start_backend": self.config_start_backend_checkbutton.get_active( ),
|
||||||
"start_result_script": self.config_start_result_script_checkbutton.get_active( ),
|
"start_result_script": self.config_start_result_script_checkbutton.get_active( ),
|
||||||
"start_experiment_script": self.config_start_experiment_script_checkbutton.get_active( ),
|
"start_experiment_script": self.config_start_experiment_script_checkbutton.get_active( ),
|
||||||
@@ -2056,8 +2331,29 @@ pygobject version %(pygobject)s
|
|||||||
"data_pool_write_interval": self.config_data_pool_write_interval_entry.get_text( ),
|
"data_pool_write_interval": self.config_data_pool_write_interval_entry.get_text( ),
|
||||||
"data_pool_complib": complib,
|
"data_pool_complib": complib,
|
||||||
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
|
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
|
||||||
"script_font": self.config_script_font_button.get_font_name( )
|
"script_font": self.config_script_font_button.get_font_name( ),
|
||||||
|
"adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(),
|
||||||
|
# Temperature monitoring configuration (only if widgets exist)
|
||||||
|
"enable_temperature_monitoring": self.config_temp_monitoring_checkbutton.get_active() if hasattr(self, 'config_temp_monitoring_checkbutton') else False,
|
||||||
|
"temperature_controller_type": self.config_temp_controller_type_combobox.get_active_text() if hasattr(self, 'config_temp_controller_type_combobox') else "eurotherm",
|
||||||
|
"temperature_device": self.config_temp_device_entry.get_text() if hasattr(self, 'config_temp_device_entry') else "",
|
||||||
|
"temperature_baudrate": self._get_temp_baudrate() if hasattr(self, 'config_temp_baudrate_combobox') else 19200,
|
||||||
|
"temperature_interval": self.config_temp_interval_spinbutton.get_value() if hasattr(self, 'config_temp_interval_spinbutton') else 2.0
|
||||||
}
|
}
|
||||||
|
ADC_Result.default_bit_depth = actual_config["adc_bit_depth"]
|
||||||
|
return actual_config
|
||||||
|
|
||||||
|
def _get_temp_baudrate(self):
|
||||||
|
"""Get selected baud rate from combobox."""
|
||||||
|
if not hasattr(self, 'config_temp_baudrate_combobox'):
|
||||||
|
return 19200
|
||||||
|
active_text = self.config_temp_baudrate_combobox.get_active_text()
|
||||||
|
if active_text:
|
||||||
|
try:
|
||||||
|
return int(active_text)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return 19200
|
||||||
|
|
||||||
def set( self, config ):
|
def set( self, config ):
|
||||||
if "start_backend" in config:
|
if "start_backend" in config:
|
||||||
@@ -2083,6 +2379,47 @@ pygobject version %(pygobject)s
|
|||||||
if "script_font" in config:
|
if "script_font" in config:
|
||||||
self.config_script_font_button.set_font_name( config[ "script_font" ] )
|
self.config_script_font_button.set_font_name( config[ "script_font" ] )
|
||||||
self.set_script_font_handler( None )
|
self.set_script_font_handler( None )
|
||||||
|
if "adc_bit_depth" in config:
|
||||||
|
try:
|
||||||
|
self.config_adc_bit_depth_spinbutton.set_value(float(config["adc_bit_depth"]))
|
||||||
|
ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Temperature monitoring configuration (only if widgets exist)
|
||||||
|
if hasattr(self, 'config_temp_monitoring_checkbutton') and "enable_temperature_monitoring" in config:
|
||||||
|
try:
|
||||||
|
self.config_temp_monitoring_checkbutton.set_active(bool(config["enable_temperature_monitoring"]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_controller_type_combobox') and "temperature_controller_type" in config:
|
||||||
|
try:
|
||||||
|
controller_type = config["temperature_controller_type"]
|
||||||
|
if controller_type in ["eurotherm", "simulated"]:
|
||||||
|
self._set_combo_active_text(self.config_temp_controller_type_combobox, controller_type)
|
||||||
|
# Update widget visibility based on type
|
||||||
|
self._update_temp_widgets_visibility()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_device_entry') and "temperature_device" in config:
|
||||||
|
try:
|
||||||
|
self.config_temp_device_entry.set_text(str(config["temperature_device"]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_baudrate_combobox') and "temperature_baudrate" in config:
|
||||||
|
try:
|
||||||
|
baudrate = str(config["temperature_baudrate"])
|
||||||
|
if baudrate in ["9600", "19200", "38400", "57600", "115200"]:
|
||||||
|
self._set_combo_active_text(self.config_temp_baudrate_combobox, baudrate)
|
||||||
|
else:
|
||||||
|
self.config_temp_baudrate_combobox.set_active(0) # Default to 19200
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_interval_spinbutton') and "temperature_interval" in config:
|
||||||
|
try:
|
||||||
|
self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"]))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
self.config_temp_interval_spinbutton.set_value(2.0)
|
||||||
if "data_pool_complib" in config:
|
if "data_pool_complib" in config:
|
||||||
# find combo-box entry and make it active...
|
# find combo-box entry and make it active...
|
||||||
model = self.config_data_pool_complib.get_model( )
|
model = self.config_data_pool_complib.get_model( )
|
||||||
@@ -2096,6 +2433,87 @@ pygobject version %(pygobject)s
|
|||||||
if iter is None:
|
if iter is None:
|
||||||
print("compression method %s is not supported" % config[ "data_pool_complib" ])
|
print("compression method %s is not supported" % config[ "data_pool_complib" ])
|
||||||
|
|
||||||
|
def on_adc_bit_depth_changed(self, widget):
|
||||||
|
ADC_Result.default_bit_depth = widget.get_value_as_int()
|
||||||
|
|
||||||
|
def on_temp_device_browse_clicked(self, widget):
|
||||||
|
"""
|
||||||
|
Open file chooser dialog to select temperature controller device.
|
||||||
|
Defaults to /dev/serial/by-id/ which is typically used for USB serial converters.
|
||||||
|
"""
|
||||||
|
dialog = gtk.FileChooserDialog(
|
||||||
|
title="Select Temperature Controller Device",
|
||||||
|
parent=None,
|
||||||
|
action=gtk.FileChooserAction.OPEN,
|
||||||
|
buttons=(
|
||||||
|
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
|
||||||
|
gtk.STOCK_OPEN, gtk.ResponseType.OK
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set default folder to /dev/serial/by-id/ if it exists
|
||||||
|
default_folder = "/dev/serial/by-id/"
|
||||||
|
if os.path.isdir(default_folder):
|
||||||
|
dialog.set_current_folder(default_folder)
|
||||||
|
else:
|
||||||
|
# Fallback to /dev
|
||||||
|
dialog.set_current_folder("/dev/")
|
||||||
|
|
||||||
|
# Only show regular files (device files)
|
||||||
|
dialog.set_select_multiple(False)
|
||||||
|
|
||||||
|
response = dialog.run()
|
||||||
|
if response == gtk.ResponseType.OK:
|
||||||
|
selected_file = dialog.get_filename()
|
||||||
|
self.config_temp_device_entry.set_text(selected_file)
|
||||||
|
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
def on_temp_controller_type_changed(self, widget):
|
||||||
|
"""Update widget visibility when controller type changes."""
|
||||||
|
self._update_temp_widgets_visibility()
|
||||||
|
|
||||||
|
def _update_temp_widgets_visibility(self):
|
||||||
|
"""Show/hide widgets based on selected controller type."""
|
||||||
|
# Defensive: check if combobox exists
|
||||||
|
if not hasattr(self, 'config_temp_controller_type_combobox'):
|
||||||
|
return
|
||||||
|
|
||||||
|
active_type = self.config_temp_controller_type_combobox.get_active_text()
|
||||||
|
if active_type is None:
|
||||||
|
active_type = "eurotherm"
|
||||||
|
|
||||||
|
# Defensive: check if widgets exist before showing/hiding
|
||||||
|
show_device = active_type == "eurotherm"
|
||||||
|
if hasattr(self, 'config_temp_device_hbox'):
|
||||||
|
if show_device:
|
||||||
|
self.config_temp_device_hbox.show()
|
||||||
|
else:
|
||||||
|
self.config_temp_device_hbox.hide()
|
||||||
|
if hasattr(self, 'config_temp_baudrate_hbox'):
|
||||||
|
if show_device:
|
||||||
|
self.config_temp_baudrate_hbox.show()
|
||||||
|
else:
|
||||||
|
self.config_temp_baudrate_hbox.hide()
|
||||||
|
|
||||||
|
def _set_combo_active_text(self, combobox, text):
|
||||||
|
"""
|
||||||
|
Set a ComboBoxText's active entry by text.
|
||||||
|
|
||||||
|
Gtk.ComboBoxText.set_active_text() does not exist in GTK 3.24.x,
|
||||||
|
so we iterate the model to find the matching entry.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
model = combobox.get_model()
|
||||||
|
if model is None:
|
||||||
|
return
|
||||||
|
for i in range(model.iter_n_children(None)):
|
||||||
|
iter = model.iter_nth_child(None, i)
|
||||||
|
if iter and model.get(iter, 0)[0] == text:
|
||||||
|
combobox.set_active(i)
|
||||||
|
return
|
||||||
|
|
||||||
def load_config_handler( self, widget ):
|
def load_config_handler( self, widget ):
|
||||||
if self.system_default_filename:
|
if self.system_default_filename:
|
||||||
self.load_config( self.system_default_filename )
|
self.load_config( self.system_default_filename )
|
||||||
@@ -2270,6 +2688,13 @@ class MonitorWidgets:
|
|||||||
self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" )
|
self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" )
|
||||||
self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" )
|
self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" )
|
||||||
|
|
||||||
|
self.display_descriptions_checkbutton = gtk.CheckButton(label="Show Desc.")
|
||||||
|
self.display_descriptions_checkbutton.set_active(True)
|
||||||
|
self.display_descriptions_checkbutton.show()
|
||||||
|
self.display_settings_frame.set_property("n-columns", 5)
|
||||||
|
self.display_settings_frame.attach(self.display_descriptions_checkbutton, 4, 5, 0, 1, gtk.AttachOptions.FILL, 0, 0, 0)
|
||||||
|
self.display_descriptions_checkbutton.connect("toggled", self.display_descriptions_toggled)
|
||||||
|
|
||||||
# Matplot (Display_Table, 1st Row) --------------------------------------------------------
|
# Matplot (Display_Table, 1st Row) --------------------------------------------------------
|
||||||
|
|
||||||
# create new plot
|
# create new plot
|
||||||
@@ -2307,7 +2732,9 @@ class MonitorWidgets:
|
|||||||
# now let's add a button to the toolbar
|
# now let's add a button to the toolbar
|
||||||
self.matplot_cursor_button = gtk.ToggleButton(label='')
|
self.matplot_cursor_button = gtk.ToggleButton(label='')
|
||||||
try:
|
try:
|
||||||
image = gtk.Image.new_from_file("/usr/share/icons/breeze/actions/24/crosshairs-symbolic.svg")
|
icon_theme = gtk.IconTheme.get_default()
|
||||||
|
icon = icon_theme.load_icon("crosshairs", 24, 0)
|
||||||
|
image = gtk.Image.new_from_pixbuf(icon)
|
||||||
self.matplot_cursor_button.set_image(image)
|
self.matplot_cursor_button.set_image(image)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -2363,6 +2790,8 @@ class MonitorWidgets:
|
|||||||
self.__rescale = True
|
self.__rescale = True
|
||||||
self.update_counter = 0
|
self.update_counter = 0
|
||||||
self.update_counter_lock = threading.Lock( )
|
self.update_counter_lock = threading.Lock( )
|
||||||
|
self.description_text = None
|
||||||
|
self.clipping_marker = None
|
||||||
|
|
||||||
def source_list_reset( self ):
|
def source_list_reset( self ):
|
||||||
self.display_source_treestore.clear( )
|
self.display_source_treestore.clear( )
|
||||||
@@ -2374,7 +2803,7 @@ class MonitorWidgets:
|
|||||||
def source_list_find_one( self, model, iter, what ):
|
def source_list_find_one( self, model, iter, what ):
|
||||||
"""find node in subcategory"""
|
"""find node in subcategory"""
|
||||||
while iter is not None:
|
while iter is not None:
|
||||||
if model.get( iter, 0 )[ 0 ] == what:
|
if str( model.get( iter, 0 )[ 0 ] ) == what:
|
||||||
return iter
|
return iter
|
||||||
iter = model.iter_next( iter )
|
iter = model.iter_next( iter )
|
||||||
return iter
|
return iter
|
||||||
@@ -2445,31 +2874,50 @@ class MonitorWidgets:
|
|||||||
register a listener and save reference to data
|
register a listener and save reference to data
|
||||||
assume to be in gtk/gdk lock
|
assume to be in gtk/gdk lock
|
||||||
"""
|
"""
|
||||||
if self.data_pool is not None:
|
gdk.threads_enter( )
|
||||||
# maybe some extra cleanup needed
|
try:
|
||||||
print("ToDo: cleanup widgets")
|
if self.data_pool is not None:
|
||||||
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
|
# maybe some extra cleanup needed
|
||||||
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
|
print("ToDo: cleanup widgets")
|
||||||
self.displayed_data[ 1 ] = None
|
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
|
||||||
self.displayed_data = [ None, None ]
|
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
|
||||||
self.data_pool.unregister_listener( self.datapool_listener )
|
self.displayed_data[ 1 ] = None
|
||||||
self.data_pool = None
|
self.displayed_data = [ None, None ]
|
||||||
|
self.data_pool.unregister_listener( self.datapool_listener )
|
||||||
|
self.data_pool = None
|
||||||
|
|
||||||
|
self.update_counter_lock.acquire( )
|
||||||
|
self.update_counter = 0
|
||||||
|
self.update_counter_lock.release( )
|
||||||
|
|
||||||
|
# display states
|
||||||
self.source_list_reset( )
|
self.source_list_reset( )
|
||||||
self.update_counter_lock.acquire( )
|
self.__rescale = True
|
||||||
self.update_counter = 0
|
self.displayed_data = [ None, None ]
|
||||||
self.update_counter_lock.release( )
|
self.display_source_path_label.set_label( "" )
|
||||||
|
self.clear_display( )
|
||||||
|
|
||||||
# display states
|
if data_pool is not None:
|
||||||
self.__rescale = True
|
# keep track of data
|
||||||
self.displayed_data = [ None, None ]
|
self.data_pool = data_pool
|
||||||
self.display_source_path_label.set_label( "" )
|
self.data_pool.register_listener( self.datapool_listener )
|
||||||
self.clear_display( )
|
|
||||||
|
# Populate source list with existing data since the listener
|
||||||
if data_pool is not None:
|
# was registered after the data was loaded
|
||||||
# keep track of data
|
for key in sorted(data_pool.keys()):
|
||||||
self.data_pool = data_pool
|
if key != "__builtins__": # Skip internal keys
|
||||||
self.data_pool.register_listener( self.datapool_listener )
|
self.source_list_add(key)
|
||||||
|
|
||||||
|
# Select first data item if available
|
||||||
|
keys = [k for k in data_pool.keys() if not k.startswith("__")]
|
||||||
|
if keys:
|
||||||
|
keys.sort()
|
||||||
|
first_key = keys[0]
|
||||||
|
first_iter = self.source_list_find(first_key.split("/"))
|
||||||
|
if first_iter:
|
||||||
|
self.display_source_combobox.set_active_iter(first_iter)
|
||||||
|
finally:
|
||||||
|
gdk.threads_leave( )
|
||||||
|
|
||||||
#################### observing data structures and produce idle events
|
#################### observing data structures and produce idle events
|
||||||
|
|
||||||
@@ -2606,7 +3054,7 @@ class MonitorWidgets:
|
|||||||
gdk.threads_leave( )
|
gdk.threads_leave( )
|
||||||
elif event.what == DataPool.Event.destroy:
|
elif event.what == DataPool.Event.destroy:
|
||||||
gdk.threads_enter( )
|
gdk.threads_enter( )
|
||||||
self.source_list_reset( 'None' )
|
self.source_list_reset( )
|
||||||
self.displayed_data = [ None, None ]
|
self.displayed_data = [ None, None ]
|
||||||
self.clear_display( )
|
self.clear_display( )
|
||||||
gdk.threads_leave( )
|
gdk.threads_leave( )
|
||||||
@@ -2685,6 +3133,10 @@ class MonitorWidgets:
|
|||||||
if self.displayed_data[ 0 ] is not None:
|
if self.displayed_data[ 0 ] is not None:
|
||||||
self.update_display( self.displayed_data[ 0 ][ : ] )
|
self.update_display( self.displayed_data[ 0 ][ : ] )
|
||||||
|
|
||||||
|
def display_descriptions_toggled( self, widget, data=None ):
|
||||||
|
if self.displayed_data[ 0 ] is not None:
|
||||||
|
self.update_display( self.displayed_data[ 0 ][ : ] )
|
||||||
|
|
||||||
def save_display_data_as_text( self, widget, data=None ):
|
def save_display_data_as_text( self, widget, data=None ):
|
||||||
"""
|
"""
|
||||||
copy data to tmp file and show save dialog
|
copy data to tmp file and show save dialog
|
||||||
@@ -2762,6 +3214,7 @@ class MonitorWidgets:
|
|||||||
self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes,
|
self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes,
|
||||||
useblit=False, color='red', linewidth=1)
|
useblit=False, color='red', linewidth=1)
|
||||||
self.matplot_canvas.draw()
|
self.matplot_canvas.draw()
|
||||||
|
# TODO: Deactivate the pane button
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Deactivate cursor
|
# Deactivate cursor
|
||||||
@@ -2889,7 +3342,7 @@ class MonitorWidgets:
|
|||||||
|
|
||||||
x_scale = self.display_x_scaling_combobox.get_active_text()
|
x_scale = self.display_x_scaling_combobox.get_active_text()
|
||||||
y_scale = self.display_y_scaling_combobox.get_active_text()
|
y_scale = self.display_y_scaling_combobox.get_active_text()
|
||||||
print(x_scale)
|
|
||||||
if x_scale == "lin":
|
if x_scale == "lin":
|
||||||
self.matplot_axes.set_xscale("linear")
|
self.matplot_axes.set_xscale("linear")
|
||||||
elif x_scale == "log10":
|
elif x_scale == "log10":
|
||||||
@@ -2935,6 +3388,18 @@ class MonitorWidgets:
|
|||||||
self.graphen = [ ]
|
self.graphen = [ ]
|
||||||
self.matplot_axes.clear( )
|
self.matplot_axes.clear( )
|
||||||
self.matplot_axes.grid( True )
|
self.matplot_axes.grid( True )
|
||||||
|
if self.description_text is not None:
|
||||||
|
try:
|
||||||
|
self.description_text.remove()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.description_text = None
|
||||||
|
if self.clipping_marker is not None:
|
||||||
|
try:
|
||||||
|
self.clipping_marker.remove()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.clipping_marker = None
|
||||||
self.matplot_canvas.draw_idle( )
|
self.matplot_canvas.draw_idle( )
|
||||||
|
|
||||||
def update_display( self, subject=None ):
|
def update_display( self, subject=None ):
|
||||||
@@ -2948,7 +3413,7 @@ class MonitorWidgets:
|
|||||||
if in_result is None:
|
if in_result is None:
|
||||||
self.clear_display( )
|
self.clear_display( )
|
||||||
return
|
return
|
||||||
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ):
|
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
|
||||||
|
|
||||||
xmin = in_result.get_xmin( )
|
xmin = in_result.get_xmin( )
|
||||||
xmax = in_result.get_xmax( )
|
xmax = in_result.get_xmax( )
|
||||||
@@ -3093,25 +3558,48 @@ class MonitorWidgets:
|
|||||||
self.matplot_axes.set_ylabel( "" )
|
self.matplot_axes.set_ylabel( "" )
|
||||||
|
|
||||||
# Any variables to be drawn on plot
|
# Any variables to be drawn on plot
|
||||||
if False:
|
description_lines = []
|
||||||
descriptions = None
|
if self.display_descriptions_checkbutton.get_active():
|
||||||
if isinstance(in_result, Accumulation):
|
if isinstance(in_result, Accumulation):
|
||||||
descriptions = in_result.common_descriptions
|
description_lines.append("n = %d" % in_result.n)
|
||||||
|
if in_result.common_descriptions is not None:
|
||||||
|
description_lines.extend(["%s = %s" % (key, in_result.common_descriptions[key]) for key in in_result.common_descriptions.keys()])
|
||||||
elif isinstance(in_result, ADC_Result):
|
elif isinstance(in_result, ADC_Result):
|
||||||
descriptions = in_result.description
|
if hasattr(in_result, "job_id") and in_result.job_id is not None:
|
||||||
|
description_lines.append("job_id = %s" % str(in_result.job_id))
|
||||||
|
if hasattr(in_result, "description") and in_result.description is not None:
|
||||||
|
description_lines.extend(["%s = %s" % (key, in_result.description[key]) for key in in_result.description.keys()])
|
||||||
|
|
||||||
|
if description_lines:
|
||||||
|
description_string = "\n".join(description_lines)
|
||||||
|
if self.description_text is None:
|
||||||
|
self.description_text = self.matplot_axes.text(0.98, 0.95, description_string,
|
||||||
|
size=10,
|
||||||
|
transform=self.matplot_axes.transAxes,
|
||||||
|
va='top', ha='right', ma='left',
|
||||||
|
bbox=dict(facecolor='lightgray', alpha=0.5, boxstyle='round,pad=0.2'))
|
||||||
else:
|
else:
|
||||||
pass
|
self.description_text.set_text(description_string)
|
||||||
#actual_config = self.config.get()
|
elif self.description_text is not None:
|
||||||
if (descriptions is not None):
|
try:
|
||||||
#print(actual_config['pretty_descriptions'])
|
self.description_text.remove()
|
||||||
description_string = ""
|
except:
|
||||||
for key in descriptions.keys():
|
pass
|
||||||
description_string += "%s = %s\n" % (key,descriptions[key])
|
self.description_text = None
|
||||||
self.matplot_axes.text(0.8,0.95, description_string[:-1],
|
|
||||||
size=10,
|
if getattr(in_result, "is_clipped", False):
|
||||||
transform=self.matplot_axes.transAxes,
|
if self.clipping_marker is None:
|
||||||
va='top',
|
self.clipping_marker = self.matplot_axes.text(0.5, 0.95, "ADC OVERFLOW",
|
||||||
backgroundcolor='white')
|
transform=self.matplot_axes.transAxes,
|
||||||
|
color="red", fontsize=20, fontweight="bold",
|
||||||
|
ha="center", va="top",
|
||||||
|
bbox=dict(facecolor='white', alpha=0.7, edgecolor='red'))
|
||||||
|
elif self.clipping_marker is not None:
|
||||||
|
try:
|
||||||
|
self.clipping_marker.remove()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.clipping_marker = None
|
||||||
|
|
||||||
# Draw it!
|
# Draw it!
|
||||||
self.matplot_canvas.draw_idle( )
|
self.matplot_canvas.draw_idle( )
|
||||||
@@ -3287,6 +3775,8 @@ class ScriptInterface:
|
|||||||
self.back_driver.start( )
|
self.back_driver.start( )
|
||||||
while (not self.back_driver.quit_flag.isSet( ) and \
|
while (not self.back_driver.quit_flag.isSet( ) and \
|
||||||
(self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)):
|
(self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)):
|
||||||
|
while gtk.events_pending():
|
||||||
|
gtk.main_iteration_do(False)
|
||||||
self.back_driver.quit_flag.wait( 0.1 )
|
self.back_driver.quit_flag.wait( 0.1 )
|
||||||
if self.exp_handling:
|
if self.exp_handling:
|
||||||
self.exp_handling.start( )
|
self.exp_handling.start( )
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import io
|
|||||||
import traceback
|
import traceback
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import ast
|
||||||
from damaris.experiments.Experiment import Quit
|
from damaris.experiments.Experiment import Quit
|
||||||
from damaris.experiments import Experiment
|
from damaris.experiments import Experiment
|
||||||
|
|
||||||
@@ -48,8 +49,32 @@ class ExperimentHandling(threading.Thread):
|
|||||||
self.raised_exception = None
|
self.raised_exception = None
|
||||||
self.location = None
|
self.location = None
|
||||||
exp_iterator=None
|
exp_iterator=None
|
||||||
|
|
||||||
|
# check for time.sleep()
|
||||||
|
try:
|
||||||
|
tree = ast.parse(self.script)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
found_blocking_sleep = False
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
func = node.func
|
||||||
|
if isinstance(func, ast.Attribute) and func.attr == 'sleep':
|
||||||
|
if isinstance(func.value, ast.Name) and func.value.id == 'time':
|
||||||
|
found_blocking_sleep = True
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module == 'time':
|
||||||
|
for alias in node.names:
|
||||||
|
if alias.name == 'sleep':
|
||||||
|
found_blocking_sleep = True
|
||||||
|
|
||||||
|
if found_blocking_sleep:
|
||||||
|
print("Warning: time.sleep() detected in experiment script. This is blocking and not interruptible. Please use sleep() instead.")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exec(self.script, dataspace)
|
exec(self.script, dataspace)
|
||||||
|
except StopExperiment:
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.raised_exception=e
|
self.raised_exception=e
|
||||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||||
@@ -61,6 +86,8 @@ class ExperimentHandling(threading.Thread):
|
|||||||
if "experiment" in dataspace:
|
if "experiment" in dataspace:
|
||||||
try:
|
try:
|
||||||
exp_iterator=dataspace["experiment"]()
|
exp_iterator=dataspace["experiment"]()
|
||||||
|
except StopExperiment:
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.raised_exception=e
|
self.raised_exception=e
|
||||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import traceback
|
import traceback
|
||||||
|
import ast
|
||||||
from damaris.data import Resultable
|
from damaris.data import Resultable
|
||||||
|
from damaris.gui.ExperimentHandling import StopExperiment
|
||||||
|
|
||||||
class ResultHandling(threading.Thread):
|
class ResultHandling(threading.Thread):
|
||||||
"""
|
"""
|
||||||
@@ -30,10 +32,35 @@ class ResultHandling(threading.Thread):
|
|||||||
del data_classes
|
del data_classes
|
||||||
dataspace["results"]=self
|
dataspace["results"]=self
|
||||||
dataspace["data"]=self.data_space
|
dataspace["data"]=self.data_space
|
||||||
|
dataspace["sleep"]=self.sleep
|
||||||
self.raised_exception=None
|
self.raised_exception=None
|
||||||
self.location = None
|
self.location = None
|
||||||
|
|
||||||
|
# check for time.sleep()
|
||||||
|
try:
|
||||||
|
tree = ast.parse(self.script)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
found_blocking_sleep = False
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
func = node.func
|
||||||
|
if isinstance(func, ast.Attribute) and func.attr == 'sleep':
|
||||||
|
if isinstance(func.value, ast.Name) and func.value.id == 'time':
|
||||||
|
found_blocking_sleep = True
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module == 'time':
|
||||||
|
for alias in node.names:
|
||||||
|
if alias.name == 'sleep':
|
||||||
|
found_blocking_sleep = True
|
||||||
|
|
||||||
|
if found_blocking_sleep:
|
||||||
|
print("Warning: time.sleep() detected in result script. This is blocking and not interruptible. Please use sleep() instead.")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exec(self.script, dataspace)
|
exec(self.script, dataspace)
|
||||||
|
except StopExperiment:
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.raised_exception=e
|
self.raised_exception=e
|
||||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||||
@@ -47,6 +74,8 @@ class ResultHandling(threading.Thread):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
dataspace["result"]()
|
dataspace["result"]()
|
||||||
|
except StopExperiment:
|
||||||
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.raised_exception=e
|
self.raised_exception=e
|
||||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||||
@@ -56,6 +85,11 @@ class ResultHandling(threading.Thread):
|
|||||||
traceback_file=None
|
traceback_file=None
|
||||||
dataspace=None
|
dataspace=None
|
||||||
|
|
||||||
|
def sleep(self, seconds):
|
||||||
|
self.quit_flag.wait(seconds)
|
||||||
|
if self.quit_flag.isSet():
|
||||||
|
raise StopExperiment
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
if self.quit_flag.isSet():
|
if self.quit_flag.isSet():
|
||||||
self.results=None
|
self.results=None
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ class ResultReader:
|
|||||||
self.result.index = [(0,tmp_size-1)]
|
self.result.index = [(0,tmp_size-1)]
|
||||||
tmp_index += tmp_size
|
tmp_index += tmp_size
|
||||||
self.result.cont_data=True
|
self.result.cont_data=True
|
||||||
|
self.result.check_clipping()
|
||||||
tmp_part = None
|
tmp_part = None
|
||||||
|
|
||||||
def __parseFile_expat(self, in_file):
|
def __parseFile_expat(self, in_file):
|
||||||
@@ -310,6 +311,7 @@ class ResultReader:
|
|||||||
self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1))
|
self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1))
|
||||||
tmp_sample_counter+=tmp_size
|
tmp_sample_counter+=tmp_size
|
||||||
self.result.cont_data=True
|
self.result.cont_data=True
|
||||||
|
self.result.check_clipping()
|
||||||
|
|
||||||
# Callback when a xml start tag is found
|
# Callback when a xml start tag is found
|
||||||
def __xmlStartTagFound(self, in_name, in_attribute):
|
def __xmlStartTagFound(self, in_name, in_attribute):
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Temperature monitoring thread.
|
||||||
|
|
||||||
|
This module provides a thread that periodically reads temperature from a controller
|
||||||
|
and stores it in a TemperatureResult object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from damaris.data import TemperatureResult
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Thread that periodically reads temperature from a controller
|
||||||
|
and stores it in a TemperatureResult object.
|
||||||
|
|
||||||
|
This thread runs independently and can be configured with any
|
||||||
|
TemperatureController implementation (Eurotherm, Lakeshore, etc.).
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
controller: The TemperatureController instance to read from.
|
||||||
|
interval: Polling interval in seconds.
|
||||||
|
result: The TemperatureResult object storing collected data.
|
||||||
|
quit_flag: Event to signal the thread to stop.
|
||||||
|
data_pool: Optional DataPool to store results.
|
||||||
|
data_key: Key to use when storing in data_pool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, controller: TemperatureController,
|
||||||
|
interval: float = 1.0,
|
||||||
|
data_pool=None,
|
||||||
|
data_key: str = "temperature"):
|
||||||
|
"""
|
||||||
|
Initialize the temperature monitor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
controller: TemperatureController implementation (Eurotherm, etc.)
|
||||||
|
interval: Polling interval in seconds (default: 1.0)
|
||||||
|
data_pool: Optional DataPool to store results (default: None)
|
||||||
|
data_key: Key to use when storing in data_pool (default: "temperature")
|
||||||
|
"""
|
||||||
|
threading.Thread.__init__(self, name="TemperatureMonitor")
|
||||||
|
self.controller = controller
|
||||||
|
self.interval = float(interval)
|
||||||
|
self.data_pool = data_pool
|
||||||
|
self.data_key = data_key
|
||||||
|
self.quit_flag = threading.Event()
|
||||||
|
self.result = TemperatureResult()
|
||||||
|
self._running = False
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
|
# Initialize the result with metadata
|
||||||
|
self.result.xlabel = "Time (s)"
|
||||||
|
self.result.ylabel = "Temperature (C)"
|
||||||
|
self.result.description = {"type": "temperature_monitoring"}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Main thread loop: periodically reads temperature and stores it.
|
||||||
|
"""
|
||||||
|
self._running = True
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while not self.quit_flag.is_set() and self._running:
|
||||||
|
try:
|
||||||
|
# Read temperature and setpoint
|
||||||
|
timestamp = time.time() - start_time
|
||||||
|
current_temp = self.controller.get_temperature()
|
||||||
|
setpoint = self.controller.get_setpoint()
|
||||||
|
|
||||||
|
# Store in result
|
||||||
|
with self._lock:
|
||||||
|
if not hasattr(self.result, 'x'):
|
||||||
|
self.result.x = []
|
||||||
|
self.result.y = []
|
||||||
|
self.result.setpoint = []
|
||||||
|
|
||||||
|
self.result.x.append(timestamp)
|
||||||
|
self.result.y.append(current_temp)
|
||||||
|
self.result.setpoint.append(setpoint)
|
||||||
|
|
||||||
|
# Update job metadata on first sample
|
||||||
|
if len(self.result.x) == 1:
|
||||||
|
self.result.job_date = datetime.datetime.now()
|
||||||
|
self.result.job_id = id(self)
|
||||||
|
|
||||||
|
# Store result in data_pool (thread-safe for GTK)
|
||||||
|
# Replace the entry on each update to trigger DataPool events
|
||||||
|
if self.data_pool is not None:
|
||||||
|
with self._lock:
|
||||||
|
# Create a copy for the data_pool with current data
|
||||||
|
data_pool_result = TemperatureResult()
|
||||||
|
data_pool_result.xlabel = self.result.xlabel
|
||||||
|
data_pool_result.ylabel = self.result.ylabel
|
||||||
|
data_pool_result.description = self.result.description.copy() if isinstance(self.result.description, dict) else {}
|
||||||
|
# Copy the data from self.result
|
||||||
|
data_pool_result.x = list(self.result.x) if hasattr(self.result, 'x') else []
|
||||||
|
data_pool_result.y = list(self.result.y) if hasattr(self.result, 'y') else []
|
||||||
|
data_pool_result.setpoint = list(self.result.setpoint) if hasattr(self.result, 'setpoint') else []
|
||||||
|
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
|
||||||
|
|
||||||
|
# Store in data_pool in GTK thread (if GTK available)
|
||||||
|
gdk = _get_gdk()
|
||||||
|
if gdk is not None:
|
||||||
|
gdk.threads_enter()
|
||||||
|
try:
|
||||||
|
self.data_pool[self.data_key] = data_pool_result
|
||||||
|
finally:
|
||||||
|
gdk.threads_leave()
|
||||||
|
else:
|
||||||
|
self.data_pool[self.data_key] = data_pool_result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"TemperatureMonitor error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Sleep in small increments for responsive shutdown
|
||||||
|
sleep_remaining = self.interval
|
||||||
|
while sleep_remaining > 0 and not self.quit_flag.is_set() and self._running:
|
||||||
|
sleep_time = min(0.1, sleep_remaining)
|
||||||
|
self.quit_flag.wait(sleep_time)
|
||||||
|
sleep_remaining -= sleep_time
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Graceful shutdown: signal the thread to stop and wait for it.
|
||||||
|
"""
|
||||||
|
self._running = False
|
||||||
|
self.quit_flag.set()
|
||||||
|
# Wait for the thread to finish, with a generous timeout
|
||||||
|
# Use a longer timeout to account for potential delays
|
||||||
|
if self.is_alive():
|
||||||
|
self.join(timeout=5.0) # Wait up to 5 seconds
|
||||||
|
if self.is_alive():
|
||||||
|
print("Warning: TemperatureMonitor thread did not stop gracefully")
|
||||||
|
# Thread is still running, but we can't force stop it
|
||||||
|
# The next start will check if it's alive and handle it
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Clear the collected data and start fresh.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self.result = TemperatureResult()
|
||||||
|
self.result.xlabel = "Time (s)"
|
||||||
|
self.result.ylabel = "Temperature (C)"
|
||||||
|
self.result.description = {"type": "temperature_monitoring"}
|
||||||
|
|
||||||
|
def get_current_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the latest temperature reading.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Latest temperature in Celsius, or 0.0 if no data.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if hasattr(self.result, 'y') and len(self.result.y) > 0:
|
||||||
|
return self.result.y[-1]
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_current_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the latest setpoint reading.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Latest setpoint in Celsius, or 0.0 if no data.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if hasattr(self.result, 'setpoint') and len(self.result.setpoint) > 0:
|
||||||
|
return self.result.setpoint[-1]
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the monitor is currently running.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the monitor thread is active.
|
||||||
|
"""
|
||||||
|
return self._running and not self.quit_flag.is_set()
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
|
|
||||||
<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd">
|
|
||||||
|
|
||||||
<glade-project>
|
|
||||||
<name>damaris-gui</name>
|
|
||||||
<program_name>damaris</program_name>
|
|
||||||
<gnome_support>FALSE</gnome_support>
|
|
||||||
</glade-project>
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,21 @@ Public License instead of this License.
|
|||||||
<property name="homogeneous">True</property>
|
<property name="homogeneous">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkToolButton" id="toolbar_open_hdf5_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">Open DAMARIS HDF5 file</property>
|
||||||
|
<property name="label">Open HDF5</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="icon-name">document-open</property>
|
||||||
|
<signal name="clicked" handler="on_toolbar_open_hdf5_button_clicked" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="homogeneous">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkToolButton" id="toolbar_save_file_button">
|
<object class="GtkToolButton" id="toolbar_save_file_button">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import re
|
|||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ def checksum(message):
|
|||||||
bcc = (reduce(operator.xor, list(map(ord,message))))
|
bcc = (reduce(operator.xor, list(map(ord,message))))
|
||||||
return chr(bcc)
|
return chr(bcc)
|
||||||
|
|
||||||
class Eurotherm(object):
|
class Eurotherm(TemperatureController):
|
||||||
def __init__(self, serial_device, baudrate = 19200):
|
def __init__(self, serial_device, baudrate = 19200):
|
||||||
self.device = standard_device
|
self.device = standard_device
|
||||||
# timeout: 110 ms to get all answers.
|
# timeout: 110 ms to get all answers.
|
||||||
@@ -100,6 +102,39 @@ class Eurotherm(object):
|
|||||||
def get_setpoint_temperature(self):
|
def get_setpoint_temperature(self):
|
||||||
return self.read_param('SL')
|
return self.read_param('SL')
|
||||||
|
|
||||||
|
# TemperatureController interface implementation
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""Read current temperature in Celsius."""
|
||||||
|
temp = self.read_param('PV')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""Read target setpoint temperature in Celsius."""
|
||||||
|
temp = self.read_param('SL')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""Set target temperature in Celsius."""
|
||||||
|
return self.write_param('SL', str(temperature))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the serial connection."""
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
self.s = None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import time
|
import time
|
||||||
delta=5
|
delta=5
|
||||||
@@ -118,3 +153,6 @@ if __name__ == '__main__':
|
|||||||
f.write(l)
|
f.write(l)
|
||||||
f.flush()
|
f.flush()
|
||||||
f.write('# MARK -- %s --\n'%(time.asctime()))
|
f.write('# MARK -- %s --\n'%(time.asctime()))
|
||||||
|
|
||||||
|
# Register Eurotherm with the factory
|
||||||
|
register_controller('eurotherm', Eurotherm)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import serial
|
||||||
|
import re
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL)
|
||||||
|
# example answer '\x02PV279.8\x03/'
|
||||||
|
# [EOT] = \x04
|
||||||
|
# [STX] = \x02
|
||||||
|
# [ENQ] = \x05
|
||||||
|
# [ETX] = \x03
|
||||||
|
# [ACK] = \x06
|
||||||
|
# BCC = checksum
|
||||||
|
|
||||||
|
standard_device = '/dev/ttyUSB0'
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parameter read example:
|
||||||
|
|
||||||
|
Master: [EOT]0011PV[ENQ]
|
||||||
|
Instrument: [STX]PV16.4[ETX]{BCC}
|
||||||
|
|
||||||
|
Writing data:
|
||||||
|
|
||||||
|
Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)<DATA>[ETX](BCC)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Lakeshore(TemperatureController):
|
||||||
|
def __init__(self, serial_device, baudrate=57600):
|
||||||
|
self.device = standard_device
|
||||||
|
# timeout: 110 ms to get all answers.
|
||||||
|
self.s = serial.Serial(serial_device,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=7,
|
||||||
|
parity='O',
|
||||||
|
stopbits=1,
|
||||||
|
timeout=0.4)
|
||||||
|
self._expect_len = 50
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
|
||||||
|
#def get_current_temperature(self): # Temp A
|
||||||
|
# self.s.write("KRDG? a\n")
|
||||||
|
# a = self.s.readline().strip()[1:]
|
||||||
|
# return float(a)
|
||||||
|
|
||||||
|
def get_temperature_A(self): # Temp A
|
||||||
|
self.s.write("KRDG? a\n")
|
||||||
|
a = self.s.readline().strip()[1:]
|
||||||
|
return float(a)
|
||||||
|
|
||||||
|
def get_temperature_B(self): # Temp B
|
||||||
|
self.s.write("KRDG? b\n")
|
||||||
|
b = self.s.readline().strip()[1:]
|
||||||
|
return float(b)
|
||||||
|
|
||||||
|
def set_temperature(self, temperature): # setze Setpoint
|
||||||
|
t = temperature
|
||||||
|
self.s.write(f"SETP 1,{t}\n")
|
||||||
|
|
||||||
|
def set_ramp(self, ramp):
|
||||||
|
return self.s.write(f'RAMP 1, 1, {ramp}\n')
|
||||||
|
|
||||||
|
def get_set_temperature(self): # Setpoint abfrage
|
||||||
|
self.s.write("SETP? 1\n")
|
||||||
|
s = self.s.readline().strip()[1:]
|
||||||
|
return float(s)
|
||||||
|
|
||||||
|
def get_output(self): # Heater Output
|
||||||
|
self.s.write("HTR? 1\n")
|
||||||
|
o = self.s.readline().strip()[1:]
|
||||||
|
return float(o)
|
||||||
|
|
||||||
|
def get_ramp(self):
|
||||||
|
self.s.write('RAMP? 1\n')
|
||||||
|
ret = self.s.readline().strip()
|
||||||
|
onoff, rate = ret.split(',')
|
||||||
|
if onoff:
|
||||||
|
return float(rate)
|
||||||
|
else:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def read_param(self, param):
|
||||||
|
"""Read a parameter using simple ASCII commands."""
|
||||||
|
self.s.write(f"{param}?\n")
|
||||||
|
answer = self.s.readline().strip()
|
||||||
|
if answer and len(answer) > 1:
|
||||||
|
# Remove the first character (command echo) and return the value
|
||||||
|
return answer[1:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_value(self, name):
|
||||||
|
temp = self.read_param(name)
|
||||||
|
if temp is None:
|
||||||
|
return np.nan
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except ValueError:
|
||||||
|
return np.nan
|
||||||
|
|
||||||
|
# TemperatureController interface implementation
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""Read current temperature in Celsius."""
|
||||||
|
temp = self.read_param('KRDG')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""Read target setpoint temperature in Celsius."""
|
||||||
|
temp = self.read_param('SETP')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""Set target temperature in Celsius."""
|
||||||
|
self.s.write(f"SETP {temperature}\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the serial connection."""
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
self.s = None
|
||||||
|
|
||||||
|
|
||||||
|
# Register Lakeshore with the factory
|
||||||
|
register_controller('lakeshore', Lakeshore)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Temperature controller interface and base classes.
|
||||||
|
|
||||||
|
This module provides a consistent interface for temperature controllers
|
||||||
|
to enable pluggable temperature monitoring systems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
# Plugin registry for temperature controllers
|
||||||
|
_controller_registry = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_controller(name: str, controller_class):
|
||||||
|
"""
|
||||||
|
Register a temperature controller implementation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique identifier for this controller type
|
||||||
|
controller_class: Class that implements TemperatureController
|
||||||
|
"""
|
||||||
|
_controller_registry[name] = controller_class
|
||||||
|
|
||||||
|
|
||||||
|
def create_controller(name: str, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a temperature controller instance by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Registered controller type name
|
||||||
|
*args: Positional arguments to pass to controller constructor
|
||||||
|
**kwargs: Keyword arguments to pass to controller constructor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemperatureController: Instance of the requested controller
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If controller name is not registered and cannot be imported
|
||||||
|
"""
|
||||||
|
# If not found, try to auto-import the module
|
||||||
|
if name not in _controller_registry:
|
||||||
|
# Map known controller names to their modules
|
||||||
|
module_map = {
|
||||||
|
'simulated': 'damaris.tools.temperature_simulator',
|
||||||
|
'eurotherm': 'damaris.tools.eurotherm',
|
||||||
|
'lakeshore': 'damaris.tools.lakeshore',
|
||||||
|
}
|
||||||
|
if name in module_map:
|
||||||
|
try:
|
||||||
|
# Import the module which will register the controller
|
||||||
|
import importlib
|
||||||
|
importlib.import_module(module_map[name])
|
||||||
|
except ImportError as e:
|
||||||
|
raise ValueError(f"Failed to import controller '{name}': {e}. Available: {list(_controller_registry.keys())}")
|
||||||
|
|
||||||
|
# Check again after import
|
||||||
|
if name not in _controller_registry:
|
||||||
|
raise ValueError(f"Unknown temperature controller: {name}. Available: {list(_controller_registry.keys())}")
|
||||||
|
|
||||||
|
return _controller_registry[name](*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_controller_names() -> list:
|
||||||
|
"""
|
||||||
|
Get list of available controller names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of registered controller type names
|
||||||
|
"""
|
||||||
|
return list(_controller_registry.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# Pre-register built-in controllers by importing their modules
|
||||||
|
# This ensures they're available without explicit imports
|
||||||
|
try:
|
||||||
|
from . import eurotherm
|
||||||
|
except ImportError as e:
|
||||||
|
# Eurotherm may require serial module which might not be installed
|
||||||
|
# That's OK, it will be imported on-demand if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import lakeshore
|
||||||
|
except ImportError as e:
|
||||||
|
# Lakeshore may require serial module which might not be installed
|
||||||
|
# That's OK, it will be imported on-demand if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import temperature_simulator
|
||||||
|
except ImportError as e:
|
||||||
|
# Simulator should always work (no external dependencies)
|
||||||
|
# But if it fails, it will be imported on-demand
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureController(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for temperature controllers.
|
||||||
|
|
||||||
|
All temperature controller implementations (Eurotherm, Lakeshore, etc.)
|
||||||
|
should inherit from this class and implement the abstract methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Read current temperature in Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Current temperature in degrees Celsius.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IOError: If communication with the device fails.
|
||||||
|
RuntimeError: If the device is not ready or returns invalid data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Read the target setpoint temperature in Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Target temperature in degrees Celsius.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set the target temperature setpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: Target temperature in degrees Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the setpoint was successfully set, False otherwise.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Clean up resources (e.g., close serial connections).
|
||||||
|
|
||||||
|
This method should be called when the controller is no longer needed.
|
||||||
|
The default implementation does nothing; subclasses should override
|
||||||
|
if they have resources to clean up.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Simulated temperature controller for testing.
|
||||||
|
|
||||||
|
Provides a temperature controller that simulates real temperature readings
|
||||||
|
with configurable parameters including sinusoidal variation, noise, and missing values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
|
class SimulatedTemperatureController(TemperatureController):
|
||||||
|
"""
|
||||||
|
A simulated temperature controller that generates realistic temperature readings.
|
||||||
|
|
||||||
|
This controller is useful for testing and development without requiring
|
||||||
|
physical hardware. It simulates temperature readings with:
|
||||||
|
- Base temperature around 290K (17C)
|
||||||
|
- Sinusoidal variation with amplitude of 10K
|
||||||
|
- 5% random noise
|
||||||
|
- 10% probability of missing values (returns previous value)
|
||||||
|
- 60 second period for the sine wave
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||||
|
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||||
|
period: Period of sine wave in seconds (default: 60.0)
|
||||||
|
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||||
|
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_temp: float = 290.0, amplitude: float = 10.0,
|
||||||
|
period: float = 60.0, noise_level: float = 0.05,
|
||||||
|
missing_probability: float = 0.10):
|
||||||
|
"""
|
||||||
|
Initialize the simulated temperature controller.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||||
|
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||||
|
period: Period of sine wave in seconds (default: 60.0)
|
||||||
|
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||||
|
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||||
|
"""
|
||||||
|
self.base_temp = float(base_temp)
|
||||||
|
self.amplitude = float(amplitude)
|
||||||
|
self.period = float(period)
|
||||||
|
self.noise_level = float(noise_level)
|
||||||
|
self.missing_probability = float(missing_probability)
|
||||||
|
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._setpoint = self.base_temp
|
||||||
|
self._last_temp = self.base_temp
|
||||||
|
self._last_reading_time = self._start_time
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Get simulated current temperature in Kelvin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Simulated temperature in Kelvin
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
elapsed = time.time() - self._start_time
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Calculate ideal sinusoidal temperature
|
||||||
|
temp = self.base_temp + self.amplitude * math.sin(2 * math.pi * elapsed / self.period)
|
||||||
|
|
||||||
|
# Add 5% random noise
|
||||||
|
noise = temp * self.noise_level * (random.random() * 2 - 1) # -5% to +5%
|
||||||
|
temp += noise
|
||||||
|
|
||||||
|
# 10% chance of missing value (return last good value)
|
||||||
|
if random.random() < self.missing_probability:
|
||||||
|
temp = self._last_temp
|
||||||
|
else:
|
||||||
|
self._last_temp = temp
|
||||||
|
self._last_reading_time = current_time
|
||||||
|
|
||||||
|
return temp
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the target setpoint temperature in Kelvin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Target temperature in Kelvin
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._setpoint
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set the target temperature setpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: Target temperature in Kelvin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._setpoint = float(temperature)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Clean up resources."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset the simulation to initial state."""
|
||||||
|
with self._lock:
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._last_temp = self.base_temp
|
||||||
|
self._last_reading_time = self._start_time
|
||||||
|
|
||||||
|
|
||||||
|
# Register the simulated controller with the factory
|
||||||
|
register_controller('simulated', SimulatedTemperatureController)
|
||||||
+1
-1
@@ -1,4 +1,5 @@
|
|||||||
import tables
|
import tables
|
||||||
|
import numpy
|
||||||
|
|
||||||
r = MeasurementResult("test")
|
r = MeasurementResult("test")
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ def result():
|
|||||||
data["test"] = r
|
data["test"] = r
|
||||||
accu+=ts
|
accu+=ts
|
||||||
accu.write_to_hdf(h5, "/", "name", "title")
|
accu.write_to_hdf(h5, "/", "name", "title")
|
||||||
print(accu)
|
|
||||||
try:
|
try:
|
||||||
h5.close()
|
h5.close()
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class TestAccumulation(unittest.TestCase):
|
|||||||
self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30])))
|
self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30])))
|
||||||
self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35])))
|
self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35])))
|
||||||
self.assertEqual(sub_result.index, [(0, 1)])
|
self.assertEqual(sub_result.index, [(0, 1)])
|
||||||
# TODO Fix #13: self.assertEqual(sub_result.common_descriptions, {"key": "value"})
|
self.assertEqual(sub_result.common_descriptions, {"key": "value"})
|
||||||
|
|
||||||
def test_write_to_csv_without_error(self):
|
def test_write_to_csv_without_error(self):
|
||||||
"""Test the functionality of writing to CSV."""
|
"""Test the functionality of writing to CSV."""
|
||||||
|
|||||||
Reference in new issue
Block a user