Public Access
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75863ffc19 | ||
|
|
6f0b090d9d | ||
|
|
d9f2cc3f2c | ||
|
|
e9e3f5a5f5 | ||
|
|
9441b5a4d9 | ||
|
|
7f29f2d173 | ||
|
|
847ab4d89c | ||
|
|
d6da2f2bda | ||
|
|
5338304ee8 | ||
|
|
d48f96e498 | ||
|
|
058716f3c0 | ||
|
|
6d9f55f788 | ||
|
|
e8d0e45bfb | ||
|
|
08e1fe2a1a | ||
|
|
064036241b | ||
|
|
97aa99c239 | ||
|
|
f26d203e79 | ||
|
|
594c0ee8d4 | ||
|
|
68da261ab4 | ||
|
|
00466cb559 | ||
|
|
7ba5c7d56a | ||
|
|
c181e15aa8 | ||
|
|
26970ba201 | ||
|
|
eab49db8d4 | ||
|
|
3bfb2670eb | ||
|
|
83a01f1d83 | ||
|
|
b46debad69 | ||
|
|
4250039b3c | ||
|
|
d3b1a9e3fd | ||
|
|
06867ec17b | ||
|
|
80d8f1e78d | ||
|
|
58e7846bbe | ||
|
|
1c4f8f5344 | ||
|
|
a3738025b7 |
No files matched your search
@@ -6,3 +6,8 @@ __pycache__
|
||||
*.egg-info
|
||||
*.whl
|
||||
.idea
|
||||
debian/files
|
||||
.pybuild
|
||||
.junie
|
||||
debian/debhelper-build-stamp
|
||||
debian/.debhelper
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
include src/gui/DAMARIS3.png
|
||||
include src/gui/DAMARIS3.ico
|
||||
include src/gui/damaris.glade
|
||||
include src/gui/damaris.gladep
|
||||
include src/damaris/gui/DAMARIS3.png
|
||||
include src/damaris/gui/DAMARIS3.ico
|
||||
include src/damaris/gui/damaris3.glade
|
||||
include src/damaris/gui/python.xml
|
||||
#recursive-include doc/reference-html *.html *.gif *.png *.css
|
||||
#recursive-include doc/tutorial-html *.html *.gif *.png *.css *.tar.gz *.sh
|
||||
@@ -15,3 +15,12 @@ Alternatively you can install withoout the system-site-packages:
|
||||
pip install -e .
|
||||
|
||||
This will download newer packages and install them in the venv.
|
||||
|
||||
|
||||
Using uv
|
||||
========
|
||||
Dependicies needed:
|
||||
sudo apt install libgirepository1.0-dev pkg-config
|
||||
|
||||
Then you can start DAMARIS3:
|
||||
uv run DAMARIS3
|
||||
Vendored
+9
@@ -1,3 +1,12 @@
|
||||
python3-damaris (0.22) stable; urgency=medium
|
||||
|
||||
* fixed autocomplete in the editor tabs
|
||||
* added clipping warning on ADC overload, ADC bit-depth settable in config
|
||||
* Accumulation object keeps track of job_ids
|
||||
* get_result_by_index keeps common description
|
||||
|
||||
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sun, 05 Jul 2026 14:09:34 +0200
|
||||
|
||||
python3-damaris (0.20) stable; urgency=medium
|
||||
|
||||
* Unified version management across Python app, GUI and documentation
|
||||
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
python3-damaris_0.20_all.deb science optional
|
||||
python3-damaris_0.20_amd64.buildinfo science optional
|
||||
+5
-4
@@ -14,12 +14,13 @@ license = {text = "GPL-2.0"}
|
||||
keywords = ["NMR", "data-processing"]
|
||||
readme = {file = "README", content-type = "text/plain"}
|
||||
dependencies = [
|
||||
"scipy >= 0.14.0",
|
||||
"numpy >= 1.8.2, < 2.0",
|
||||
"scipy>=0.14.0",
|
||||
"numpy>=1.8.2,<2.0",
|
||||
"tables >= 3.1.1",
|
||||
"matplotlib >= 1.4.1, <= 3.6",
|
||||
"pyxdg",
|
||||
"PyGObject >= 3.14.0, <= 3.51.0"
|
||||
"PyGObject >= 3.14.0, <= 3.51.0",
|
||||
"pyserial>=3.5",
|
||||
]
|
||||
authors = [
|
||||
{name = "Achim Gädke"},
|
||||
@@ -39,7 +40,7 @@ DAMARIS3 = "damaris.__main__:main"
|
||||
where = ["src"]
|
||||
|
||||
[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]
|
||||
version = {attr = "damaris.__version__"}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
|
||||
## module contents
|
||||
#
|
||||
__version__ = "0.20"
|
||||
__version__ = "0.22"
|
||||
__all__=["experiments", "data", "gui"]
|
||||
@@ -19,6 +19,7 @@ import tables
|
||||
#############################################################################
|
||||
|
||||
class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
default_bit_depth = 14
|
||||
"""
|
||||
Represents the result of an ADC, encapsulating data and metadata
|
||||
for processing, visualization, and export.
|
||||
@@ -50,6 +51,7 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
self.ylabel = "Samples [Digits]"
|
||||
self.lock=threading.RLock()
|
||||
self.nChannels = 0
|
||||
self.is_clipped = False
|
||||
|
||||
# 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):
|
||||
@@ -107,6 +109,44 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
||||
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):
|
||||
"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()):
|
||||
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()
|
||||
return r
|
||||
|
||||
@@ -536,6 +576,8 @@ def read_from_hdf(hdf_node):
|
||||
|
||||
# job id and x,y titles are missing
|
||||
adc=ADC_Result()
|
||||
if hdf_node._v_title:
|
||||
adc.set_title(hdf_node._v_title)
|
||||
# populate description dictionary
|
||||
adc.description={}
|
||||
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]):
|
||||
adc.y.append(adc_data[:,ch])
|
||||
|
||||
adc.check_clipping()
|
||||
return adc
|
||||
@@ -61,6 +61,7 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
self.xlabel = "Time / s"
|
||||
self.ylabel = "Avg. Samples [Digits]"
|
||||
self.lock=threading.RLock()
|
||||
self.is_clipped = False
|
||||
|
||||
self.common_descriptions=None
|
||||
self.time_period=[]
|
||||
@@ -117,9 +118,23 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
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)
|
||||
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()
|
||||
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):
|
||||
if self.uses_statistics():
|
||||
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)
|
||||
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.is_clipped = self.is_clipped
|
||||
r.job_id = self.job_id
|
||||
r.job_ids = self.job_ids.copy()
|
||||
r.time_period = self.time_period[:] if self.time_period is not None else None
|
||||
@@ -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)
|
||||
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)
|
||||
if hasattr(other, "is_clipped"):
|
||||
r.is_clipped = other.is_clipped
|
||||
r.time_period=[other.job_date,other.job_date]
|
||||
r.job_id = other.job_id
|
||||
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)
|
||||
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.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),
|
||||
max(self.time_period[1],other.job_date)]
|
||||
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()):
|
||||
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():
|
||||
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:
|
||||
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]),
|
||||
max(self.time_period[1],other.time_period[1])]
|
||||
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.lock.release()
|
||||
|
||||
if hasattr(other, "is_clipped"):
|
||||
self.is_clipped = other.is_clipped
|
||||
self.time_period=[other.job_date,other.job_date]
|
||||
self.job_id = other.job_id # added by Oleg Petrov
|
||||
self.job_ids[other.job_id] = other.get_job_date()
|
||||
@@ -967,14 +989,14 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
self.lock.acquire()
|
||||
|
||||
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):
|
||||
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():
|
||||
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)):
|
||||
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()):
|
||||
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.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),
|
||||
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.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.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()
|
||||
|
||||
return self
|
||||
@@ -1049,9 +1075,12 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
||||
self.y_square[i] += other.y_square[i]
|
||||
|
||||
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]),
|
||||
max(self.time_period[1],other.time_period[1])]
|
||||
self.job_id = other.job_id
|
||||
self.job_ids.update(other.job_ids)
|
||||
# Removes mismatched common description keys
|
||||
if self.common_descriptions is not None and other.common_descriptions is not None:
|
||||
# Get all keys that exist in both dictionaries
|
||||
@@ -1131,26 +1160,66 @@ def read_from_hdf(hdf_node):
|
||||
earliest_time=None
|
||||
if "earliest_time" in dir(hdf_node._v_attrs):
|
||||
timestring=hdf_node._v_attrs.__getattr__("earliest_time")
|
||||
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
|
||||
)
|
||||
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
|
||||
if '-' in timestring:
|
||||
# New format: "2026-07-05 19:13:59.162"
|
||||
try:
|
||||
earliest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
|
||||
except ValueError:
|
||||
# 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
|
||||
if "oldest_time" in dir(hdf_node._v_attrs):
|
||||
timestring=hdf_node._v_attrs.__getattr__("oldest_time")
|
||||
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
|
||||
)
|
||||
# Handle both formats: "20260705 19:13:59.162" and "2026-07-05 19:13:59.162"
|
||||
if '-' in timestring:
|
||||
# New format: "2026-07-05 19:13:59.162"
|
||||
try:
|
||||
oldest_time=datetime.datetime.strptime(str(timestring), '%Y-%m-%d %H:%M:%S.%f')
|
||||
except ValueError:
|
||||
# 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:
|
||||
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.n=r["number"]
|
||||
accu.sampling_rate=1.0/r["dwelltime"]
|
||||
|
||||
accu.set_title("Accumulation: n = %d" % accu.n)
|
||||
|
||||
# now really belief there are no data
|
||||
if len(accu.index)==0 or accu.n==0:
|
||||
@@ -1181,7 +1252,7 @@ def read_from_hdf(hdf_node):
|
||||
accu.y_square=[]
|
||||
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)
|
||||
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"))
|
||||
|
||||
+339
-13
@@ -7,6 +7,7 @@ import collections
|
||||
import threading
|
||||
import traceback
|
||||
import io
|
||||
import numpy
|
||||
|
||||
class DataPool(collections.abc.MutableMapping):
|
||||
"""
|
||||
@@ -48,6 +49,7 @@ class DataPool(collections.abc.MutableMapping):
|
||||
self.__mydict={}
|
||||
self.__dictlock=threading.Lock()
|
||||
self.__registered_listeners=[]
|
||||
self.__dirty_keys=set()
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
@@ -64,6 +66,7 @@ class DataPool(collections.abc.MutableMapping):
|
||||
else:
|
||||
e=DataPool.Event(DataPool.Event.new_key, name,self)
|
||||
self.__mydict[name]=value
|
||||
self.__dirty_keys.add(name)
|
||||
finally:
|
||||
self.__dictlock.release()
|
||||
self.__send_event(e)
|
||||
@@ -73,6 +76,7 @@ class DataPool(collections.abc.MutableMapping):
|
||||
try:
|
||||
self.__dictlock.acquire()
|
||||
del self.__mydict[name]
|
||||
self.__dirty_keys.add(name)
|
||||
finally:
|
||||
self.__dictlock.release()
|
||||
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.__registered_listeners=None
|
||||
|
||||
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
|
||||
if type(hdffile) is bytes:
|
||||
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None, incremental=False):
|
||||
|
||||
"""
|
||||
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")
|
||||
elif isinstance(hdffile,tables.File):
|
||||
dump_file=hdffile
|
||||
else:
|
||||
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()
|
||||
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()
|
||||
|
||||
try:
|
||||
for key in dict_keys:
|
||||
if key.startswith("__"):
|
||||
@@ -142,21 +202,37 @@ class DataPool(collections.abc.MutableMapping):
|
||||
|
||||
# convert last part of key to a valid name
|
||||
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:
|
||||
extension_count=0
|
||||
while group_keyname+"_%03d"%extension_count in dump_dir:
|
||||
extension_count+=1
|
||||
group_keyname+="_%03d"%extension_count
|
||||
if dump_dir._v_children[group_keyname]._v_title == key:
|
||||
dump_file.remove_node(dump_dir, group_keyname, recursive=True)
|
||||
else:
|
||||
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()
|
||||
if key not in self.__mydict:
|
||||
# outdated ...
|
||||
# Key was deleted
|
||||
if key in self.__dirty_keys:
|
||||
self.__dirty_keys.remove(key)
|
||||
self.__dictlock.release()
|
||||
continue
|
||||
value=self.__mydict[key]
|
||||
if key in self.__dirty_keys:
|
||||
self.__dirty_keys.remove(key)
|
||||
self.__dictlock.release()
|
||||
# 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:
|
||||
value.write_to_hdf(hdffile=dump_file,
|
||||
where=dump_dir,
|
||||
@@ -170,16 +246,266 @@ class DataPool(collections.abc.MutableMapping):
|
||||
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
|
||||
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
|
||||
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:
|
||||
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
|
||||
|
||||
finally:
|
||||
dump_group=None
|
||||
if type(hdffile) is bytes:
|
||||
if isinstance(hdffile, (bytes, str)):
|
||||
dump_file.close()
|
||||
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):
|
||||
self.__registered_listeners.append(listening_function)
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
|
||||
def uses_statistics(self):
|
||||
"""
|
||||
drawable interface method, returns True
|
||||
drawable interface method returns True
|
||||
"""
|
||||
return True
|
||||
|
||||
@@ -203,10 +203,11 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
destination can be a file or a filename
|
||||
suitable for further processing
|
||||
"""
|
||||
# write sorted
|
||||
the_destination=destination
|
||||
file_opened = False
|
||||
if type(destination) in (str,):
|
||||
the_destination=open(destination, "w")
|
||||
file_opened = True
|
||||
|
||||
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
|
||||
the_destination.write("# x y ysigma n\n")
|
||||
@@ -222,7 +223,8 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
||||
y.mean_error(),
|
||||
delimiter,
|
||||
y.n))
|
||||
the_destination=None
|
||||
if file_opened:
|
||||
the_destination.close()
|
||||
|
||||
|
||||
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from .Resultable import Resultable
|
||||
from .Drawable import Drawable
|
||||
import numpy
|
||||
import threading
|
||||
import tables
|
||||
|
||||
#############################################################################
|
||||
# #
|
||||
@@ -15,17 +18,224 @@ from .Drawable import Drawable
|
||||
class TemperatureResult(Resultable, 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)
|
||||
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):
|
||||
pass
|
||||
Stores x (timestamps), y (temperature readings), and setpoint arrays
|
||||
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):
|
||||
pass
|
||||
with self.__listener_lock:
|
||||
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:
|
||||
raise ValueError("Wrong usage of __init__!")
|
||||
# Use the length of the longest array; shorter ones are padded with 0
|
||||
n = max(len(xdata), len(ydata), 1)
|
||||
if len(xdata) < n:
|
||||
xdata = numpy.pad(xdata, (0, n - len(xdata)), constant_values=0.0)
|
||||
if len(ydata) < n:
|
||||
ydata = numpy.pad(ydata, (0, n - len(ydata)), constant_values=0.0)
|
||||
if len(spdata) < n:
|
||||
spdata = numpy.pad(spdata, (0, n - len(spdata)), constant_values=0.0)
|
||||
|
||||
temp_table = hdffile.create_table(
|
||||
where=where, name=name,
|
||||
description=h5_table_format,
|
||||
title=title,
|
||||
filters=filter,
|
||||
expectedrows=n,
|
||||
)
|
||||
temp_table.flavor = "numpy"
|
||||
temp_table.attrs.damaris_type = "TemperatureResult"
|
||||
temp_table.attrs.xlabel = self.xlabel
|
||||
temp_table.attrs.ylabel = self.ylabel
|
||||
|
||||
if hasattr(self, "job_id") and self.job_id is not None:
|
||||
temp_table.attrs.job_id = self.job_id
|
||||
if hasattr(self, "job_date") and self.job_date is not None:
|
||||
temp_table.attrs.time = "%04d%02d%02d %02d:%02d:%02d.%03d" % (
|
||||
self.job_date.year,
|
||||
self.job_date.month,
|
||||
self.job_date.day,
|
||||
self.job_date.hour,
|
||||
self.job_date.minute,
|
||||
self.job_date.second,
|
||||
self.job_date.microsecond // 1000,
|
||||
)
|
||||
if hasattr(self, "description") and self.description is not None:
|
||||
for (key, value) in self.description.items():
|
||||
temp_table.attrs.__setattr__("description_" + key, str(value))
|
||||
|
||||
row = temp_table.row
|
||||
for i in range(n):
|
||||
row["x"] = xdata[i]
|
||||
row["y"] = ydata[i]
|
||||
row["setpoint"] = spdata[i]
|
||||
row.append()
|
||||
|
||||
temp_table.flush()
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of the temperature result."""
|
||||
return f"TemperatureResult(points={len(self)}, job_id={self.job_id})"
|
||||
|
||||
|
||||
def read_from_hdf(hdf_node):
|
||||
"""
|
||||
Read a TemperatureResult object from an HDF5 table node.
|
||||
Returns None if the node is not suitable.
|
||||
"""
|
||||
if not isinstance(hdf_node, tables.Table):
|
||||
return None
|
||||
|
||||
if hdf_node._v_attrs.damaris_type != "TemperatureResult":
|
||||
return None
|
||||
|
||||
tr = TemperatureResult()
|
||||
tr.xlabel = getattr(hdf_node._v_attrs, "xlabel", "Time (s)")
|
||||
tr.ylabel = getattr(hdf_node._v_attrs, "ylabel", "Temperature (C)")
|
||||
|
||||
for r in hdf_node.iterrows():
|
||||
tr.x.append(r["x"])
|
||||
tr.y.append(r["y"])
|
||||
sp = r["setpoint"]
|
||||
if sp != 0.0 or len(tr.setpoint) > 0:
|
||||
tr.setpoint.append(sp)
|
||||
|
||||
return tr
|
||||
@@ -6,5 +6,5 @@ from damaris.data.Error_Result import Error_Result
|
||||
from damaris.data.Config_Result import Config_Result
|
||||
from damaris.data.Temperature import TemperatureResult
|
||||
|
||||
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "FFT", "Error_Result", "Config_Result", "TemperatureResult" ]
|
||||
__all__=["ADC_Result", "Accumulation", "MeasurementResult", "AccumulatedValue", "DataPool", "Error_Result", "Config_Result", "TemperatureResult" ]
|
||||
|
||||
@@ -96,12 +96,10 @@ class Experiment:
|
||||
|
||||
#for tracking the experiment length:
|
||||
#because loops are possible we need to track the length for each loop level
|
||||
self.total_time=[]
|
||||
self.total_time.append(0.0)
|
||||
self.total_time=[0.0]
|
||||
|
||||
#and we need to know the number of iterations of the loops to multiply the state.
|
||||
self.loop_iterations=[]
|
||||
self.loop_iterations.append(1)
|
||||
self.loop_iterations=[1]
|
||||
|
||||
# Commands -------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
+566
-76
@@ -61,7 +61,8 @@ from damaris.gui import ResultReader, ResultHandling
|
||||
from damaris.gui import BackendDriver
|
||||
#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 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
|
||||
debug = False
|
||||
@@ -88,7 +89,6 @@ class logstream:
|
||||
def __del__( self ):
|
||||
pass
|
||||
|
||||
|
||||
global log
|
||||
log = logstream( )
|
||||
sys.stdout = log
|
||||
@@ -134,9 +134,6 @@ class LockFile:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DamarisGUI:
|
||||
ExpScript_Display = 0
|
||||
ResScript_Display = 1
|
||||
@@ -169,7 +166,7 @@ class DamarisGUI:
|
||||
|
||||
self.log = LogWindow( self.xml_gui , self)
|
||||
|
||||
self.sw = ScriptWidgets( self.xml_gui )
|
||||
self.sw = ScriptWidgets( self.xml_gui, self )
|
||||
|
||||
self.toolbar_init( )
|
||||
|
||||
@@ -184,6 +181,10 @@ class DamarisGUI:
|
||||
|
||||
#to stop queued experiments
|
||||
self.stop_experiment_flag = threading.Event()
|
||||
|
||||
# Temperature monitoring
|
||||
self.temp_monitor = None
|
||||
self.temp_controller = None
|
||||
|
||||
exp_script = ""
|
||||
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_column_textfield_value_changed": self.sw.column_line_widgets_changed_event,
|
||||
"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_save_as_button_clicked": self.sw.save_file_as,
|
||||
"on_toolbar_save_file_button_clicked": self.sw.save_file,
|
||||
@@ -297,6 +299,14 @@ class DamarisGUI:
|
||||
self.config = 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
|
||||
|
||||
# first global events
|
||||
@@ -471,9 +481,10 @@ class DamarisGUI:
|
||||
|
||||
self.id = self.lockfile.add_experiment()
|
||||
while not self.lockfile.am_i_next():
|
||||
self.stop_experiment_flag.wait(interval)
|
||||
while gtk.events_pending():
|
||||
gtk.main_iteration_do(False)
|
||||
if self.stop_experiment_flag.wait(interval):
|
||||
break
|
||||
if loop_run > 1:
|
||||
self.experiment_script_statusbar_label.set_text("Waiting for other experiment to finish")
|
||||
loop_run = 0
|
||||
@@ -506,6 +517,8 @@ class DamarisGUI:
|
||||
self.data = self.si.data
|
||||
# run frontend and script engines
|
||||
self.monitor.observe_data_pool( self.data )
|
||||
# Start temperature monitoring if configured (before scripts start)
|
||||
self.start_temperature_monitoring(actual_config)
|
||||
self.si.runScripts( )
|
||||
except Exception as e:
|
||||
#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=' ')
|
||||
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:
|
||||
t.join( )
|
||||
while t.is_alive():
|
||||
while gtk.events_pending():
|
||||
gtk.main_iteration_do(False)
|
||||
t.join( 0.05 )
|
||||
print("done")
|
||||
|
||||
# cleanup
|
||||
@@ -563,6 +579,7 @@ class DamarisGUI:
|
||||
self.dump_filename = ""
|
||||
if actual_config[ "data_pool_name" ] != "":
|
||||
self.dump_states( init=True )
|
||||
|
||||
gobject.timeout_add( 200, self.observe_running_experiment )
|
||||
|
||||
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]
|
||||
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 != "":
|
||||
print("all subprocesses ended, saving data pool")
|
||||
# thread to save data...
|
||||
@@ -865,15 +884,6 @@ class DamarisGUI:
|
||||
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 ):
|
||||
# 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
|
||||
dump_file = tables.open_file( self.dump_filename, mode="r+" )
|
||||
|
||||
@@ -890,7 +900,8 @@ class DamarisGUI:
|
||||
|
||||
# save the data!
|
||||
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
|
||||
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]
|
||||
for r in still_running:
|
||||
r.quit_flag.set( )
|
||||
|
||||
# Stop temperature monitoring
|
||||
self.stop_temperature_monitoring()
|
||||
|
||||
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 ):
|
||||
|
||||
self.doc_urls = {
|
||||
@@ -1099,11 +1200,12 @@ class LogWindow:
|
||||
|
||||
|
||||
class ScriptWidgets:
|
||||
def __init__( self, xml_gui ):
|
||||
def __init__( self, xml_gui, outer_space ):
|
||||
"""
|
||||
initialize text widgets with text
|
||||
"""
|
||||
self.xml_gui = xml_gui
|
||||
self.outer_space = outer_space
|
||||
|
||||
# my states
|
||||
# editing enabled/disabled
|
||||
@@ -1170,7 +1272,7 @@ get_ylabel set_ylabel"""
|
||||
|
||||
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None)
|
||||
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.register(self.experiment_script_textbuffer)
|
||||
@@ -1186,7 +1288,6 @@ get_ylabel set_ylabel"""
|
||||
compexp.add_provider(self.compwexp)
|
||||
compres.add_provider(self.compwres)
|
||||
|
||||
# script fonts are atlered by configuration
|
||||
# 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.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_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_column_indicator = self.xml_gui.get_object( "data_handling_column_textfield" )
|
||||
|
||||
|
||||
# some event handlers
|
||||
self.experiment_script_textbuffer.connect( "modified-changed", self.textviews_modified )
|
||||
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( "button-release-event", self.textviews_clicked )
|
||||
|
||||
# and the editing toolbar part
|
||||
# buttons
|
||||
# and the editing toolbar buttons
|
||||
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_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_search_button = self.xml_gui.get_object("toolbar_search_button")
|
||||
|
||||
# my notebook
|
||||
# main notebook
|
||||
self.main_notebook = self.xml_gui.get_object( "main_notebook" )
|
||||
|
||||
# config toolbar
|
||||
self.main_notebook.connect_after( "switch_page", self.notebook_page_switched )
|
||||
|
||||
@@ -1570,7 +1670,7 @@ get_ylabel set_ylabel"""
|
||||
elif response == gtk.ResponseType.CANCEL:
|
||||
return 0
|
||||
|
||||
def response( self, response_id, script_widget ):
|
||||
def response( dialog, response_id, script_widget ):
|
||||
if response_id == gtk.ResponseType.OK:
|
||||
file_name = dialog.get_filename( )
|
||||
if file_name is None:
|
||||
@@ -1578,7 +1678,13 @@ get_ylabel set_ylabel"""
|
||||
|
||||
script_filename = os.path.abspath( file_name )
|
||||
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
|
||||
|
||||
script_string = script_widget.load_file_as_unicode( script_filename )
|
||||
@@ -1611,6 +1717,94 @@ get_ylabel set_ylabel"""
|
||||
|
||||
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 ):
|
||||
"""
|
||||
save file to associated filename
|
||||
@@ -1798,9 +1992,9 @@ get_ylabel set_ylabel"""
|
||||
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
pass
|
||||
|
||||
@@ -1857,7 +2051,7 @@ get_ylabel set_ylabel"""
|
||||
dialog.connect("response", response, self, textbox)
|
||||
dialog.set_resizable(False)
|
||||
|
||||
dialog.show()
|
||||
dialog.show_all()
|
||||
|
||||
if startit is not None and endit is not None:
|
||||
if current_page == 0:
|
||||
@@ -1951,6 +2145,87 @@ class ConfigTab:
|
||||
self.config_info_textview = self.xml_gui.get_object( "info_textview" )
|
||||
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
|
||||
components_text = """
|
||||
operating system %(os)s
|
||||
@@ -2044,7 +2319,7 @@ pygobject version %(pygobject)s
|
||||
"""
|
||||
complib_iter = self.config_data_pool_complib.get_active_iter( )
|
||||
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_result_script": self.config_start_result_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_complib": complib,
|
||||
"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 ):
|
||||
if "start_backend" in config:
|
||||
@@ -2083,6 +2379,47 @@ pygobject version %(pygobject)s
|
||||
if "script_font" in config:
|
||||
self.config_script_font_button.set_font_name( config[ "script_font" ] )
|
||||
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:
|
||||
# find combo-box entry and make it active...
|
||||
model = self.config_data_pool_complib.get_model( )
|
||||
@@ -2096,6 +2433,87 @@ pygobject version %(pygobject)s
|
||||
if iter is None:
|
||||
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 ):
|
||||
if 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_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) --------------------------------------------------------
|
||||
|
||||
# create new plot
|
||||
@@ -2307,7 +2732,9 @@ class MonitorWidgets:
|
||||
# now let's add a button to the toolbar
|
||||
self.matplot_cursor_button = gtk.ToggleButton(label='')
|
||||
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)
|
||||
except:
|
||||
pass
|
||||
@@ -2363,6 +2790,8 @@ class MonitorWidgets:
|
||||
self.__rescale = True
|
||||
self.update_counter = 0
|
||||
self.update_counter_lock = threading.Lock( )
|
||||
self.description_text = None
|
||||
self.clipping_marker = None
|
||||
|
||||
def source_list_reset( self ):
|
||||
self.display_source_treestore.clear( )
|
||||
@@ -2374,7 +2803,7 @@ class MonitorWidgets:
|
||||
def source_list_find_one( self, model, iter, what ):
|
||||
"""find node in subcategory"""
|
||||
while iter is not None:
|
||||
if model.get( iter, 0 )[ 0 ] == what:
|
||||
if str( model.get( iter, 0 )[ 0 ] ) == what:
|
||||
return iter
|
||||
iter = model.iter_next( iter )
|
||||
return iter
|
||||
@@ -2445,31 +2874,50 @@ class MonitorWidgets:
|
||||
register a listener and save reference to data
|
||||
assume to be in gtk/gdk lock
|
||||
"""
|
||||
if self.data_pool is not None:
|
||||
# maybe some extra cleanup needed
|
||||
print("ToDo: cleanup widgets")
|
||||
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
|
||||
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
|
||||
self.displayed_data[ 1 ] = None
|
||||
self.displayed_data = [ None, None ]
|
||||
self.data_pool.unregister_listener( self.datapool_listener )
|
||||
self.data_pool = None
|
||||
gdk.threads_enter( )
|
||||
try:
|
||||
if self.data_pool is not None:
|
||||
# maybe some extra cleanup needed
|
||||
print("ToDo: cleanup widgets")
|
||||
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
|
||||
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
|
||||
self.displayed_data[ 1 ] = None
|
||||
self.displayed_data = [ None, None ]
|
||||
self.data_pool.unregister_listener( self.datapool_listener )
|
||||
self.data_pool = None
|
||||
|
||||
self.update_counter_lock.acquire( )
|
||||
self.update_counter = 0
|
||||
self.update_counter_lock.release( )
|
||||
|
||||
# display states
|
||||
self.source_list_reset( )
|
||||
self.update_counter_lock.acquire( )
|
||||
self.update_counter = 0
|
||||
self.update_counter_lock.release( )
|
||||
self.__rescale = True
|
||||
self.displayed_data = [ None, None ]
|
||||
self.display_source_path_label.set_label( "" )
|
||||
self.clear_display( )
|
||||
|
||||
# display states
|
||||
self.__rescale = True
|
||||
self.displayed_data = [ None, None ]
|
||||
self.display_source_path_label.set_label( "" )
|
||||
self.clear_display( )
|
||||
|
||||
if data_pool is not None:
|
||||
# keep track of data
|
||||
self.data_pool = data_pool
|
||||
self.data_pool.register_listener( self.datapool_listener )
|
||||
if data_pool is not None:
|
||||
# keep track of data
|
||||
self.data_pool = data_pool
|
||||
self.data_pool.register_listener( self.datapool_listener )
|
||||
|
||||
# Populate source list with existing data since the listener
|
||||
# was registered after the data was loaded
|
||||
for key in sorted(data_pool.keys()):
|
||||
if key != "__builtins__": # Skip internal keys
|
||||
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
|
||||
|
||||
@@ -2606,7 +3054,7 @@ class MonitorWidgets:
|
||||
gdk.threads_leave( )
|
||||
elif event.what == DataPool.Event.destroy:
|
||||
gdk.threads_enter( )
|
||||
self.source_list_reset( 'None' )
|
||||
self.source_list_reset( )
|
||||
self.displayed_data = [ None, None ]
|
||||
self.clear_display( )
|
||||
gdk.threads_leave( )
|
||||
@@ -2685,6 +3133,10 @@ class MonitorWidgets:
|
||||
if self.displayed_data[ 0 ] is not None:
|
||||
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 ):
|
||||
"""
|
||||
copy data to tmp file and show save dialog
|
||||
@@ -2762,6 +3214,7 @@ class MonitorWidgets:
|
||||
self.matplot_cursor = matplotlib.widgets.Cursor(self.matplot_axes,
|
||||
useblit=False, color='red', linewidth=1)
|
||||
self.matplot_canvas.draw()
|
||||
# TODO: Deactivate the pane button
|
||||
|
||||
else:
|
||||
# Deactivate cursor
|
||||
@@ -2889,7 +3342,7 @@ class MonitorWidgets:
|
||||
|
||||
x_scale = self.display_x_scaling_combobox.get_active_text()
|
||||
y_scale = self.display_y_scaling_combobox.get_active_text()
|
||||
print(x_scale)
|
||||
|
||||
if x_scale == "lin":
|
||||
self.matplot_axes.set_xscale("linear")
|
||||
elif x_scale == "log10":
|
||||
@@ -2935,6 +3388,18 @@ class MonitorWidgets:
|
||||
self.graphen = [ ]
|
||||
self.matplot_axes.clear( )
|
||||
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( )
|
||||
|
||||
def update_display( self, subject=None ):
|
||||
@@ -2948,7 +3413,7 @@ class MonitorWidgets:
|
||||
if in_result is None:
|
||||
self.clear_display( )
|
||||
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( )
|
||||
xmax = in_result.get_xmax( )
|
||||
@@ -3093,25 +3558,48 @@ class MonitorWidgets:
|
||||
self.matplot_axes.set_ylabel( "" )
|
||||
|
||||
# Any variables to be drawn on plot
|
||||
if False:
|
||||
descriptions = None
|
||||
description_lines = []
|
||||
if self.display_descriptions_checkbutton.get_active():
|
||||
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):
|
||||
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:
|
||||
pass
|
||||
#actual_config = self.config.get()
|
||||
if (descriptions is not None):
|
||||
#print(actual_config['pretty_descriptions'])
|
||||
description_string = ""
|
||||
for key in descriptions.keys():
|
||||
description_string += "%s = %s\n" % (key,descriptions[key])
|
||||
self.matplot_axes.text(0.8,0.95, description_string[:-1],
|
||||
size=10,
|
||||
transform=self.matplot_axes.transAxes,
|
||||
va='top',
|
||||
backgroundcolor='white')
|
||||
self.description_text.set_text(description_string)
|
||||
elif self.description_text is not None:
|
||||
try:
|
||||
self.description_text.remove()
|
||||
except:
|
||||
pass
|
||||
self.description_text = None
|
||||
|
||||
if getattr(in_result, "is_clipped", False):
|
||||
if self.clipping_marker is None:
|
||||
self.clipping_marker = self.matplot_axes.text(0.5, 0.95, "ADC OVERFLOW",
|
||||
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!
|
||||
self.matplot_canvas.draw_idle( )
|
||||
@@ -3287,6 +3775,8 @@ class ScriptInterface:
|
||||
self.back_driver.start( )
|
||||
while (not self.back_driver.quit_flag.isSet( ) and \
|
||||
(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 )
|
||||
if self.exp_handling:
|
||||
self.exp_handling.start( )
|
||||
|
||||
@@ -3,6 +3,7 @@ import io
|
||||
import traceback
|
||||
import sys
|
||||
import time
|
||||
import ast
|
||||
from damaris.experiments.Experiment import Quit
|
||||
from damaris.experiments import Experiment
|
||||
|
||||
@@ -48,8 +49,32 @@ class ExperimentHandling(threading.Thread):
|
||||
self.raised_exception = None
|
||||
self.location = 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:
|
||||
exec(self.script, dataspace)
|
||||
except StopExperiment:
|
||||
return
|
||||
except Exception as e:
|
||||
self.raised_exception=e
|
||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||
@@ -61,6 +86,8 @@ class ExperimentHandling(threading.Thread):
|
||||
if "experiment" in dataspace:
|
||||
try:
|
||||
exp_iterator=dataspace["experiment"]()
|
||||
except StopExperiment:
|
||||
return
|
||||
except Exception as e:
|
||||
self.raised_exception=e
|
||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||
|
||||
@@ -4,7 +4,9 @@ import sys
|
||||
import os
|
||||
import os.path
|
||||
import traceback
|
||||
import ast
|
||||
from damaris.data import Resultable
|
||||
from damaris.gui.ExperimentHandling import StopExperiment
|
||||
|
||||
class ResultHandling(threading.Thread):
|
||||
"""
|
||||
@@ -30,10 +32,35 @@ class ResultHandling(threading.Thread):
|
||||
del data_classes
|
||||
dataspace["results"]=self
|
||||
dataspace["data"]=self.data_space
|
||||
dataspace["sleep"]=self.sleep
|
||||
self.raised_exception=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:
|
||||
exec(self.script, dataspace)
|
||||
except StopExperiment:
|
||||
return
|
||||
except Exception as e:
|
||||
self.raised_exception=e
|
||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||
@@ -47,6 +74,8 @@ class ResultHandling(threading.Thread):
|
||||
return
|
||||
try:
|
||||
dataspace["result"]()
|
||||
except StopExperiment:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.raised_exception=e
|
||||
self.location=traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||
@@ -56,6 +85,11 @@ class ResultHandling(threading.Thread):
|
||||
traceback_file=None
|
||||
dataspace=None
|
||||
|
||||
def sleep(self, seconds):
|
||||
self.quit_flag.wait(seconds)
|
||||
if self.quit_flag.isSet():
|
||||
raise StopExperiment
|
||||
|
||||
def __iter__(self):
|
||||
if self.quit_flag.isSet():
|
||||
self.results=None
|
||||
|
||||
@@ -239,6 +239,7 @@ class ResultReader:
|
||||
self.result.index = [(0,tmp_size-1)]
|
||||
tmp_index += tmp_size
|
||||
self.result.cont_data=True
|
||||
self.result.check_clipping()
|
||||
tmp_part = None
|
||||
|
||||
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))
|
||||
tmp_sample_counter+=tmp_size
|
||||
self.result.cont_data=True
|
||||
self.result.check_clipping()
|
||||
|
||||
# Callback when a xml start tag is found
|
||||
def __xmlStartTagFound(self, in_name, in_attribute):
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
Temperature monitoring thread.
|
||||
|
||||
This module provides a thread that periodically reads temperature from a controller
|
||||
and stores it in a TemperatureResult object.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from damaris.data import TemperatureResult
|
||||
from damaris.tools.temperature import TemperatureController
|
||||
|
||||
# GTK imports are done lazily to avoid initialization issues
|
||||
_HAS_GTK = None
|
||||
_gdk = None
|
||||
|
||||
|
||||
def _get_gdk():
|
||||
"""Lazily import gdk to avoid GTK initialization issues."""
|
||||
global _HAS_GTK, _gdk
|
||||
if _HAS_GTK is None:
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gdk as gdk
|
||||
_gdk = gdk
|
||||
_HAS_GTK = True
|
||||
except (ImportError, ValueError):
|
||||
_HAS_GTK = False
|
||||
return _gdk if _HAS_GTK else None
|
||||
|
||||
|
||||
class TemperatureMonitor(threading.Thread):
|
||||
"""
|
||||
Thread that periodically reads temperature from a controller
|
||||
and stores it in a TemperatureResult object.
|
||||
|
||||
This thread runs independently and can be configured with any
|
||||
TemperatureController implementation (Eurotherm, Lakeshore, etc.).
|
||||
|
||||
Attributes:
|
||||
controller: The TemperatureController instance to read from.
|
||||
interval: Polling interval in seconds.
|
||||
result: The TemperatureResult object storing collected data.
|
||||
quit_flag: Event to signal the thread to stop.
|
||||
data_pool: Optional DataPool to store results.
|
||||
data_key: Key to use when storing in data_pool.
|
||||
"""
|
||||
|
||||
def __init__(self, controller: TemperatureController,
|
||||
interval: float = 1.0,
|
||||
data_pool=None,
|
||||
data_key: str = "temperature"):
|
||||
"""
|
||||
Initialize the temperature monitor.
|
||||
|
||||
Args:
|
||||
controller: TemperatureController implementation (Eurotherm, etc.)
|
||||
interval: Polling interval in seconds (default: 1.0)
|
||||
data_pool: Optional DataPool to store results (default: None)
|
||||
data_key: Key to use when storing in data_pool (default: "temperature")
|
||||
"""
|
||||
threading.Thread.__init__(self, name="TemperatureMonitor")
|
||||
self.controller = controller
|
||||
self.interval = float(interval)
|
||||
self.data_pool = data_pool
|
||||
self.data_key = data_key
|
||||
self.quit_flag = threading.Event()
|
||||
self.result = TemperatureResult()
|
||||
self._running = False
|
||||
self._lock = threading.RLock()
|
||||
|
||||
# Initialize the result with metadata
|
||||
self.result.xlabel = "Time (s)"
|
||||
self.result.ylabel = "Temperature (C)"
|
||||
self.result.description = {"type": "temperature_monitoring"}
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Main thread loop: periodically reads temperature and stores it.
|
||||
"""
|
||||
self._running = True
|
||||
start_time = time.time()
|
||||
|
||||
while not self.quit_flag.is_set() and self._running:
|
||||
try:
|
||||
# Read temperature and setpoint
|
||||
timestamp = time.time() - start_time
|
||||
current_temp = self.controller.get_temperature()
|
||||
setpoint = self.controller.get_setpoint()
|
||||
|
||||
# Store in result
|
||||
with self._lock:
|
||||
if not hasattr(self.result, 'x'):
|
||||
self.result.x = []
|
||||
self.result.y = []
|
||||
self.result.setpoint = []
|
||||
|
||||
self.result.x.append(timestamp)
|
||||
self.result.y.append(current_temp)
|
||||
self.result.setpoint.append(setpoint)
|
||||
|
||||
# Update job metadata on first sample
|
||||
if len(self.result.x) == 1:
|
||||
self.result.job_date = datetime.datetime.now()
|
||||
self.result.job_id = id(self)
|
||||
|
||||
# Store result in data_pool (thread-safe for GTK)
|
||||
# Replace the entry on each update to trigger DataPool events
|
||||
if self.data_pool is not None:
|
||||
with self._lock:
|
||||
# Create a copy for the data_pool with current data
|
||||
data_pool_result = TemperatureResult()
|
||||
data_pool_result.xlabel = self.result.xlabel
|
||||
data_pool_result.ylabel = self.result.ylabel
|
||||
data_pool_result.description = self.result.description.copy() if isinstance(self.result.description, dict) else {}
|
||||
# Copy the data from self.result
|
||||
data_pool_result.x = list(self.result.x) if hasattr(self.result, 'x') else []
|
||||
data_pool_result.y = list(self.result.y) if hasattr(self.result, 'y') else []
|
||||
data_pool_result.setpoint = list(self.result.setpoint) if hasattr(self.result, 'setpoint') else []
|
||||
data_pool_result.job_id = self.result.job_id if hasattr(self.result, 'job_id') else None
|
||||
data_pool_result.job_date = self.result.job_date if hasattr(self.result, 'job_date') else None
|
||||
|
||||
# Store in data_pool in GTK thread (if GTK available)
|
||||
gdk = _get_gdk()
|
||||
if gdk is not None:
|
||||
gdk.threads_enter()
|
||||
try:
|
||||
self.data_pool[self.data_key] = data_pool_result
|
||||
finally:
|
||||
gdk.threads_leave()
|
||||
else:
|
||||
self.data_pool[self.data_key] = data_pool_result
|
||||
|
||||
except Exception as e:
|
||||
print(f"TemperatureMonitor error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Sleep in small increments for responsive shutdown
|
||||
sleep_remaining = self.interval
|
||||
while sleep_remaining > 0 and not self.quit_flag.is_set() and self._running:
|
||||
sleep_time = min(0.1, sleep_remaining)
|
||||
self.quit_flag.wait(sleep_time)
|
||||
sleep_remaining -= sleep_time
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Graceful shutdown: signal the thread to stop and wait for it.
|
||||
"""
|
||||
self._running = False
|
||||
self.quit_flag.set()
|
||||
# Wait for the thread to finish, with a generous timeout
|
||||
# Use a longer timeout to account for potential delays
|
||||
if self.is_alive():
|
||||
self.join(timeout=5.0) # Wait up to 5 seconds
|
||||
if self.is_alive():
|
||||
print("Warning: TemperatureMonitor thread did not stop gracefully")
|
||||
# Thread is still running, but we can't force stop it
|
||||
# The next start will check if it's alive and handle it
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Clear the collected data and start fresh.
|
||||
"""
|
||||
with self._lock:
|
||||
self.result = TemperatureResult()
|
||||
self.result.xlabel = "Time (s)"
|
||||
self.result.ylabel = "Temperature (C)"
|
||||
self.result.description = {"type": "temperature_monitoring"}
|
||||
|
||||
def get_current_temperature(self) -> float:
|
||||
"""
|
||||
Get the latest temperature reading.
|
||||
|
||||
Returns:
|
||||
float: Latest temperature in Celsius, or 0.0 if no data.
|
||||
"""
|
||||
with self._lock:
|
||||
if hasattr(self.result, 'y') and len(self.result.y) > 0:
|
||||
return self.result.y[-1]
|
||||
return 0.0
|
||||
|
||||
def get_current_setpoint(self) -> float:
|
||||
"""
|
||||
Get the latest setpoint reading.
|
||||
|
||||
Returns:
|
||||
float: Latest setpoint in Celsius, or 0.0 if no data.
|
||||
"""
|
||||
with self._lock:
|
||||
if hasattr(self.result, 'setpoint') and len(self.result.setpoint) > 0:
|
||||
return self.result.setpoint[-1]
|
||||
return 0.0
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""
|
||||
Check if the monitor is currently running.
|
||||
|
||||
Returns:
|
||||
bool: True if the monitor thread is active.
|
||||
"""
|
||||
return self._running and not self.quit_flag.is_set()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
|
||||
<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd">
|
||||
|
||||
<glade-project>
|
||||
<name>damaris-gui</name>
|
||||
<program_name>damaris</program_name>
|
||||
<gnome_support>FALSE</gnome_support>
|
||||
</glade-project>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,21 @@ Public License instead of this License.
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</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>
|
||||
<object class="GtkToolButton" id="toolbar_save_file_button">
|
||||
<property name="visible">True</property>
|
||||
|
||||
@@ -3,6 +3,8 @@ import re
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from .temperature import TemperatureController, register_controller
|
||||
|
||||
|
||||
DEBUG=False
|
||||
|
||||
@@ -40,7 +42,7 @@ def checksum(message):
|
||||
bcc = (reduce(operator.xor, list(map(ord,message))))
|
||||
return chr(bcc)
|
||||
|
||||
class Eurotherm(object):
|
||||
class Eurotherm(TemperatureController):
|
||||
def __init__(self, serial_device, baudrate = 19200):
|
||||
self.device = standard_device
|
||||
# timeout: 110 ms to get all answers.
|
||||
@@ -100,6 +102,39 @@ class Eurotherm(object):
|
||||
def get_setpoint_temperature(self):
|
||||
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__':
|
||||
import time
|
||||
delta=5
|
||||
@@ -118,3 +153,6 @@ if __name__ == '__main__':
|
||||
f.write(l)
|
||||
f.flush()
|
||||
f.write('# MARK -- %s --\n'%(time.asctime()))
|
||||
|
||||
# Register Eurotherm with the factory
|
||||
register_controller('eurotherm', Eurotherm)
|
||||
@@ -0,0 +1,149 @@
|
||||
import serial
|
||||
import re
|
||||
import numpy as np
|
||||
|
||||
from .temperature import TemperatureController, register_controller
|
||||
|
||||
|
||||
DEBUG = False
|
||||
|
||||
reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL)
|
||||
# example answer '\x02PV279.8\x03/'
|
||||
# [EOT] = \x04
|
||||
# [STX] = \x02
|
||||
# [ENQ] = \x05
|
||||
# [ETX] = \x03
|
||||
# [ACK] = \x06
|
||||
# BCC = checksum
|
||||
|
||||
standard_device = '/dev/ttyUSB0'
|
||||
|
||||
|
||||
"""
|
||||
Parameter read example:
|
||||
|
||||
Master: [EOT]0011PV[ENQ]
|
||||
Instrument: [STX]PV16.4[ETX]{BCC}
|
||||
|
||||
Writing data:
|
||||
|
||||
Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)<DATA>[ETX](BCC)
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class Lakeshore(TemperatureController):
|
||||
def __init__(self, serial_device, baudrate=57600):
|
||||
self.device = standard_device
|
||||
# timeout: 110 ms to get all answers.
|
||||
self.s = serial.Serial(serial_device,
|
||||
baudrate=baudrate,
|
||||
bytesize=7,
|
||||
parity='O',
|
||||
stopbits=1,
|
||||
timeout=0.4)
|
||||
self._expect_len = 50
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, 's') and self.s is not None:
|
||||
if self.s.is_open:
|
||||
self.s.close()
|
||||
|
||||
#def get_current_temperature(self): # Temp A
|
||||
# self.s.write("KRDG? a\n")
|
||||
# a = self.s.readline().strip()[1:]
|
||||
# return float(a)
|
||||
|
||||
def get_temperature_A(self): # Temp A
|
||||
self.s.write("KRDG? a\n")
|
||||
a = self.s.readline().strip()[1:]
|
||||
return float(a)
|
||||
|
||||
def get_temperature_B(self): # Temp B
|
||||
self.s.write("KRDG? b\n")
|
||||
b = self.s.readline().strip()[1:]
|
||||
return float(b)
|
||||
|
||||
def set_temperature(self, temperature): # setze Setpoint
|
||||
t = temperature
|
||||
self.s.write(f"SETP 1,{t}\n")
|
||||
|
||||
def set_ramp(self, ramp):
|
||||
return self.s.write(f'RAMP 1, 1, {ramp}\n')
|
||||
|
||||
def get_set_temperature(self): # Setpoint abfrage
|
||||
self.s.write("SETP? 1\n")
|
||||
s = self.s.readline().strip()[1:]
|
||||
return float(s)
|
||||
|
||||
def get_output(self): # Heater Output
|
||||
self.s.write("HTR? 1\n")
|
||||
o = self.s.readline().strip()[1:]
|
||||
return float(o)
|
||||
|
||||
def get_ramp(self):
|
||||
self.s.write('RAMP? 1\n')
|
||||
ret = self.s.readline().strip()
|
||||
onoff, rate = ret.split(',')
|
||||
if onoff:
|
||||
return float(rate)
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
def read_param(self, param):
|
||||
"""Read a parameter using simple ASCII commands."""
|
||||
self.s.write(f"{param}?\n")
|
||||
answer = self.s.readline().strip()
|
||||
if answer and len(answer) > 1:
|
||||
# Remove the first character (command echo) and return the value
|
||||
return answer[1:]
|
||||
return None
|
||||
|
||||
def get_value(self, name):
|
||||
temp = self.read_param(name)
|
||||
if temp is None:
|
||||
return np.nan
|
||||
else:
|
||||
try:
|
||||
return float(temp)
|
||||
except ValueError:
|
||||
return np.nan
|
||||
|
||||
# TemperatureController interface implementation
|
||||
|
||||
def get_temperature(self) -> float:
|
||||
"""Read current temperature in Celsius."""
|
||||
temp = self.read_param('KRDG')
|
||||
if temp is None:
|
||||
return 0.0
|
||||
try:
|
||||
return float(temp)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
def get_setpoint(self) -> float:
|
||||
"""Read target setpoint temperature in Celsius."""
|
||||
temp = self.read_param('SETP')
|
||||
if temp is None:
|
||||
return 0.0
|
||||
try:
|
||||
return float(temp)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
def set_setpoint(self, temperature: float) -> bool:
|
||||
"""Set target temperature in Celsius."""
|
||||
self.s.write(f"SETP {temperature}\n")
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
"""Close the serial connection."""
|
||||
if hasattr(self, 's') and self.s is not None:
|
||||
if self.s.is_open:
|
||||
self.s.close()
|
||||
self.s = None
|
||||
|
||||
|
||||
# Register Lakeshore with the factory
|
||||
register_controller('lakeshore', Lakeshore)
|
||||
@@ -0,0 +1,151 @@
|
||||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
Temperature controller interface and base classes.
|
||||
|
||||
This module provides a consistent interface for temperature controllers
|
||||
to enable pluggable temperature monitoring systems.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# Plugin registry for temperature controllers
|
||||
_controller_registry = {}
|
||||
|
||||
|
||||
def register_controller(name: str, controller_class):
|
||||
"""
|
||||
Register a temperature controller implementation.
|
||||
|
||||
Args:
|
||||
name: Unique identifier for this controller type
|
||||
controller_class: Class that implements TemperatureController
|
||||
"""
|
||||
_controller_registry[name] = controller_class
|
||||
|
||||
|
||||
def create_controller(name: str, *args, **kwargs):
|
||||
"""
|
||||
Create a temperature controller instance by name.
|
||||
|
||||
Args:
|
||||
name: Registered controller type name
|
||||
*args: Positional arguments to pass to controller constructor
|
||||
**kwargs: Keyword arguments to pass to controller constructor
|
||||
|
||||
Returns:
|
||||
TemperatureController: Instance of the requested controller
|
||||
|
||||
Raises:
|
||||
ValueError: If controller name is not registered and cannot be imported
|
||||
"""
|
||||
# If not found, try to auto-import the module
|
||||
if name not in _controller_registry:
|
||||
# Map known controller names to their modules
|
||||
module_map = {
|
||||
'simulated': 'damaris.tools.temperature_simulator',
|
||||
'eurotherm': 'damaris.tools.eurotherm',
|
||||
'lakeshore': 'damaris.tools.lakeshore',
|
||||
}
|
||||
if name in module_map:
|
||||
try:
|
||||
# Import the module which will register the controller
|
||||
import importlib
|
||||
importlib.import_module(module_map[name])
|
||||
except ImportError as e:
|
||||
raise ValueError(f"Failed to import controller '{name}': {e}. Available: {list(_controller_registry.keys())}")
|
||||
|
||||
# Check again after import
|
||||
if name not in _controller_registry:
|
||||
raise ValueError(f"Unknown temperature controller: {name}. Available: {list(_controller_registry.keys())}")
|
||||
|
||||
return _controller_registry[name](*args, **kwargs)
|
||||
|
||||
|
||||
def get_controller_names() -> list:
|
||||
"""
|
||||
Get list of available controller names.
|
||||
|
||||
Returns:
|
||||
list: List of registered controller type names
|
||||
"""
|
||||
return list(_controller_registry.keys())
|
||||
|
||||
|
||||
# Pre-register built-in controllers by importing their modules
|
||||
# This ensures they're available without explicit imports
|
||||
try:
|
||||
from . import eurotherm
|
||||
except ImportError as e:
|
||||
# Eurotherm may require serial module which might not be installed
|
||||
# That's OK, it will be imported on-demand if needed
|
||||
pass
|
||||
|
||||
try:
|
||||
from . import lakeshore
|
||||
except ImportError as e:
|
||||
# Lakeshore may require serial module which might not be installed
|
||||
# That's OK, it will be imported on-demand if needed
|
||||
pass
|
||||
|
||||
try:
|
||||
from . import temperature_simulator
|
||||
except ImportError as e:
|
||||
# Simulator should always work (no external dependencies)
|
||||
# But if it fails, it will be imported on-demand
|
||||
pass
|
||||
|
||||
|
||||
class TemperatureController(ABC):
|
||||
"""
|
||||
Abstract base class for temperature controllers.
|
||||
|
||||
All temperature controller implementations (Eurotherm, Lakeshore, etc.)
|
||||
should inherit from this class and implement the abstract methods.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_temperature(self) -> float:
|
||||
"""
|
||||
Read current temperature in Celsius.
|
||||
|
||||
Returns:
|
||||
float: Current temperature in degrees Celsius.
|
||||
|
||||
Raises:
|
||||
IOError: If communication with the device fails.
|
||||
RuntimeError: If the device is not ready or returns invalid data.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_setpoint(self) -> float:
|
||||
"""
|
||||
Read the target setpoint temperature in Celsius.
|
||||
|
||||
Returns:
|
||||
float: Target temperature in degrees Celsius.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_setpoint(self, temperature: float) -> bool:
|
||||
"""
|
||||
Set the target temperature setpoint.
|
||||
|
||||
Args:
|
||||
temperature: Target temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
bool: True if the setpoint was successfully set, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Clean up resources (e.g., close serial connections).
|
||||
|
||||
This method should be called when the controller is no longer needed.
|
||||
The default implementation does nothing; subclasses should override
|
||||
if they have resources to clean up.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: iso-8859-1 -*-
|
||||
"""
|
||||
Simulated temperature controller for testing.
|
||||
|
||||
Provides a temperature controller that simulates real temperature readings
|
||||
with configurable parameters including sinusoidal variation, noise, and missing values.
|
||||
"""
|
||||
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import threading
|
||||
|
||||
from .temperature import TemperatureController, register_controller
|
||||
|
||||
|
||||
class SimulatedTemperatureController(TemperatureController):
|
||||
"""
|
||||
A simulated temperature controller that generates realistic temperature readings.
|
||||
|
||||
This controller is useful for testing and development without requiring
|
||||
physical hardware. It simulates temperature readings with:
|
||||
- Base temperature around 290K (17C)
|
||||
- Sinusoidal variation with amplitude of 10K
|
||||
- 5% random noise
|
||||
- 10% probability of missing values (returns previous value)
|
||||
- 60 second period for the sine wave
|
||||
|
||||
Attributes:
|
||||
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||
period: Period of sine wave in seconds (default: 60.0)
|
||||
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||
"""
|
||||
|
||||
def __init__(self, base_temp: float = 290.0, amplitude: float = 10.0,
|
||||
period: float = 60.0, noise_level: float = 0.05,
|
||||
missing_probability: float = 0.10):
|
||||
"""
|
||||
Initialize the simulated temperature controller.
|
||||
|
||||
Args:
|
||||
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||
period: Period of sine wave in seconds (default: 60.0)
|
||||
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||
"""
|
||||
self.base_temp = float(base_temp)
|
||||
self.amplitude = float(amplitude)
|
||||
self.period = float(period)
|
||||
self.noise_level = float(noise_level)
|
||||
self.missing_probability = float(missing_probability)
|
||||
|
||||
self._start_time = time.time()
|
||||
self._lock = threading.RLock()
|
||||
self._setpoint = self.base_temp
|
||||
self._last_temp = self.base_temp
|
||||
self._last_reading_time = self._start_time
|
||||
|
||||
def get_temperature(self) -> float:
|
||||
"""
|
||||
Get simulated current temperature in Kelvin.
|
||||
|
||||
Returns:
|
||||
float: Simulated temperature in Kelvin
|
||||
"""
|
||||
with self._lock:
|
||||
elapsed = time.time() - self._start_time
|
||||
current_time = time.time()
|
||||
|
||||
# Calculate ideal sinusoidal temperature
|
||||
temp = self.base_temp + self.amplitude * math.sin(2 * math.pi * elapsed / self.period)
|
||||
|
||||
# Add 5% random noise
|
||||
noise = temp * self.noise_level * (random.random() * 2 - 1) # -5% to +5%
|
||||
temp += noise
|
||||
|
||||
# 10% chance of missing value (return last good value)
|
||||
if random.random() < self.missing_probability:
|
||||
temp = self._last_temp
|
||||
else:
|
||||
self._last_temp = temp
|
||||
self._last_reading_time = current_time
|
||||
|
||||
return temp
|
||||
|
||||
def get_setpoint(self) -> float:
|
||||
"""
|
||||
Get the target setpoint temperature in Kelvin.
|
||||
|
||||
Returns:
|
||||
float: Target temperature in Kelvin
|
||||
"""
|
||||
with self._lock:
|
||||
return self._setpoint
|
||||
|
||||
def set_setpoint(self, temperature: float) -> bool:
|
||||
"""
|
||||
Set the target temperature setpoint.
|
||||
|
||||
Args:
|
||||
temperature: Target temperature in Kelvin
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
with self._lock:
|
||||
self._setpoint = float(temperature)
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
"""Clean up resources."""
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
"""Reset the simulation to initial state."""
|
||||
with self._lock:
|
||||
self._start_time = time.time()
|
||||
self._last_temp = self.base_temp
|
||||
self._last_reading_time = self._start_time
|
||||
|
||||
|
||||
# Register the simulated controller with the factory
|
||||
register_controller('simulated', SimulatedTemperatureController)
|
||||
+1
-1
@@ -1,4 +1,5 @@
|
||||
import tables
|
||||
import numpy
|
||||
|
||||
r = MeasurementResult("test")
|
||||
|
||||
@@ -17,7 +18,6 @@ def result():
|
||||
data["test"] = r
|
||||
accu+=ts
|
||||
accu.write_to_hdf(h5, "/", "name", "title")
|
||||
print(accu)
|
||||
try:
|
||||
h5.close()
|
||||
except:
|
||||
|
||||
@@ -105,7 +105,7 @@ class TestAccumulation(unittest.TestCase):
|
||||
self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30])))
|
||||
self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35])))
|
||||
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):
|
||||
"""Test the functionality of writing to CSV."""
|
||||
|
||||
Reference in new issue
Block a user