34 Commits
Author SHA1 Message Date
markusro 75863ffc19 fixed lakeshore to keep using the simple ASCII commands
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m22s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m24s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-12 20:15:50 +02:00
markusro 6f0b090d9d added lakeshore to temperature logging facility 2026-07-12 20:09:07 +02:00
markusro d9f2cc3f2c added lakeshore temperature controller 2026-07-12 20:00:39 +02:00
markusro e9e3f5a5f5 write Temperature data to hdf
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m12s
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m18s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-12 13:14:05 +02:00
markusro 9441b5a4d9 added ignore 2026-07-12 13:00:14 +02:00
markusro 7f29f2d173 fix: replace ComboBoxText.set_active_text() with model-based lookup
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m8s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m13s
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 13m14s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 13m53s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
set_active_text() does not exist in GTK 3.24.x (only in GTK 4.x).
When loading a saved config with temperature_controller_type='simulated',
the set() method crashed on startup with AttributeError.

Added _set_combo_active_text() helper that iterates the model to find
the matching entry and calls set_active(index) instead.
2026-07-12 10:11:58 +02:00
markusro 847ab4d89c working prototype: stopping with experiment, display data 2026-07-12 02:16:57 +02:00
markusro d6da2f2bda fix thread locking when reading temeprature 2026-07-11 22:18:08 +02:00
markusro 5338304ee8 added POC for temperature logging 2026-07-11 22:11:03 +02:00
markusro d48f96e498 added scipy and numpy to pyproject.toml, clarified dependencies when using uv run DAMARIS3 2026-07-11 22:08:17 +02:00
markusro 058716f3c0 merged sequential writing
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m30s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m7s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-09 16:53:31 +02:00
markusro 6d9f55f788 implemented incremental HDF5 writing (Fixes #33) 2026-07-09 16:49:51 +02:00
markusro e8d0e45bfb doc string
Build Debian Packages / build (trixie, debian13) (push) Successful in 12m51s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m2s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-09 16:42:03 +02:00
markusro 08e1fe2a1a Small fixes
Merge branch 'main' of gitea.pkm.physik.tu-darmstadt.de:IPKM/python3-damaris
2026-07-09 14:45:28 +02:00
markusro 064036241b small fixes 2026-07-09 14:45:25 +02:00
markusro 97aa99c239 more files to ignore
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m34s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m35s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 21:01:54 +02:00
markusro f26d203e79 ignore debian/files 2026-07-06 21:01:19 +02:00
markusro 594c0ee8d4 clean up residual gui files
Build Debian Packages / build (trixie, debian13) (push) Successful in 15m18s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 15m11s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 20:56:17 +02:00
markusro 68da261ab4 remove load successful message from hdf5 loader 2026-07-06 20:48:21 +02:00
markusro 00466cb559 Merge pull request 'Hdfreader' (#31) from hdfreader into main
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m56s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m24s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
Reviewed-on: #31
2026-07-06 18:45:46 +00:00
markusro 7ba5c7d56a added HDF5 read capability. Closes #27
Build Debian Packages / build (trixie, debian13) (pull_request) Successful in 14m37s
Build Debian Packages / build (bookworm, debian12) (pull_request) Successful in 14m50s
Build Debian Packages / build (bullseye, debian11) (pull_request) Has been cancelled
2026-07-06 20:42:04 +02:00
markusro c181e15aa8 fixed some python2 to python3 changes as well as dat time formats 2026-07-06 20:40:59 +02:00
markusro 26970ba201 clean up write_to_csv for MeasurmentResult (Fixs #30)
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m56s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m7s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 16:55:49 +02:00
markusro eab49db8d4 gtksourceview: minimum-word-size needs to be >= 2 otherwise a warning is triggered. (Fixes #28)
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m56s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m3s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-06 16:10:56 +02:00
markusro 3bfb2670eb initial hdfreader implementation 2026-07-05 19:36:20 +02:00
markusro 83a01f1d83 new release
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m25s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m22s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 14:13:32 +02:00
markusro b46debad69 Fixed #13 and #14
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m2s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m11s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 14:07:57 +02:00
markusro 4250039b3c can now set ADC_Result bit depth in the config window (Bug #16)
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m3s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m4s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 13:59:09 +02:00
markusro d3b1a9e3fd Show message if ADC_Data is clipped (solves #16)
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m38s
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m40s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 13:44:20 +02:00
markusro 06867ec17b Missing search input field fixed (Fixes #25)
Build Debian Packages / build (bookworm, debian12) (push) Successful in 14m5s
Build Debian Packages / build (trixie, debian13) (push) Successful in 14m7s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-05 13:24:20 +02:00
markusro 80d8f1e78d fixed stop button, no can interrupt running experiment
Build Debian Packages / build (trixie, debian13) (push) Successful in 15m23s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 15m46s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-04 21:03:23 +02:00
markusro 58e7846bbe description is plotted better now 2026-07-04 20:50:57 +02:00
markusro 1c4f8f5344 Improved interruptable sleep and warn user when using time.sleep.
Build Debian Packages / build (trixie, debian13) (push) Successful in 13m4s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m9s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-04 20:26:14 +02:00
markusro a3738025b7 fixed autocomplete in the editor windows
Build Debian Packages / build (trixie, debian13) (push) Successful in 12m59s
Build Debian Packages / build (bookworm, debian12) (push) Successful in 13m9s
Build Debian Packages / build (bullseye, debian11) (push) Has been cancelled
2026-07-04 20:01:34 +02:00
29 changed files with 2055 additions and 4460 deletions

No files matched your search

+5
View File
@@ -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
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -1,3 +1,12 @@
python3-damaris (0.22) stable; urgency=medium
* fixed autocomplete in the editor tabs
* added clipping warning on ADC overload, ADC bit-depth settable in config
* Accumulation object keeps track of job_ids
* get_result_by_index keeps common description
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 05 Jul 2026 14:09:34 +0200
python3-damaris (0.20) stable; urgency=medium python3-damaris (0.20) stable; urgency=medium
* Unified version management across Python app, GUI and documentation * Unified version management across Python app, GUI and documentation
-2
View File
@@ -1,2 +0,0 @@
python3-damaris_0.20_all.deb science optional
python3-damaris_0.20_amd64.buildinfo science optional
+5 -4
View File
@@ -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__"}
+1 -1
View File
@@ -15,5 +15,5 @@
## module contents ## module contents
# #
__version__ = "0.20" __version__ = "0.22"
__all__=["experiments", "data", "gui"] __all__=["experiments", "data", "gui"]
+44 -1
View File
@@ -19,6 +19,7 @@ import tables
############################################################################# #############################################################################
class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath): class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
default_bit_depth = 14
""" """
Represents the result of an ADC, encapsulating data and metadata Represents the result of an ADC, encapsulating data and metadata
for processing, visualization, and export. for processing, visualization, and export.
@@ -50,6 +51,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
self.ylabel = "Samples [Digits]" self.ylabel = "Samples [Digits]"
self.lock=threading.RLock() self.lock=threading.RLock()
self.nChannels = 0 self.nChannels = 0
self.is_clipped = False
# using no argument for initialization # using no argument for initialization
if (x is None) and (y is None) and (index is None) and (sampl_freq is None) and (desc is None) and (job_id is None) and (job_date is None): if (x is None) and (y is None) and (index is None) and (sampl_freq is None) and (desc is None) and (job_id is None) and (job_date is None):
@@ -107,6 +109,44 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
return self.cont_data return self.cont_data
def check_clipping(self, bit_depth=None):
"""
Check if any data in y is clipped.
For a 14-bit ADC, clipping occurs at 2**13 (8192) or 2**13-1 (8191).
In general, for a bit_depth-bit signed ADC, limits are 2**(bit_depth-1) and -(2**(bit_depth-1)).
"""
if not self.contains_data():
return False
if bit_depth is None:
# try to get bit_depth from description
if hasattr(self, "description") and self.description is not None:
if "adc_bit_depth" in self.description:
try:
bit_depth = int(self.description["adc_bit_depth"])
except (ValueError, TypeError):
bit_depth = ADC_Result.default_bit_depth
else:
bit_depth = ADC_Result.default_bit_depth
else:
bit_depth = ADC_Result.default_bit_depth
limit_pos = 2**(bit_depth-1) - 1
limit_neg = -2**(bit_depth-1)
self.lock.acquire()
try:
self.is_clipped = False
for ch_data in self.y:
if numpy.any(ch_data >= limit_pos) or numpy.any(ch_data <= limit_neg):
self.is_clipped = True
break
finally:
self.lock.release()
return self.is_clipped
def add_sample_space(self, samples): def add_sample_space(self, samples):
"Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\"" "Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
@@ -143,7 +183,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(self.y[i][start:end+1].copy()) tmp_y.append(self.y[i][start:end+1].copy())
r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description, job_id = self.job_id, job_date = self.job_date) r = ADC_Result(x = tmp_x, y = tmp_y, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, desc = self.description.copy() if self.description is not None else None, job_id = self.job_id, job_date = self.job_date)
self.lock.release() self.lock.release()
return r return r
@@ -536,6 +576,8 @@ def read_from_hdf(hdf_node):
# job id and x,y titles are missing # job id and x,y titles are missing
adc=ADC_Result() adc=ADC_Result()
if hdf_node._v_title:
adc.set_title(hdf_node._v_title)
# populate description dictionary # populate description dictionary
adc.description={} adc.description={}
for attrname in hdf_node._v_attrs._v_attrnamesuser: for attrname in hdf_node._v_attrs._v_attrnamesuser:
@@ -573,4 +615,5 @@ def read_from_hdf(hdf_node):
for ch in range(adc_data.shape[1]): for ch in range(adc_data.shape[1]):
adc.y.append(adc_data[:,ch]) adc.y.append(adc_data[:,ch])
adc.check_clipping()
return adc return adc
+96 -25
View File
@@ -61,6 +61,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.xlabel = "Time / s" self.xlabel = "Time / s"
self.ylabel = "Avg. Samples [Digits]" self.ylabel = "Avg. Samples [Digits]"
self.lock=threading.RLock() self.lock=threading.RLock()
self.is_clipped = False
self.common_descriptions=None self.common_descriptions=None
self.time_period=[] self.time_period=[]
@@ -117,9 +118,23 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
tmp_y.append(self.y[i][start:end+1]) tmp_y.append(self.y[i][start:end+1])
r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error) r = Accumulation(x = tmp_x, y = tmp_y, n = self.n, index = [(0,len(tmp_y[0])-1)], sampl_freq = self.sampling_rate, error = self.use_error)
if self.uses_statistics():
r.y_square = []
for i in range(self.get_number_of_channels()):
r.y_square.append(self.y_square[i][start:end+1])
if self.common_descriptions is not None:
r.common_descriptions = self.common_descriptions.copy()
r.time_period = self.time_period[:]
r.job_ids = self.job_ids.copy()
self.lock.release() self.lock.release()
return r return r
def get_result_by_index(self, index):
return self.get_accu_by_index(index)
def get_Result_by_index(self, index):
return self.get_accu_by_index(index)
def get_ysquare(self, channel): def get_ysquare(self, channel):
if self.uses_statistics(): if self.uses_statistics():
try: try:
@@ -616,6 +631,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n, index = self.index, sampl_freq = self.sampling_rate, error = self.use_error)
r.is_clipped = self.is_clipped
r.job_id = self.job_id r.job_id = self.job_id
r.job_ids = self.job_ids.copy() r.job_ids = self.job_ids.copy()
r.time_period = self.time_period[:] if self.time_period is not None else None r.time_period = self.time_period[:] if self.time_period is not None else None
@@ -645,6 +661,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = 1, index = other.index, sampl_freq = other.sampling_rate, error = True) r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = 1, index = other.index, sampl_freq = other.sampling_rate, error = True)
else: else:
r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False) r = Accumulation(x = numpy.array(other.x, dtype="float32"), y = tmp_y, index = other.index, sampl_freq = other.sampling_rate, n = 1, error = False)
if hasattr(other, "is_clipped"):
r.is_clipped = other.is_clipped
r.time_period=[other.job_date,other.job_date] r.time_period=[other.job_date,other.job_date]
r.job_id = other.job_id r.job_id = other.job_id
r.common_descriptions=other.description.copy() r.common_descriptions=other.description.copy()
@@ -678,6 +696,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = True) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = True)
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = self.n + 1, index = self.index, sampl_freq = self.sampling_rate, error = False)
r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped)
r.time_period=[min(self.time_period[0],other.job_date), r.time_period=[min(self.time_period[0],other.job_date),
max(self.time_period[1],other.job_date)] max(self.time_period[1],other.job_date)]
r.job_id = other.job_id r.job_id = other.job_id
@@ -744,13 +763,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
for i in range(self.get_number_of_channels()): for i in range(self.get_number_of_channels()):
tmp_y.append(self.y[i] + other.y[i]) tmp_y.append(self.y[i] + other.y[i])
tmp_ysquare.append(self.y_square[i] + other.y_square[i]) if self.uses_statistics():
tmp_ysquare.append(self.y_square[i] + other.y_square[i])
if self.uses_statistics(): if self.uses_statistics():
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = True) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, y_2 = tmp_ysquare, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = True)
else: else:
r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False) r = Accumulation(x = numpy.array(self.x, dtype="float32"), y = tmp_y, n = other.n + self.n, index = self.index, sampl_freq = self.sampling_rate, error = False)
r.is_clipped = self.is_clipped or (hasattr(other, "is_clipped") and other.is_clipped)
r.time_period=[min(self.time_period[0],other.time_period[0]), r.time_period=[min(self.time_period[0],other.time_period[0]),
max(self.time_period[1],other.time_period[1])] max(self.time_period[1],other.time_period[1])]
r.job_id = other.job_id r.job_id = other.job_id
@@ -954,6 +974,8 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
self.set_title(self.__title_pattern % self.n) self.set_title(self.__title_pattern % self.n)
self.lock.release() self.lock.release()
if hasattr(other, "is_clipped"):
self.is_clipped = other.is_clipped
self.time_period=[other.job_date,other.job_date] self.time_period=[other.job_date,other.job_date]
self.job_id = other.job_id # added by Oleg Petrov self.job_id = other.job_id # added by Oleg Petrov
self.job_ids[other.job_id] = other.get_job_date() self.job_ids[other.job_id] = other.get_job_date()
@@ -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
View File
@@ -7,6 +7,7 @@ import collections
import threading import threading
import traceback import traceback
import io import io
import numpy
class DataPool(collections.abc.MutableMapping): class DataPool(collections.abc.MutableMapping):
""" """
@@ -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)
+5 -3
View File
@@ -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):
+218 -8
View File
@@ -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
+1 -1
View File
@@ -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" ]
+2 -4
View File
@@ -96,12 +96,10 @@ class Experiment:
#for tracking the experiment length: #for tracking the experiment length:
#because loops are possible we need to track the length for each loop level #because loops are possible we need to track the length for each loop level
self.total_time=[] self.total_time=[0.0]
self.total_time.append(0.0)
#and we need to know the number of iterations of the loops to multiply the state. #and we need to know the number of iterations of the loops to multiply the state.
self.loop_iterations=[] self.loop_iterations=[1]
self.loop_iterations.append(1)
# Commands ------------------------------------------------------------------------------------- # Commands -------------------------------------------------------------------------------------
+566 -76
View File
@@ -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( )
+27
View File
@@ -3,6 +3,7 @@ import io
import traceback import traceback
import sys import sys
import time import time
import ast
from damaris.experiments.Experiment import Quit from damaris.experiments.Experiment import Quit
from damaris.experiments import Experiment from damaris.experiments import Experiment
@@ -48,8 +49,32 @@ class ExperimentHandling(threading.Thread):
self.raised_exception = None self.raised_exception = None
self.location = None self.location = None
exp_iterator=None exp_iterator=None
# check for time.sleep()
try:
tree = ast.parse(self.script)
for node in ast.walk(tree):
found_blocking_sleep = False
if isinstance(node, ast.Call):
func = node.func
if isinstance(func, ast.Attribute) and func.attr == 'sleep':
if isinstance(func.value, ast.Name) and func.value.id == 'time':
found_blocking_sleep = True
elif isinstance(node, ast.ImportFrom) and node.module == 'time':
for alias in node.names:
if alias.name == 'sleep':
found_blocking_sleep = True
if found_blocking_sleep:
print("Warning: time.sleep() detected in experiment script. This is blocking and not interruptible. Please use sleep() instead.")
break
except Exception:
pass
try: try:
exec(self.script, dataspace) exec(self.script, dataspace)
except StopExperiment:
return
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -61,6 +86,8 @@ class ExperimentHandling(threading.Thread):
if "experiment" in dataspace: if "experiment" in dataspace:
try: try:
exp_iterator=dataspace["experiment"]() exp_iterator=dataspace["experiment"]()
except StopExperiment:
return
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
+34
View File
@@ -4,7 +4,9 @@ import sys
import os import os
import os.path import os.path
import traceback import traceback
import ast
from damaris.data import Resultable from damaris.data import Resultable
from damaris.gui.ExperimentHandling import StopExperiment
class ResultHandling(threading.Thread): class ResultHandling(threading.Thread):
""" """
@@ -30,10 +32,35 @@ class ResultHandling(threading.Thread):
del data_classes del data_classes
dataspace["results"]=self dataspace["results"]=self
dataspace["data"]=self.data_space dataspace["data"]=self.data_space
dataspace["sleep"]=self.sleep
self.raised_exception=None self.raised_exception=None
self.location = None self.location = None
# check for time.sleep()
try:
tree = ast.parse(self.script)
for node in ast.walk(tree):
found_blocking_sleep = False
if isinstance(node, ast.Call):
func = node.func
if isinstance(func, ast.Attribute) and func.attr == 'sleep':
if isinstance(func.value, ast.Name) and func.value.id == 'time':
found_blocking_sleep = True
elif isinstance(node, ast.ImportFrom) and node.module == 'time':
for alias in node.names:
if alias.name == 'sleep':
found_blocking_sleep = True
if found_blocking_sleep:
print("Warning: time.sleep() detected in result script. This is blocking and not interruptible. Please use sleep() instead.")
break
except Exception:
pass
try: try:
exec(self.script, dataspace) exec(self.script, dataspace)
except StopExperiment:
return
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -47,6 +74,8 @@ class ResultHandling(threading.Thread):
return return
try: try:
dataspace["result"]() dataspace["result"]()
except StopExperiment:
pass
except Exception as e: except Exception as e:
self.raised_exception=e self.raised_exception=e
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3] self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
@@ -56,6 +85,11 @@ class ResultHandling(threading.Thread):
traceback_file=None traceback_file=None
dataspace=None dataspace=None
def sleep(self, seconds):
self.quit_flag.wait(seconds)
if self.quit_flag.isSet():
raise StopExperiment
def __iter__(self): def __iter__(self):
if self.quit_flag.isSet(): if self.quit_flag.isSet():
self.results=None self.results=None
+2
View File
@@ -239,6 +239,7 @@ class ResultReader:
self.result.index = [(0,tmp_size-1)] self.result.index = [(0,tmp_size-1)]
tmp_index += tmp_size tmp_index += tmp_size
self.result.cont_data=True self.result.cont_data=True
self.result.check_clipping()
tmp_part = None tmp_part = None
def __parseFile_expat(self, in_file): def __parseFile_expat(self, in_file):
@@ -310,6 +311,7 @@ class ResultReader:
self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1)) self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1))
tmp_sample_counter+=tmp_size tmp_sample_counter+=tmp_size
self.result.cont_data=True self.result.cont_data=True
self.result.check_clipping()
# Callback when a xml start tag is found # Callback when a xml start tag is found
def __xmlStartTagFound(self, in_name, in_attribute): def __xmlStartTagFound(self, in_name, in_attribute):
+206
View File
@@ -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
-8
View File
@@ -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
+15
View File
@@ -491,6 +491,21 @@ Public License instead of this License.
<property name="homogeneous">True</property> <property name="homogeneous">True</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkToolButton" id="toolbar_open_hdf5_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Open DAMARIS HDF5 file</property>
<property name="label">Open HDF5</property>
<property name="use-underline">True</property>
<property name="icon-name">document-open</property>
<signal name="clicked" handler="on_toolbar_open_hdf5_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child> <child>
<object class="GtkToolButton" id="toolbar_save_file_button"> <object class="GtkToolButton" id="toolbar_save_file_button">
<property name="visible">True</property> <property name="visible">True</property>
+39 -1
View File
@@ -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)
+149
View File
@@ -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)
+151
View File
@@ -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
+126
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -105,7 +105,7 @@ class TestAccumulation(unittest.TestCase):
self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30]))) self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30])))
self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35]))) self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35])))
self.assertEqual(sub_result.index, [(0, 1)]) self.assertEqual(sub_result.index, [(0, 1)])
# TODO Fix #13: self.assertEqual(sub_result.common_descriptions, {"key": "value"}) self.assertEqual(sub_result.common_descriptions, {"key": "value"})
def test_write_to_csv_without_error(self): def test_write_to_csv_without_error(self):
"""Test the functionality of writing to CSV.""" """Test the functionality of writing to CSV."""