Public Access
Merge
Merge branch 'main' of gitea.pkm.physik.tu-darmstadt.de:IPKM/python3-damaris
This commit is contained in:
@@ -10,10 +10,8 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: [debian11, debian12, debian13]
|
target: [debian12, debian13]
|
||||||
include:
|
include:
|
||||||
- target: debian11
|
|
||||||
codename: bullseye
|
|
||||||
- target: debian12
|
- target: debian12
|
||||||
codename: bookworm
|
codename: bookworm
|
||||||
- target: debian13
|
- target: debian13
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ __pycache__
|
|||||||
debian/files
|
debian/files
|
||||||
.pybuild
|
.pybuild
|
||||||
.junie
|
.junie
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/.debhelper
|
||||||
|
|||||||
@@ -15,3 +15,12 @@ Alternatively you can install withoout the system-site-packages:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
This will download newer packages and install them in the venv.
|
This will download newer packages and install them in the venv.
|
||||||
|
|
||||||
|
|
||||||
|
Using uv
|
||||||
|
========
|
||||||
|
Dependicies needed:
|
||||||
|
sudo apt install libgirepository1.0-dev pkg-config
|
||||||
|
|
||||||
|
Then you can start DAMARIS3:
|
||||||
|
uv run DAMARIS3
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "python3-damaris"
|
||||||
|
version = "0.22"
|
||||||
|
requires-python = ">= 3.11"
|
||||||
|
description = "Python 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
|
||||||
|
license = {text = "GPL-2.0"}
|
||||||
|
keywords = ["NMR", "data-processing"]
|
||||||
|
readme = {file = "README", content-type = "text/plain"}
|
||||||
|
dependencies = [
|
||||||
|
"scipy>=1.7",
|
||||||
|
"numpy>=1.21,<2.0",
|
||||||
|
"tables>=3.7",
|
||||||
|
"matplotlib>=3.5,<3.11",
|
||||||
|
"pyxdg",
|
||||||
|
# PyGObject must come from system packages:
|
||||||
|
# apt-get install python3-gi gir1.2-gtk-3.0 gir1.2-gtksource-3.0
|
||||||
|
]
|
||||||
|
authors = [
|
||||||
|
{name = "Achim Gädke"},
|
||||||
|
{name = "Christopher Schmitt"},
|
||||||
|
{name = "Christian Tacke"},
|
||||||
|
{name = "Joachim Beerwerth"},
|
||||||
|
{name = "Markus Rosenstihl", email = "markus.rosenstihl@pkm.tu-darmstadt.de"}
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
DAMARIS3 = "damaris.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"damaris.gui" = ["DAMARIS3.png", "DAMARIS3.ico", "python.xml", "damaris3.glade"]
|
||||||
+3
-2
@@ -6,7 +6,7 @@ requires = [
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
requires-python = ">= 3.9"
|
requires-python = ">= 3.11"
|
||||||
name = "python3-damaris"
|
name = "python3-damaris"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Python version 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
|
description = "Python version 3 frontend for DAMARIS (DArmstadt MAgnetic Resonance Instrument Software)"
|
||||||
@@ -19,7 +19,8 @@ dependencies = [
|
|||||||
"tables >= 3.1.1",
|
"tables >= 3.1.1",
|
||||||
"matplotlib >= 1.4.1, <= 3.6",
|
"matplotlib >= 1.4.1, <= 3.6",
|
||||||
"pyxdg",
|
"pyxdg",
|
||||||
"PyGObject >= 3.14.0, <= 3.51.0"
|
"PyGObject >= 3.14.0, <= 3.51.0",
|
||||||
|
"pyserial>=3.5",
|
||||||
]
|
]
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Achim Gädke"},
|
{name = "Achim Gädke"},
|
||||||
|
|||||||
+117
-28
@@ -9,6 +9,7 @@ import numpy
|
|||||||
import sys
|
import sys
|
||||||
import datetime
|
import datetime
|
||||||
import tables
|
import tables
|
||||||
|
from scipy.signal import filtfilt, remez
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# #
|
# #
|
||||||
# Name: Class ADC_Result #
|
# Name: Class ADC_Result #
|
||||||
@@ -93,8 +94,6 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
print("Warning ADC-Result: Tried to run \"create_data_space()\" more than once.")
|
print("Warning ADC-Result: Tried to run \"create_data_space()\" more than once.")
|
||||||
return
|
return
|
||||||
|
|
||||||
raise ValueError("ValueError: You cant create an ADC-Result with less than 1 sample!")
|
|
||||||
|
|
||||||
for i in range(channels):
|
for i in range(channels):
|
||||||
self.y.append(numpy.zeros((samples,), dtype="int16"))
|
self.y.append(numpy.zeros((samples,), dtype="int16"))
|
||||||
|
|
||||||
@@ -147,6 +146,97 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
return self.is_clipped
|
return self.is_clipped
|
||||||
|
|
||||||
|
|
||||||
|
def lowpass(self, cutoff, numtaps=None):
|
||||||
|
"""
|
||||||
|
Apply a zero-phase lowpass FIR filter to all channels.
|
||||||
|
|
||||||
|
Uses scipy.signal.filtfilt (forward-backward filtering) with a Parks-McClellan
|
||||||
|
FIR filter (remez). The forward-backward pass guarantees zero phase distortion
|
||||||
|
and doubles the effective filter order.
|
||||||
|
|
||||||
|
The stopband is automatically set to 0.1 * Nyquist beyond the cutoff. If
|
||||||
|
numtaps is not provided, it is estimated from the transition width to yield
|
||||||
|
approximately 40-50 dB stopband attenuation (typically ~40 taps).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cutoff : float
|
||||||
|
Passband edge frequency in Hz. Frequencies above this value are attenuated.
|
||||||
|
Must be a positive number below the Nyquist frequency.
|
||||||
|
numtaps : int, optional
|
||||||
|
Number of FIR taps. If omitted, computed automatically from the transition
|
||||||
|
width.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ADC_Result
|
||||||
|
A new ADC_Result with the filtered data (float64 per channel).
|
||||||
|
The original object is not modified.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If the result contains no data.
|
||||||
|
ValueError
|
||||||
|
If cutoff is invalid for the current sampling rate.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> # Lowpass at 1 MHz (taps auto-computed, ~40)
|
||||||
|
... filtered = adc.lowpass(cutoff=1e6)
|
||||||
|
>>> # Lowpass at 500 kHz with fixed 63 taps
|
||||||
|
... filtered = adc.lowpass(cutoff=500e3, numtaps=63)
|
||||||
|
"""
|
||||||
|
if not self.contains_data():
|
||||||
|
raise RuntimeError("lowpass: no data present")
|
||||||
|
|
||||||
|
nyquist = self.sampling_rate / 2.0
|
||||||
|
cutoff = float(cutoff)
|
||||||
|
|
||||||
|
if cutoff <= 0 or cutoff >= nyquist:
|
||||||
|
raise ValueError(
|
||||||
|
f"cutoff {cutoff} Hz is outside valid range (0, {nyquist})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transition width: 0.1 * Nyquist
|
||||||
|
trans_width = 0.1 * nyquist
|
||||||
|
stopband = min(cutoff + trans_width, nyquist * 0.99)
|
||||||
|
|
||||||
|
# Auto-estimate taps if not given
|
||||||
|
if numtaps is None:
|
||||||
|
numtaps = max(21, int(round(8.0 / (stopband - cutoff) * nyquist)))
|
||||||
|
if numtaps % 2 == 0:
|
||||||
|
numtaps += 1
|
||||||
|
|
||||||
|
coeffs = remez(numtaps, [0, cutoff, stopband, nyquist], [1, 0], fs=self.sampling_rate)
|
||||||
|
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
tmp_y = []
|
||||||
|
for i in range(self.get_number_of_channels()):
|
||||||
|
data = numpy.asarray(self.y[i], dtype='float64')
|
||||||
|
filtered = filtfilt(
|
||||||
|
coeffs, [1.0], data,
|
||||||
|
padtype='odd',
|
||||||
|
padlen=(numtaps - 1) * 3
|
||||||
|
)
|
||||||
|
tmp_y.append(filtered)
|
||||||
|
|
||||||
|
r = ADC_Result(
|
||||||
|
x=self.x[:],
|
||||||
|
y=tmp_y,
|
||||||
|
index=self.index[:],
|
||||||
|
sampl_freq=self.sampling_rate,
|
||||||
|
desc=self.description,
|
||||||
|
job_id=self.job_id,
|
||||||
|
job_date=self.job_date,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def add_sample_space(self, samples):
|
def add_sample_space(self, samples):
|
||||||
"Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
|
"Adds space for n samples, where n can also be negative (deletes space). New space is filled up with \"0\""
|
||||||
|
|
||||||
@@ -220,26 +310,25 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
suitable for further processing
|
suitable for further processing
|
||||||
"""
|
"""
|
||||||
# write sorted
|
if isinstance(destination, str):
|
||||||
the_destination=destination
|
with open(destination, "w") as f:
|
||||||
if type(destination) in (str,):
|
self._write_to_csv_dest(f, delimiter)
|
||||||
the_destination=open(destination, "w")
|
else:
|
||||||
|
self._write_to_csv_dest(destination, delimiter)
|
||||||
|
|
||||||
the_destination.write("# adc_result\n")
|
def _write_to_csv_dest(self, dest, delimiter):
|
||||||
the_destination.write("# t y0 y1 ...\n")
|
dest.write("# adc_result\n")
|
||||||
|
dest.write("# t y0 y1 ...\n")
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
xdata=self.get_xdata()
|
xdata=self.get_xdata()
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
#yerr=map(self.get_yerr, xrange(ch_no))
|
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
the_destination.write("%e"%xdata[i])
|
dest.write("%e"%xdata[i])
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
the_destination.write("%s%e"%(delimiter, ydata[j][i]))
|
dest.write("%s%e"%(delimiter, ydata[j][i]))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
the_destination=None
|
|
||||||
xdata=ydata=None
|
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
@@ -249,28 +338,28 @@ class ADC_Result(Resultable, Drawable, DamarisFFT, Signalpath):
|
|||||||
for further processing with the NMRnotebook software;
|
for further processing with the NMRnotebook software;
|
||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
"""
|
"""
|
||||||
# write sorted
|
if isinstance(destination, str):
|
||||||
the_destination=destination
|
with open(destination, "w") as f:
|
||||||
if type(destination) in (str,):
|
self._write_to_simpson_dest(f, delimiter)
|
||||||
the_destination=open(destination, "w")
|
else:
|
||||||
|
self._write_to_simpson_dest(destination, delimiter)
|
||||||
|
|
||||||
|
def _write_to_simpson_dest(self, dest, delimiter):
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
xdata=self.get_xdata()
|
xdata=self.get_xdata()
|
||||||
the_destination.write("SIMP\n")
|
dest.write("SIMP\n")
|
||||||
the_destination.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
dest.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
||||||
the_destination.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
dest.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
||||||
the_destination.write("TYPE=FID\n")
|
dest.write("TYPE=FID\n")
|
||||||
the_destination.write("DATA\n")
|
dest.write("DATA\n")
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
the_destination.write("%g%s"%(ydata[j][i], delimiter))
|
dest.write("%g%s"%(ydata[j][i], delimiter))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
the_destination.write("END\n")
|
dest.write("END\n")
|
||||||
the_destination=None
|
|
||||||
xdata=ydata=None
|
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
|
|||||||
@@ -248,38 +248,41 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
writes the data to a file.
|
writes the data to a file.
|
||||||
destination can be a filehandle or a filename, default sys.stdout
|
destination can be a filehandle or a filename, default sys.stdout
|
||||||
"""
|
"""
|
||||||
the_destination=destination
|
|
||||||
if isinstance(destination, str):
|
if isinstance(destination, str):
|
||||||
the_destination=open(destination, "w")
|
with open(destination, "w") as f:
|
||||||
|
self._write_to_csv_dest(f, delimiter)
|
||||||
|
else:
|
||||||
|
self._write_to_csv_dest(destination, delimiter)
|
||||||
|
|
||||||
the_destination.write("# accumulation %d\n"%self.n)
|
def _write_to_csv_dest(self, dest, delimiter):
|
||||||
|
dest.write("# accumulation %d\n"%self.n)
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
if self.common_descriptions is not None:
|
if self.common_descriptions is not None:
|
||||||
for (key,value) in self.common_descriptions.items():
|
for (key,value) in self.common_descriptions.items():
|
||||||
the_destination.write("# %s : %s\n"%(key, str(value)))
|
dest.write("# %s : %s\n"%(key, str(value)))
|
||||||
the_destination.write("# t")
|
dest.write("# t")
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
if self.use_error:
|
if self.use_error:
|
||||||
for i in range(ch_no):
|
for i in range(ch_no):
|
||||||
the_destination.write(" ch%d_mean ch%d_err"%(i,i))
|
dest.write(" ch%d_mean ch%d_err"%(i,i))
|
||||||
else:
|
else:
|
||||||
for i in range(ch_no):
|
for i in range(ch_no):
|
||||||
the_destination.write(" ch%d_mean"%i)
|
dest.write(" ch%d_mean"%i)
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
xdata=self.get_xdata()
|
xdata=self.get_xdata()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
yerr=None
|
yerr=None
|
||||||
if self.use_error:
|
if self.use_error:
|
||||||
yerr=list(map(self.get_yerr, range(ch_no)))
|
yerr=list(map(self.get_yerr, range(ch_no)))
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
the_destination.write("%e"%xdata[i])
|
dest.write("%e"%xdata[i])
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
if self.use_error:
|
if self.use_error:
|
||||||
the_destination.write("%s%e%s%e"%(delimiter, ydata[j][i], delimiter, yerr[j][i]))
|
dest.write("%s%e%s%e"%(delimiter, ydata[j][i], delimiter, yerr[j][i]))
|
||||||
else:
|
else:
|
||||||
the_destination.write("%s%e"%(delimiter,ydata[j][i]))
|
dest.write("%s%e"%(delimiter,ydata[j][i]))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
@@ -290,27 +293,29 @@ class Accumulation(Errorable, Drawable, DamarisFFT, Signalpath):
|
|||||||
for further processing with the NMRnotebook software;
|
for further processing with the NMRnotebook software;
|
||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
"""
|
"""
|
||||||
the_destination=destination
|
|
||||||
if isinstance(destination, str):
|
if isinstance(destination, str):
|
||||||
the_destination=open(destination, "w")
|
with open(destination, "w") as f:
|
||||||
|
self._write_to_simpson_dest(f, delimiter, frequency)
|
||||||
|
else:
|
||||||
|
self._write_to_simpson_dest(destination, delimiter, frequency)
|
||||||
|
|
||||||
|
def _write_to_simpson_dest(self, dest, delimiter, frequency):
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
xdata=self.get_xdata()
|
xdata=self.get_xdata()
|
||||||
the_destination.write("SIMP\n")
|
dest.write("SIMP\n")
|
||||||
the_destination.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
dest.write("%s%i%s"%("NP=", len(xdata), "\n"))
|
||||||
the_destination.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
dest.write("%s%i%s"%("SW=", self.get_sampling_rate(), "\n"))
|
||||||
the_destination.write("%s%i%s"%("REF=", frequency, "\n"))
|
dest.write("%s%i%s"%("REF=", frequency, "\n"))
|
||||||
the_destination.write("TYPE=FID\n")
|
dest.write("TYPE=FID\n")
|
||||||
the_destination.write("DATA\n")
|
dest.write("DATA\n")
|
||||||
ch_no=self.get_number_of_channels()
|
ch_no=self.get_number_of_channels()
|
||||||
ydata=list(map(self.get_ydata, range(ch_no)))
|
ydata=list(map(self.get_ydata, range(ch_no)))
|
||||||
for i in range(len(xdata)):
|
for i in range(len(xdata)):
|
||||||
for j in range(ch_no):
|
for j in range(ch_no):
|
||||||
the_destination.write("%g%s"%(ydata[j][i], delimiter))
|
dest.write("%g%s"%(ydata[j][i], delimiter))
|
||||||
the_destination.write("\n")
|
dest.write("\n")
|
||||||
the_destination.write("END\n")
|
dest.write("END\n")
|
||||||
the_destination.close()
|
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
|
|||||||
@@ -366,6 +366,12 @@ class DataPool(collections.abc.MutableMapping):
|
|||||||
if obj is not None:
|
if obj is not None:
|
||||||
self[full_path] = obj
|
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
|
# Skip further processing of this group since we handled it
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import threading
|
import threading
|
||||||
import math
|
import math
|
||||||
import types
|
|
||||||
import sys
|
import sys
|
||||||
import tables
|
import tables
|
||||||
import numpy
|
import numpy
|
||||||
@@ -45,7 +44,7 @@ class AccumulatedValue:
|
|||||||
|
|
||||||
def __add__(self,y):
|
def __add__(self,y):
|
||||||
new_one=AccumulatedValue()
|
new_one=AccumulatedValue()
|
||||||
if (type(y) is types.InstanceType and isinstance(y, AccumulatedValue)):
|
if isinstance(y, AccumulatedValue):
|
||||||
new_one.y=self.y+y.y
|
new_one.y=self.y+y.y
|
||||||
new_one.y2=self.y2+y.y2
|
new_one.y2=self.y2+y.y2
|
||||||
new_one.n=self.n+y.n
|
new_one.n=self.n+y.n
|
||||||
@@ -56,7 +55,7 @@ class AccumulatedValue:
|
|||||||
return new_one
|
return new_one
|
||||||
|
|
||||||
def __iadd__(self,y):
|
def __iadd__(self,y):
|
||||||
if (type(y) is types.InstanceType and isinstance(y, AccumulatedValue)):
|
if isinstance(y, AccumulatedValue):
|
||||||
self.y+=y.y
|
self.y+=y.y
|
||||||
self.y2+=y.y2
|
self.y2+=y.y2
|
||||||
self.n+=y.n
|
self.n+=y.n
|
||||||
@@ -203,28 +202,27 @@ class MeasurementResult(Drawable.Drawable, collections.UserDict):
|
|||||||
destination can be a file or a filename
|
destination can be a file or a filename
|
||||||
suitable for further processing
|
suitable for further processing
|
||||||
"""
|
"""
|
||||||
the_destination=destination
|
if isinstance(destination, str):
|
||||||
file_opened = False
|
with open(destination, "w") as f:
|
||||||
if type(destination) in (str,):
|
self._write_to_csv_dest(f, delimiter)
|
||||||
the_destination=open(destination, "w")
|
else:
|
||||||
file_opened = True
|
self._write_to_csv_dest(destination, delimiter)
|
||||||
|
|
||||||
the_destination.write("# quantity:"+str(self.quantity_name)+"\n")
|
def _write_to_csv_dest(self, dest, delimiter):
|
||||||
the_destination.write("# x y ysigma n\n")
|
dest.write("# quantity:"+str(self.quantity_name)+"\n")
|
||||||
|
dest.write("# x y ysigma n\n")
|
||||||
for x in self.get_xdata():
|
for x in self.get_xdata():
|
||||||
y = self.data[x]
|
y = self.data[x]
|
||||||
if type(y) in [float, int, int]:
|
if isinstance(y, (float, int)):
|
||||||
the_destination.write("%e%s%e%s0%s1\n"%(x, delimiter, y, delimiter, delimiter))
|
dest.write("%e%s%e%s0%s1\n" % (x, delimiter, y, delimiter, delimiter))
|
||||||
else:
|
else:
|
||||||
the_destination.write("%e%s%e%s%e%s%d\n"%(x,
|
dest.write("%e%s%e%s%e%s%d\n" % (x,
|
||||||
delimiter,
|
delimiter,
|
||||||
y.mean(),
|
y.mean(),
|
||||||
delimiter,
|
delimiter,
|
||||||
y.mean_error(),
|
y.mean_error(),
|
||||||
delimiter,
|
delimiter,
|
||||||
y.n))
|
y.n))
|
||||||
if file_opened:
|
|
||||||
the_destination.close()
|
|
||||||
|
|
||||||
|
|
||||||
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from .Resultable import Resultable
|
from .Resultable import Resultable
|
||||||
from .Drawable import Drawable
|
from .Drawable import Drawable
|
||||||
|
import numpy
|
||||||
|
import threading
|
||||||
|
import tables
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# #
|
# #
|
||||||
@@ -15,17 +18,224 @@ from .Drawable import Drawable
|
|||||||
class TemperatureResult(Resultable, Drawable):
|
class TemperatureResult(Resultable, Drawable):
|
||||||
"""
|
"""
|
||||||
Specialised class of Resultable and Drawable
|
Specialised class of Resultable and Drawable
|
||||||
Contains recorded temperature data
|
Contains recorded temperature data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
x: List of timestamps in seconds.
|
||||||
|
y: List of temperature readings in Celsius.
|
||||||
|
setpoint: Optional list of setpoint temperatures in Celsius.
|
||||||
|
xlabel: Label for x-axis (default: "Time (s)").
|
||||||
|
ylabel: Label for y-axis (default: "Temperature (C)").
|
||||||
"""
|
"""
|
||||||
def __init__(self, x = None, y = None, desc = None, job_id = None, job_date = None):
|
def __init__(self, x = None, y = None, setpoint = None, desc = None, job_id = None, job_date = None):
|
||||||
Resultable.__init__(self)
|
Resultable.__init__(self)
|
||||||
Drawable.__init__(self)
|
Drawable.__init__(self)
|
||||||
|
|
||||||
if (x is None) and (y is None) and (desc is None) and (job_id is None) and (job_date is None):
|
# Set default labels
|
||||||
pass
|
self.xlabel = "Time (s)"
|
||||||
|
self.ylabel = "Temperature (C)"
|
||||||
|
|
||||||
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):
|
# Initialize data lists
|
||||||
pass
|
self.x = [] if x is None else x
|
||||||
|
self.y = [] if y is None else y
|
||||||
|
self.setpoint = [] if setpoint is None else setpoint
|
||||||
|
|
||||||
else:
|
# Set metadata if provided
|
||||||
raise ValueError("Wrong usage of __init__!")
|
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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from damaris.experiments import Experiment
|
||||||
|
|
||||||
|
|
||||||
|
class DurationEstimator:
|
||||||
|
"""
|
||||||
|
Consumes an experiment script's generator and estimates total duration
|
||||||
|
without writing any files or invoking the backend.
|
||||||
|
|
||||||
|
Sleeps and synchronizations in the script are tracked instead of
|
||||||
|
blocking, so their wall-clock time is included in the estimate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def estimate(self, script):
|
||||||
|
"""
|
||||||
|
Run the experiment script in a sandbox and return a duration estimate.
|
||||||
|
|
||||||
|
:param str script: experiment script source code
|
||||||
|
:returns: dict with 'experiment_time', 'sleep_time', 'total_time', and 'job_count'
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
sleep_time = [0.0] # list so it's mutable in nested function
|
||||||
|
|
||||||
|
def tracking_sleep(seconds):
|
||||||
|
sleep_time[0] += seconds
|
||||||
|
|
||||||
|
def noop_sync(before=0, waitsteps=0.1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def noop_data():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
dataspace = {}
|
||||||
|
exp_classes = __import__('damaris.experiments', dataspace, dataspace, ['Experiment'])
|
||||||
|
for name in dir(exp_classes):
|
||||||
|
if name[:2] == "__" and name[-2:] == "__":
|
||||||
|
continue
|
||||||
|
dataspace[name] = exp_classes.__dict__[name]
|
||||||
|
del exp_classes
|
||||||
|
|
||||||
|
# ranges module for lin_range, log_range, etc.
|
||||||
|
ranges = __import__('damaris.tools.ranges', dataspace, dataspace, [])
|
||||||
|
for name in dir(ranges):
|
||||||
|
if name[:2] == "__" and name[-2:] == "__":
|
||||||
|
continue
|
||||||
|
dataspace[name] = getattr(ranges, name)
|
||||||
|
del ranges
|
||||||
|
|
||||||
|
# inject sandbox helpers (non-blocking versions)
|
||||||
|
dataspace["data"] = noop_data
|
||||||
|
dataspace["synchronize"] = noop_sync
|
||||||
|
dataspace["sleep"] = tracking_sleep
|
||||||
|
|
||||||
|
# execute the script
|
||||||
|
try:
|
||||||
|
exec(script, dataspace)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise RuntimeError("Script execution failed") from e
|
||||||
|
|
||||||
|
if "experiment" not in dataspace:
|
||||||
|
raise ValueError("Script does not define an experiment() function")
|
||||||
|
|
||||||
|
# consume the generator
|
||||||
|
try:
|
||||||
|
exp_iterator = dataspace["experiment"]()
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise RuntimeError("Calling experiment() failed") from e
|
||||||
|
|
||||||
|
experiment_time = 0.0
|
||||||
|
job_count = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
job = next(exp_iterator)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise RuntimeError("Generator failed") from e
|
||||||
|
|
||||||
|
experiment_time += job.get_length()
|
||||||
|
job_count += 1
|
||||||
|
|
||||||
|
total_time = experiment_time + sleep_time[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"experiment_time": experiment_time,
|
||||||
|
"sleep_time": sleep_time[0],
|
||||||
|
"total_time": total_time,
|
||||||
|
"job_count": job_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_duration(script):
|
||||||
|
"""
|
||||||
|
Convenience function to estimate experiment duration.
|
||||||
|
|
||||||
|
:param str script: experiment script source code
|
||||||
|
:returns: dict with 'experiment_time', 'sleep_time', 'total_time', and 'job_count'
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
return DurationEstimator().estimate(script)
|
||||||
@@ -61,7 +61,8 @@ from damaris.gui import ResultReader, ResultHandling
|
|||||||
from damaris.gui import BackendDriver
|
from damaris.gui import BackendDriver
|
||||||
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader
|
#from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader
|
||||||
#from damaris.data import Drawable # this is a base class, it should be used...
|
#from damaris.data import Drawable # this is a base class, it should be used...
|
||||||
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult
|
from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult, TemperatureResult
|
||||||
|
# TemperatureMonitor imported lazily to avoid circular import issues
|
||||||
|
|
||||||
# default, can be set to true from start script
|
# default, can be set to true from start script
|
||||||
debug = False
|
debug = False
|
||||||
@@ -181,6 +182,10 @@ class DamarisGUI:
|
|||||||
#to stop queued experiments
|
#to stop queued experiments
|
||||||
self.stop_experiment_flag = threading.Event()
|
self.stop_experiment_flag = threading.Event()
|
||||||
|
|
||||||
|
# Temperature monitoring
|
||||||
|
self.temp_monitor = None
|
||||||
|
self.temp_controller = None
|
||||||
|
|
||||||
exp_script = ""
|
exp_script = ""
|
||||||
if exp_script_filename is not None and exp_script_filename != "":
|
if exp_script_filename is not None and exp_script_filename != "":
|
||||||
self.sw.exp_script_filename = exp_script_filename[ : ]
|
self.sw.exp_script_filename = exp_script_filename[ : ]
|
||||||
@@ -205,6 +210,7 @@ class DamarisGUI:
|
|||||||
|
|
||||||
#connect event handlers
|
#connect event handlers
|
||||||
eventhandlers = {"on_toolbar_run_button_clicked": self.start_experiment,
|
eventhandlers = {"on_toolbar_run_button_clicked": self.start_experiment,
|
||||||
|
"on_toolbar_estimate_button_clicked": self.estimate_duration,
|
||||||
"on_toolbar_pause_button_toggled": self.pause_experiment,
|
"on_toolbar_pause_button_toggled": self.pause_experiment,
|
||||||
"on_toolbar_stop_button_clicked": self.stop_experiment,
|
"on_toolbar_stop_button_clicked": self.stop_experiment,
|
||||||
"on_doc_menu_activate": self.show_doc_menu,
|
"on_doc_menu_activate": self.show_doc_menu,
|
||||||
@@ -233,7 +239,6 @@ class DamarisGUI:
|
|||||||
"on_display_y_scaling_combobox_changed": self.monitor.display_scaling_changed,
|
"on_display_y_scaling_combobox_changed": self.monitor.display_scaling_changed,
|
||||||
"on_display_save_data_as_text_button_clicked": self.monitor.save_display_data_as_text,
|
"on_display_save_data_as_text_button_clicked": self.monitor.save_display_data_as_text,
|
||||||
"on_display_copy_data_to_clipboard_button_clicked": self.monitor.copy_display_data_to_clipboard,
|
"on_display_copy_data_to_clipboard_button_clicked": self.monitor.copy_display_data_to_clipboard,
|
||||||
"on_data_handling_textview_event": self.sw.textviews_modified,
|
|
||||||
"on_experiment_script_textview_event": self.sw.textviews_modified,
|
"on_experiment_script_textview_event": self.sw.textviews_modified,
|
||||||
"on_main_notebook_switch_page": self.nopevent,
|
"on_main_notebook_switch_page": self.nopevent,
|
||||||
"main_window_close": self.nopevent
|
"main_window_close": self.nopevent
|
||||||
@@ -384,6 +389,117 @@ class DamarisGUI:
|
|||||||
|
|
||||||
# toolbar related events:
|
# toolbar related events:
|
||||||
|
|
||||||
|
def estimate_duration( self, widget, data=None ):
|
||||||
|
"""
|
||||||
|
Estimate the total experiment duration and show a dialog.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from damaris.experiments.DurationEstimator import estimate_duration
|
||||||
|
|
||||||
|
exp_script, _ = self.sw.get_scripts()
|
||||||
|
|
||||||
|
# validate syntax first
|
||||||
|
try:
|
||||||
|
compile(exp_script, "Experiment Script", "exec")
|
||||||
|
except SyntaxError as e:
|
||||||
|
ln = e.lineno if isinstance(e.lineno, int) else 0
|
||||||
|
lo = e.offset if isinstance(e.offset, int) else 0
|
||||||
|
msg = "Experiment Script: %s at line %d, col %d" % (e.__class__.__name__, ln, lo)
|
||||||
|
dialog = gtk.MessageDialog(
|
||||||
|
parent=self.main_window,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
type=gtk.MessageType.ERROR,
|
||||||
|
buttons=gtk.ButtonsType.OK,
|
||||||
|
message_format=msg
|
||||||
|
)
|
||||||
|
dialog.run()
|
||||||
|
dialog.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
# run estimator in a background thread to not freeze the GUI
|
||||||
|
dialog = gtk.MessageDialog(
|
||||||
|
parent=self.main_window,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
type=gtk.MessageType.INFO,
|
||||||
|
buttons=gtk.ButtonsType.CLOSE,
|
||||||
|
message_format="Estimating duration..."
|
||||||
|
)
|
||||||
|
dialog.set_resizable(False)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
result = [None]
|
||||||
|
error = [None]
|
||||||
|
done = threading.Event()
|
||||||
|
|
||||||
|
def run_estimate():
|
||||||
|
try:
|
||||||
|
result[0] = estimate_duration(exp_script)
|
||||||
|
except Exception as e:
|
||||||
|
error[0] = str(e)
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
t = threading.Thread(target=run_estimate)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
while not done.is_set():
|
||||||
|
while gtk.events_pending():
|
||||||
|
gtk.main_iteration_do(False)
|
||||||
|
if done.wait(0.1):
|
||||||
|
break
|
||||||
|
|
||||||
|
dialog.hide()
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
if error[0] is not None:
|
||||||
|
dialog = gtk.MessageDialog(
|
||||||
|
parent=self.main_window,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
type=gtk.MessageType.ERROR,
|
||||||
|
buttons=gtk.ButtonsType.OK,
|
||||||
|
message_format="Estimation failed: " + error[0]
|
||||||
|
)
|
||||||
|
dialog.run()
|
||||||
|
dialog.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
r = result[0]
|
||||||
|
total = r["total_time"]
|
||||||
|
exp_time = r["experiment_time"]
|
||||||
|
sleep_t = r["sleep_time"]
|
||||||
|
jobs = r["job_count"]
|
||||||
|
|
||||||
|
# format duration as HH:MM:SS
|
||||||
|
def fmt(seconds):
|
||||||
|
h = int(seconds) // 3600
|
||||||
|
m = (int(seconds) % 3600) // 60
|
||||||
|
s = int(seconds) % 60
|
||||||
|
return "%02d:%02d:%02d" % (h, m, s)
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
end = now + timedelta(seconds=total)
|
||||||
|
end_str = end.strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
text = (
|
||||||
|
"Jobs: %d\n"
|
||||||
|
"Pulse program: %s (%.2f s)\n"
|
||||||
|
"Sleeps: %s (%.2f s)\n"
|
||||||
|
"---------------------------\n"
|
||||||
|
"Total: %s\n\n"
|
||||||
|
"Start now:\n"
|
||||||
|
"Estimated end: %s"
|
||||||
|
) % (jobs, fmt(exp_time), exp_time, fmt(sleep_t), sleep_t, fmt(total), end_str)
|
||||||
|
|
||||||
|
dialog = gtk.MessageDialog(
|
||||||
|
parent=self.main_window,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
type=gtk.MessageType.INFO,
|
||||||
|
buttons=gtk.ButtonsType.OK,
|
||||||
|
message_format=text
|
||||||
|
)
|
||||||
|
dialog.run()
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
def start_experiment( self, widget, data=None ):
|
def start_experiment( self, widget, data=None ):
|
||||||
|
|
||||||
# something running?
|
# something running?
|
||||||
@@ -512,6 +628,8 @@ class DamarisGUI:
|
|||||||
self.data = self.si.data
|
self.data = self.si.data
|
||||||
# run frontend and script engines
|
# run frontend and script engines
|
||||||
self.monitor.observe_data_pool( self.data )
|
self.monitor.observe_data_pool( self.data )
|
||||||
|
# Start temperature monitoring if configured (before scripts start)
|
||||||
|
self.start_temperature_monitoring(actual_config)
|
||||||
self.si.runScripts( )
|
self.si.runScripts( )
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
|
||||||
@@ -572,6 +690,7 @@ class DamarisGUI:
|
|||||||
self.dump_filename = ""
|
self.dump_filename = ""
|
||||||
if actual_config[ "data_pool_name" ] != "":
|
if actual_config[ "data_pool_name" ] != "":
|
||||||
self.dump_states( init=True )
|
self.dump_states( init=True )
|
||||||
|
|
||||||
gobject.timeout_add( 200, self.observe_running_experiment )
|
gobject.timeout_add( 200, self.observe_running_experiment )
|
||||||
|
|
||||||
def observe_running_experiment( self ):
|
def observe_running_experiment( self ):
|
||||||
@@ -684,6 +803,8 @@ class DamarisGUI:
|
|||||||
|
|
||||||
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f]
|
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f]
|
||||||
if len( still_running ) == 0:
|
if len( still_running ) == 0:
|
||||||
|
# Stop temperature monitoring when experiment finishes
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
if self.save_thread is None and self.dump_filename != "":
|
if self.save_thread is None and self.dump_filename != "":
|
||||||
print("all subprocesses ended, saving data pool")
|
print("all subprocesses ended, saving data pool")
|
||||||
# thread to save data...
|
# thread to save data...
|
||||||
@@ -974,8 +1095,98 @@ class DamarisGUI:
|
|||||||
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f]
|
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f]
|
||||||
for r in still_running:
|
for r in still_running:
|
||||||
r.quit_flag.set( )
|
r.quit_flag.set( )
|
||||||
|
|
||||||
|
# Stop temperature monitoring
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
|
|
||||||
self.state = DamarisGUI.Stop_State
|
self.state = DamarisGUI.Stop_State
|
||||||
|
|
||||||
|
def start_temperature_monitoring(self, config):
|
||||||
|
"""
|
||||||
|
Start temperature monitoring thread if configured.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary from self.config.get()
|
||||||
|
"""
|
||||||
|
# Check if temperature monitoring is enabled
|
||||||
|
if not config.get("enable_temperature_monitoring", False):
|
||||||
|
# Stop any existing monitor if temperature monitoring is disabled
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stop any existing monitor before starting a new one
|
||||||
|
if self.temp_monitor is not None:
|
||||||
|
self.stop_temperature_monitoring()
|
||||||
|
|
||||||
|
controller_type = config.get("temperature_controller_type", "eurotherm")
|
||||||
|
temp_interval = config.get("temperature_interval", 2.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the factory and TemperatureMonitor (lazy import to avoid circular issues)
|
||||||
|
from damaris.tools.temperature import create_controller
|
||||||
|
from damaris.gui.TemperatureMonitor import TemperatureMonitor
|
||||||
|
|
||||||
|
# Create controller based on type
|
||||||
|
if controller_type == "eurotherm":
|
||||||
|
temp_device = config.get("temperature_device", "")
|
||||||
|
temp_baudrate = config.get("temperature_baudrate", 19200)
|
||||||
|
|
||||||
|
if not temp_device:
|
||||||
|
print("Temperature monitoring: No device configured for Eurotherm")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.temp_controller = create_controller(
|
||||||
|
"eurotherm", temp_device, baudrate=temp_baudrate
|
||||||
|
)
|
||||||
|
print(f"Temperature monitoring started with Eurotherm on {temp_device} (interval: {temp_interval}s)")
|
||||||
|
|
||||||
|
elif controller_type == "simulated":
|
||||||
|
# Simulated controller doesn't need device configuration
|
||||||
|
# Use default parameters: 290K base, 10K amplitude, 60s period, 5% noise, 10% missing
|
||||||
|
self.temp_controller = create_controller("simulated")
|
||||||
|
print(f"Temperature monitoring started with simulated controller (interval: {temp_interval}s)")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Temperature monitoring: Unknown controller type: {controller_type}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create and start monitor thread
|
||||||
|
self.temp_monitor = TemperatureMonitor(
|
||||||
|
controller=self.temp_controller,
|
||||||
|
interval=temp_interval,
|
||||||
|
data_pool=self.data,
|
||||||
|
data_key="temperature"
|
||||||
|
)
|
||||||
|
self.temp_monitor.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to start temperature monitoring: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
self.temp_controller = None
|
||||||
|
self.temp_monitor = None
|
||||||
|
|
||||||
|
def stop_temperature_monitoring(self):
|
||||||
|
"""
|
||||||
|
Stop the temperature monitoring thread if running.
|
||||||
|
"""
|
||||||
|
if self.temp_monitor is not None:
|
||||||
|
try:
|
||||||
|
self.temp_monitor.stop()
|
||||||
|
print("Temperature monitoring stopped")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error stopping temperature monitoring: {e}")
|
||||||
|
finally:
|
||||||
|
self.temp_monitor = None
|
||||||
|
|
||||||
|
if self.temp_controller is not None:
|
||||||
|
try:
|
||||||
|
self.temp_controller.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing temperature controller: {e}")
|
||||||
|
finally:
|
||||||
|
self.temp_controller = None
|
||||||
|
|
||||||
def documentation_init( self ):
|
def documentation_init( self ):
|
||||||
|
|
||||||
self.doc_urls = {
|
self.doc_urls = {
|
||||||
@@ -1154,7 +1365,7 @@ class ScriptWidgets:
|
|||||||
class import from as return yield while continue break assert None True False AccumulatedValue
|
class import from as return yield while continue break assert None True False AccumulatedValue
|
||||||
Accumulation MeasurementResult ADC_Result Experiment synchronize sleep result data
|
Accumulation MeasurementResult ADC_Result Experiment synchronize sleep result data
|
||||||
issubclass min max abs pow range xrange log_range lin_range staggered_range file
|
issubclass min max abs pow range xrange log_range lin_range staggered_range file
|
||||||
combine_ranges interleaved_range get_sampling_rate uses_statistics write_to_csv write_to_hdf
|
combine_ranges interleaved_range get_sampling_rate uses_statistics write_to_csv write_to_hdf lowpass
|
||||||
get_job_id get_description set_description get_xdata get_ydata set_xdata set_ydata
|
get_job_id get_description set_description get_xdata get_ydata set_xdata set_ydata
|
||||||
ttl_pulse ttls rf_pulse state_start state_end wait record loop_start loop_end set_frequency
|
ttl_pulse ttls rf_pulse state_start state_end wait record loop_start loop_end set_frequency
|
||||||
set_pfg set_dac set_phase set_pts_local get_length set_title get_title add_lineplotdata
|
set_pfg set_dac set_phase set_pts_local get_length set_title get_title add_lineplotdata
|
||||||
@@ -2045,6 +2256,13 @@ class ConfigTab:
|
|||||||
self.config_info_textview = self.xml_gui.get_object( "info_textview" )
|
self.config_info_textview = self.xml_gui.get_object( "info_textview" )
|
||||||
self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" )
|
self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" )
|
||||||
|
|
||||||
|
# Connect keyboard event handlers for Ctrl+S support
|
||||||
|
self.config_info_textview.connect("key-press-event", self.config_keypress)
|
||||||
|
self.config_backend_executable_entry.connect("key-press-event", self.config_keypress)
|
||||||
|
self.config_spool_dir_entry.connect("key-press-event", self.config_keypress)
|
||||||
|
self.config_data_pool_name_entry.connect("key-press-event", self.config_keypress)
|
||||||
|
self.config_data_pool_write_interval_entry.connect("key-press-event", self.config_keypress)
|
||||||
|
|
||||||
# ADC bit depth setting
|
# ADC bit depth setting
|
||||||
self.config_adc_bit_depth_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
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_label = gtk.Label(label="ADC bit depth:")
|
||||||
@@ -2052,6 +2270,7 @@ class ConfigTab:
|
|||||||
self.config_adc_bit_depth_spinbutton.set_value(ADC_Result.default_bit_depth)
|
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.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_spinbutton.connect("value-changed", self.on_adc_bit_depth_changed)
|
||||||
|
self.config_adc_bit_depth_spinbutton.connect("key-press-event", self.config_keypress)
|
||||||
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_label, False, False, 0)
|
||||||
self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_spinbutton, 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")
|
backend_vbox = self.xml_gui.get_object("vbox4")
|
||||||
@@ -2059,6 +2278,75 @@ class ConfigTab:
|
|||||||
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
|
backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0)
|
||||||
self.config_adc_bit_depth_hbox.show_all()
|
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_entry.connect("key-press-event", self.config_keypress)
|
||||||
|
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_spinbutton.connect("key-press-event", self.config_keypress)
|
||||||
|
|
||||||
|
self.config_temp_interval_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||||
|
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0)
|
||||||
|
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_spinbutton, False, False, 0)
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_interval_hbox, False, False, 0)
|
||||||
|
|
||||||
|
# Baud rate (for eurotherm)
|
||||||
|
self.config_temp_baudrate_combobox = gtk.ComboBoxText()
|
||||||
|
self.config_temp_baudrate_combobox.append_text("19200")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("9600")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("38400")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("57600")
|
||||||
|
self.config_temp_baudrate_combobox.append_text("115200")
|
||||||
|
self.config_temp_baudrate_combobox.set_active(0)
|
||||||
|
self.config_temp_baudrate_combobox.set_tooltip_text("Serial baud rate for temperature controller")
|
||||||
|
self.config_temp_baudrate_hbox = gtk.HBox(homogeneous=False, spacing=6)
|
||||||
|
self.config_temp_baudrate_hbox.pack_start(self.config_temp_baudrate_combobox, False, False, 0)
|
||||||
|
self.config_temp_hbox.pack_start(self.config_temp_baudrate_hbox, False, False, 0)
|
||||||
|
# Note: baud rate is shown for eurotherm, hidden for simulated
|
||||||
|
|
||||||
|
# Set initial visibility based on controller type
|
||||||
|
self._update_temp_widgets_visibility()
|
||||||
|
|
||||||
|
if backend_vbox:
|
||||||
|
backend_vbox.pack_start(self.config_temp_hbox, False, False, 0)
|
||||||
|
self.config_temp_hbox.show_all()
|
||||||
|
|
||||||
# insert version informations
|
# insert version informations
|
||||||
components_text = """
|
components_text = """
|
||||||
operating system %(os)s
|
operating system %(os)s
|
||||||
@@ -2165,11 +2453,29 @@ pygobject version %(pygobject)s
|
|||||||
"data_pool_complib": complib,
|
"data_pool_complib": complib,
|
||||||
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
|
"data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ),
|
||||||
"script_font": self.config_script_font_button.get_font_name( ),
|
"script_font": self.config_script_font_button.get_font_name( ),
|
||||||
"adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int()
|
"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"]
|
ADC_Result.default_bit_depth = actual_config["adc_bit_depth"]
|
||||||
return actual_config
|
return actual_config
|
||||||
|
|
||||||
|
def _get_temp_baudrate(self):
|
||||||
|
"""Get selected baud rate from combobox."""
|
||||||
|
if not hasattr(self, 'config_temp_baudrate_combobox'):
|
||||||
|
return 19200
|
||||||
|
active_text = self.config_temp_baudrate_combobox.get_active_text()
|
||||||
|
if active_text:
|
||||||
|
try:
|
||||||
|
return int(active_text)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return 19200
|
||||||
|
|
||||||
def set( self, config ):
|
def set( self, config ):
|
||||||
if "start_backend" in config:
|
if "start_backend" in config:
|
||||||
self.config_start_backend_checkbutton.set_active( config[ "start_backend" ] )
|
self.config_start_backend_checkbutton.set_active( config[ "start_backend" ] )
|
||||||
@@ -2200,6 +2506,41 @@ pygobject version %(pygobject)s
|
|||||||
ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
|
ADC_Result.default_bit_depth = int(config["adc_bit_depth"])
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Temperature monitoring configuration (only if widgets exist)
|
||||||
|
if hasattr(self, 'config_temp_monitoring_checkbutton') and "enable_temperature_monitoring" in config:
|
||||||
|
try:
|
||||||
|
self.config_temp_monitoring_checkbutton.set_active(bool(config["enable_temperature_monitoring"]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_controller_type_combobox') and "temperature_controller_type" in config:
|
||||||
|
try:
|
||||||
|
controller_type = config["temperature_controller_type"]
|
||||||
|
if controller_type in ["eurotherm", "simulated"]:
|
||||||
|
self._set_combo_active_text(self.config_temp_controller_type_combobox, controller_type)
|
||||||
|
# Update widget visibility based on type
|
||||||
|
self._update_temp_widgets_visibility()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_device_entry') and "temperature_device" in config:
|
||||||
|
try:
|
||||||
|
self.config_temp_device_entry.set_text(str(config["temperature_device"]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_baudrate_combobox') and "temperature_baudrate" in config:
|
||||||
|
try:
|
||||||
|
baudrate = str(config["temperature_baudrate"])
|
||||||
|
if baudrate in ["9600", "19200", "38400", "57600", "115200"]:
|
||||||
|
self._set_combo_active_text(self.config_temp_baudrate_combobox, baudrate)
|
||||||
|
else:
|
||||||
|
self.config_temp_baudrate_combobox.set_active(0) # Default to 19200
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if hasattr(self, 'config_temp_interval_spinbutton') and "temperature_interval" in config:
|
||||||
|
try:
|
||||||
|
self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"]))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
self.config_temp_interval_spinbutton.set_value(2.0)
|
||||||
if "data_pool_complib" in config:
|
if "data_pool_complib" in config:
|
||||||
# find combo-box entry and make it active...
|
# find combo-box entry and make it active...
|
||||||
model = self.config_data_pool_complib.get_model( )
|
model = self.config_data_pool_complib.get_model( )
|
||||||
@@ -2216,6 +2557,84 @@ pygobject version %(pygobject)s
|
|||||||
def on_adc_bit_depth_changed(self, widget):
|
def on_adc_bit_depth_changed(self, widget):
|
||||||
ADC_Result.default_bit_depth = widget.get_value_as_int()
|
ADC_Result.default_bit_depth = widget.get_value_as_int()
|
||||||
|
|
||||||
|
def on_temp_device_browse_clicked(self, widget):
|
||||||
|
"""
|
||||||
|
Open file chooser dialog to select temperature controller device.
|
||||||
|
Defaults to /dev/serial/by-id/ which is typically used for USB serial converters.
|
||||||
|
"""
|
||||||
|
dialog = gtk.FileChooserDialog(
|
||||||
|
title="Select Temperature Controller Device",
|
||||||
|
parent=None,
|
||||||
|
action=gtk.FileChooserAction.OPEN,
|
||||||
|
buttons=(
|
||||||
|
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
|
||||||
|
gtk.STOCK_OPEN, gtk.ResponseType.OK
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set default folder to /dev/serial/by-id/ if it exists
|
||||||
|
default_folder = "/dev/serial/by-id/"
|
||||||
|
if os.path.isdir(default_folder):
|
||||||
|
dialog.set_current_folder(default_folder)
|
||||||
|
else:
|
||||||
|
# Fallback to /dev
|
||||||
|
dialog.set_current_folder("/dev/")
|
||||||
|
|
||||||
|
# Only show regular files (device files)
|
||||||
|
dialog.set_select_multiple(False)
|
||||||
|
|
||||||
|
response = dialog.run()
|
||||||
|
if response == gtk.ResponseType.OK:
|
||||||
|
selected_file = dialog.get_filename()
|
||||||
|
self.config_temp_device_entry.set_text(selected_file)
|
||||||
|
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
def on_temp_controller_type_changed(self, widget):
|
||||||
|
"""Update widget visibility when controller type changes."""
|
||||||
|
self._update_temp_widgets_visibility()
|
||||||
|
|
||||||
|
def _update_temp_widgets_visibility(self):
|
||||||
|
"""Show/hide widgets based on selected controller type."""
|
||||||
|
# Defensive: check if combobox exists
|
||||||
|
if not hasattr(self, 'config_temp_controller_type_combobox'):
|
||||||
|
return
|
||||||
|
|
||||||
|
active_type = self.config_temp_controller_type_combobox.get_active_text()
|
||||||
|
if active_type is None:
|
||||||
|
active_type = "eurotherm"
|
||||||
|
|
||||||
|
# Defensive: check if widgets exist before showing/hiding
|
||||||
|
show_device = active_type == "eurotherm"
|
||||||
|
if hasattr(self, 'config_temp_device_hbox'):
|
||||||
|
if show_device:
|
||||||
|
self.config_temp_device_hbox.show()
|
||||||
|
else:
|
||||||
|
self.config_temp_device_hbox.hide()
|
||||||
|
if hasattr(self, 'config_temp_baudrate_hbox'):
|
||||||
|
if show_device:
|
||||||
|
self.config_temp_baudrate_hbox.show()
|
||||||
|
else:
|
||||||
|
self.config_temp_baudrate_hbox.hide()
|
||||||
|
|
||||||
|
def _set_combo_active_text(self, combobox, text):
|
||||||
|
"""
|
||||||
|
Set a ComboBoxText's active entry by text.
|
||||||
|
|
||||||
|
Gtk.ComboBoxText.set_active_text() does not exist in GTK 3.24.x,
|
||||||
|
so we iterate the model to find the matching entry.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
model = combobox.get_model()
|
||||||
|
if model is None:
|
||||||
|
return
|
||||||
|
for i in range(model.iter_n_children(None)):
|
||||||
|
iter = model.iter_nth_child(None, i)
|
||||||
|
if iter and model.get(iter, 0)[0] == text:
|
||||||
|
combobox.set_active(i)
|
||||||
|
return
|
||||||
|
|
||||||
def load_config_handler( self, widget ):
|
def load_config_handler( self, widget ):
|
||||||
if self.system_default_filename:
|
if self.system_default_filename:
|
||||||
self.load_config( self.system_default_filename )
|
self.load_config( self.system_default_filename )
|
||||||
@@ -2224,6 +2643,17 @@ pygobject version %(pygobject)s
|
|||||||
def save_config_handler( self, widget ):
|
def save_config_handler( self, widget ):
|
||||||
self.save_config( )
|
self.save_config( )
|
||||||
|
|
||||||
|
def config_keypress( self, widget, event ):
|
||||||
|
"""
|
||||||
|
Handle keyboard shortcuts in configuration window
|
||||||
|
"""
|
||||||
|
if event.state & gdk.ModifierType.CONTROL_MASK != 0:
|
||||||
|
if event.keyval == gdk.keyval_from_name( "s" ):
|
||||||
|
# Save configuration with Ctrl+S
|
||||||
|
self.save_config( )
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def set_script_font_handler( self, widget ):
|
def set_script_font_handler( self, widget ):
|
||||||
"""
|
"""
|
||||||
handles changes in font name
|
handles changes in font name
|
||||||
@@ -3115,7 +3545,7 @@ class MonitorWidgets:
|
|||||||
if in_result is None:
|
if in_result is None:
|
||||||
self.clear_display( )
|
self.clear_display( )
|
||||||
return
|
return
|
||||||
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ):
|
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
|
||||||
|
|
||||||
xmin = in_result.get_xmin( )
|
xmin = in_result.get_xmin( )
|
||||||
xmax = in_result.get_xmax( )
|
xmax = in_result.get_xmax( )
|
||||||
@@ -3463,7 +3893,6 @@ class ScriptInterface:
|
|||||||
self.data = DataPool( )
|
self.data = DataPool( )
|
||||||
|
|
||||||
def runScripts( self ):
|
def runScripts( self ):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# get script engines
|
# get script engines
|
||||||
self.exp_handling = self.res_handling = None
|
self.exp_handling = self.res_handling = None
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -641,6 +641,21 @@ Public License instead of this License.
|
|||||||
<property name="homogeneous">True</property>
|
<property name="homogeneous">True</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkToolButton" id="toolbar_estimate_button">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">Estimate experiment duration</property>
|
||||||
|
<property name="label">Estimate</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="icon-name">dialog-information</property>
|
||||||
|
<signal name="clicked" handler="on_toolbar_estimate_button_clicked" swapped="no"/>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="homogeneous">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkToolItem" id="toolitem4">
|
<object class="GtkToolItem" id="toolitem4">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -909,7 +924,6 @@ Public License instead of this License.
|
|||||||
<object class="GtkSourceView" id="data_handling_textview">
|
<object class="GtkSourceView" id="data_handling_textview">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<signal name="event" handler="on_data_handling_textview_event" swapped="no"/>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|||||||
@@ -191,6 +191,7 @@
|
|||||||
<keyword>set_pts_local</keyword>
|
<keyword>set_pts_local</keyword>
|
||||||
<keyword>wait</keyword>
|
<keyword>wait</keyword>
|
||||||
<keyword>record</keyword>
|
<keyword>record</keyword>
|
||||||
|
<keyword>lowpass</keyword>
|
||||||
</keywordlist>
|
</keywordlist>
|
||||||
|
|
||||||
<keywordlist style="datatype">
|
<keywordlist style="datatype">
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import re
|
|||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ def checksum(message):
|
|||||||
bcc = (reduce(operator.xor, list(map(ord,message))))
|
bcc = (reduce(operator.xor, list(map(ord,message))))
|
||||||
return chr(bcc)
|
return chr(bcc)
|
||||||
|
|
||||||
class Eurotherm(object):
|
class Eurotherm(TemperatureController):
|
||||||
def __init__(self, serial_device, baudrate = 19200):
|
def __init__(self, serial_device, baudrate = 19200):
|
||||||
self.device = standard_device
|
self.device = standard_device
|
||||||
# timeout: 110 ms to get all answers.
|
# timeout: 110 ms to get all answers.
|
||||||
@@ -100,6 +102,39 @@ class Eurotherm(object):
|
|||||||
def get_setpoint_temperature(self):
|
def get_setpoint_temperature(self):
|
||||||
return self.read_param('SL')
|
return self.read_param('SL')
|
||||||
|
|
||||||
|
# TemperatureController interface implementation
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""Read current temperature in Celsius."""
|
||||||
|
temp = self.read_param('PV')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""Read target setpoint temperature in Celsius."""
|
||||||
|
temp = self.read_param('SL')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""Set target temperature in Celsius."""
|
||||||
|
return self.write_param('SL', str(temperature))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the serial connection."""
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
self.s = None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import time
|
import time
|
||||||
delta=5
|
delta=5
|
||||||
@@ -118,3 +153,6 @@ if __name__ == '__main__':
|
|||||||
f.write(l)
|
f.write(l)
|
||||||
f.flush()
|
f.flush()
|
||||||
f.write('# MARK -- %s --\n'%(time.asctime()))
|
f.write('# MARK -- %s --\n'%(time.asctime()))
|
||||||
|
|
||||||
|
# Register Eurotherm with the factory
|
||||||
|
register_controller('eurotherm', Eurotherm)
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import serial
|
||||||
|
import re
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
reply_pattern = re.compile(r"\x02..(.*)\x03.", re.DOTALL)
|
||||||
|
# example answer '\x02PV279.8\x03/'
|
||||||
|
# [EOT] = \x04
|
||||||
|
# [STX] = \x02
|
||||||
|
# [ENQ] = \x05
|
||||||
|
# [ETX] = \x03
|
||||||
|
# [ACK] = \x06
|
||||||
|
# BCC = checksum
|
||||||
|
|
||||||
|
standard_device = '/dev/ttyUSB0'
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parameter read example:
|
||||||
|
|
||||||
|
Master: [EOT]0011PV[ENQ]
|
||||||
|
Instrument: [STX]PV16.4[ETX]{BCC}
|
||||||
|
|
||||||
|
Writing data:
|
||||||
|
|
||||||
|
Master: [EOT] {GID}{GID}{UID}{UID}[STX]{CHAN}(c1)(c2)<DATA>[ETX](BCC)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Lakeshore(TemperatureController):
|
||||||
|
def __init__(self, serial_device, baudrate=57600):
|
||||||
|
self.device = standard_device
|
||||||
|
# timeout: 110 ms to get all answers.
|
||||||
|
self.s = serial.Serial(serial_device,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=7,
|
||||||
|
parity='O',
|
||||||
|
stopbits=1,
|
||||||
|
timeout=0.4)
|
||||||
|
self._expect_len = 50
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
|
||||||
|
#def get_current_temperature(self): # Temp A
|
||||||
|
# self.s.write("KRDG? a\n")
|
||||||
|
# a = self.s.readline().strip()[1:]
|
||||||
|
# return float(a)
|
||||||
|
|
||||||
|
def get_temperature_A(self): # Temp A
|
||||||
|
self.s.write("KRDG? a\n")
|
||||||
|
a = self.s.readline().strip()[1:]
|
||||||
|
return float(a)
|
||||||
|
|
||||||
|
def get_temperature_B(self): # Temp B
|
||||||
|
self.s.write("KRDG? b\n")
|
||||||
|
b = self.s.readline().strip()[1:]
|
||||||
|
return float(b)
|
||||||
|
|
||||||
|
def set_temperature(self, temperature): # setze Setpoint
|
||||||
|
t = temperature
|
||||||
|
self.s.write(f"SETP 1,{t}\n")
|
||||||
|
|
||||||
|
def set_ramp(self, ramp):
|
||||||
|
return self.s.write(f'RAMP 1, 1, {ramp}\n')
|
||||||
|
|
||||||
|
def get_set_temperature(self): # Setpoint abfrage
|
||||||
|
self.s.write("SETP? 1\n")
|
||||||
|
s = self.s.readline().strip()[1:]
|
||||||
|
return float(s)
|
||||||
|
|
||||||
|
def get_output(self): # Heater Output
|
||||||
|
self.s.write("HTR? 1\n")
|
||||||
|
o = self.s.readline().strip()[1:]
|
||||||
|
return float(o)
|
||||||
|
|
||||||
|
def get_ramp(self):
|
||||||
|
self.s.write('RAMP? 1\n')
|
||||||
|
ret = self.s.readline().strip()
|
||||||
|
onoff, rate = ret.split(',')
|
||||||
|
if onoff:
|
||||||
|
return float(rate)
|
||||||
|
else:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def read_param(self, param):
|
||||||
|
"""Read a parameter using simple ASCII commands."""
|
||||||
|
self.s.write(f"{param}?\n")
|
||||||
|
answer = self.s.readline().strip()
|
||||||
|
if answer and len(answer) > 1:
|
||||||
|
# Remove the first character (command echo) and return the value
|
||||||
|
return answer[1:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_value(self, name):
|
||||||
|
temp = self.read_param(name)
|
||||||
|
if temp is None:
|
||||||
|
return np.nan
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except ValueError:
|
||||||
|
return np.nan
|
||||||
|
|
||||||
|
# TemperatureController interface implementation
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""Read current temperature in Celsius."""
|
||||||
|
temp = self.read_param('KRDG')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""Read target setpoint temperature in Celsius."""
|
||||||
|
temp = self.read_param('SETP')
|
||||||
|
if temp is None:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(temp)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""Set target temperature in Celsius."""
|
||||||
|
self.s.write(f"SETP {temperature}\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the serial connection."""
|
||||||
|
if hasattr(self, 's') and self.s is not None:
|
||||||
|
if self.s.is_open:
|
||||||
|
self.s.close()
|
||||||
|
self.s = None
|
||||||
|
|
||||||
|
|
||||||
|
# Register Lakeshore with the factory
|
||||||
|
register_controller('lakeshore', Lakeshore)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Temperature controller interface and base classes.
|
||||||
|
|
||||||
|
This module provides a consistent interface for temperature controllers
|
||||||
|
to enable pluggable temperature monitoring systems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
# Plugin registry for temperature controllers
|
||||||
|
_controller_registry = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_controller(name: str, controller_class):
|
||||||
|
"""
|
||||||
|
Register a temperature controller implementation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique identifier for this controller type
|
||||||
|
controller_class: Class that implements TemperatureController
|
||||||
|
"""
|
||||||
|
_controller_registry[name] = controller_class
|
||||||
|
|
||||||
|
|
||||||
|
def create_controller(name: str, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a temperature controller instance by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Registered controller type name
|
||||||
|
*args: Positional arguments to pass to controller constructor
|
||||||
|
**kwargs: Keyword arguments to pass to controller constructor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemperatureController: Instance of the requested controller
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If controller name is not registered and cannot be imported
|
||||||
|
"""
|
||||||
|
# If not found, try to auto-import the module
|
||||||
|
if name not in _controller_registry:
|
||||||
|
# Map known controller names to their modules
|
||||||
|
module_map = {
|
||||||
|
'simulated': 'damaris.tools.temperature_simulator',
|
||||||
|
'eurotherm': 'damaris.tools.eurotherm',
|
||||||
|
'lakeshore': 'damaris.tools.lakeshore',
|
||||||
|
}
|
||||||
|
if name in module_map:
|
||||||
|
try:
|
||||||
|
# Import the module which will register the controller
|
||||||
|
import importlib
|
||||||
|
importlib.import_module(module_map[name])
|
||||||
|
except ImportError as e:
|
||||||
|
raise ValueError(f"Failed to import controller '{name}': {e}. Available: {list(_controller_registry.keys())}")
|
||||||
|
|
||||||
|
# Check again after import
|
||||||
|
if name not in _controller_registry:
|
||||||
|
raise ValueError(f"Unknown temperature controller: {name}. Available: {list(_controller_registry.keys())}")
|
||||||
|
|
||||||
|
return _controller_registry[name](*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_controller_names() -> list:
|
||||||
|
"""
|
||||||
|
Get list of available controller names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of registered controller type names
|
||||||
|
"""
|
||||||
|
return list(_controller_registry.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# Pre-register built-in controllers by importing their modules
|
||||||
|
# This ensures they're available without explicit imports
|
||||||
|
try:
|
||||||
|
from . import eurotherm
|
||||||
|
except ImportError as e:
|
||||||
|
# Eurotherm may require serial module which might not be installed
|
||||||
|
# That's OK, it will be imported on-demand if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import lakeshore
|
||||||
|
except ImportError as e:
|
||||||
|
# Lakeshore may require serial module which might not be installed
|
||||||
|
# That's OK, it will be imported on-demand if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import temperature_simulator
|
||||||
|
except ImportError as e:
|
||||||
|
# Simulator should always work (no external dependencies)
|
||||||
|
# But if it fails, it will be imported on-demand
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureController(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for temperature controllers.
|
||||||
|
|
||||||
|
All temperature controller implementations (Eurotherm, Lakeshore, etc.)
|
||||||
|
should inherit from this class and implement the abstract methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Read current temperature in Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Current temperature in degrees Celsius.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IOError: If communication with the device fails.
|
||||||
|
RuntimeError: If the device is not ready or returns invalid data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Read the target setpoint temperature in Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Target temperature in degrees Celsius.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set the target temperature setpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: Target temperature in degrees Celsius.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the setpoint was successfully set, False otherwise.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Clean up resources (e.g., close serial connections).
|
||||||
|
|
||||||
|
This method should be called when the controller is no longer needed.
|
||||||
|
The default implementation does nothing; subclasses should override
|
||||||
|
if they have resources to clean up.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: iso-8859-1 -*-
|
||||||
|
"""
|
||||||
|
Simulated temperature controller for testing.
|
||||||
|
|
||||||
|
Provides a temperature controller that simulates real temperature readings
|
||||||
|
with configurable parameters including sinusoidal variation, noise, and missing values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .temperature import TemperatureController, register_controller
|
||||||
|
|
||||||
|
|
||||||
|
class SimulatedTemperatureController(TemperatureController):
|
||||||
|
"""
|
||||||
|
A simulated temperature controller that generates realistic temperature readings.
|
||||||
|
|
||||||
|
This controller is useful for testing and development without requiring
|
||||||
|
physical hardware. It simulates temperature readings with:
|
||||||
|
- Base temperature around 290K (17C)
|
||||||
|
- Sinusoidal variation with amplitude of 10K
|
||||||
|
- 5% random noise
|
||||||
|
- 10% probability of missing values (returns previous value)
|
||||||
|
- 60 second period for the sine wave
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||||
|
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||||
|
period: Period of sine wave in seconds (default: 60.0)
|
||||||
|
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||||
|
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_temp: float = 290.0, amplitude: float = 10.0,
|
||||||
|
period: float = 60.0, noise_level: float = 0.05,
|
||||||
|
missing_probability: float = 0.10):
|
||||||
|
"""
|
||||||
|
Initialize the simulated temperature controller.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_temp: Base temperature in Kelvin (default: 290.0)
|
||||||
|
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
|
||||||
|
period: Period of sine wave in seconds (default: 60.0)
|
||||||
|
noise_level: Fraction of random noise (default: 0.05 = 5%)
|
||||||
|
missing_probability: Probability of missing value (default: 0.10 = 10%)
|
||||||
|
"""
|
||||||
|
self.base_temp = float(base_temp)
|
||||||
|
self.amplitude = float(amplitude)
|
||||||
|
self.period = float(period)
|
||||||
|
self.noise_level = float(noise_level)
|
||||||
|
self.missing_probability = float(missing_probability)
|
||||||
|
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._setpoint = self.base_temp
|
||||||
|
self._last_temp = self.base_temp
|
||||||
|
self._last_reading_time = self._start_time
|
||||||
|
|
||||||
|
def get_temperature(self) -> float:
|
||||||
|
"""
|
||||||
|
Get simulated current temperature in Kelvin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Simulated temperature in Kelvin
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
elapsed = time.time() - self._start_time
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Calculate ideal sinusoidal temperature
|
||||||
|
temp = self.base_temp + self.amplitude * math.sin(2 * math.pi * elapsed / self.period)
|
||||||
|
|
||||||
|
# Add 5% random noise
|
||||||
|
noise = temp * self.noise_level * (random.random() * 2 - 1) # -5% to +5%
|
||||||
|
temp += noise
|
||||||
|
|
||||||
|
# 10% chance of missing value (return last good value)
|
||||||
|
if random.random() < self.missing_probability:
|
||||||
|
temp = self._last_temp
|
||||||
|
else:
|
||||||
|
self._last_temp = temp
|
||||||
|
self._last_reading_time = current_time
|
||||||
|
|
||||||
|
return temp
|
||||||
|
|
||||||
|
def get_setpoint(self) -> float:
|
||||||
|
"""
|
||||||
|
Get the target setpoint temperature in Kelvin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Target temperature in Kelvin
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
return self._setpoint
|
||||||
|
|
||||||
|
def set_setpoint(self, temperature: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set the target temperature setpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: Target temperature in Kelvin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._setpoint = float(temperature)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Clean up resources."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset the simulation to initial state."""
|
||||||
|
with self._lock:
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._last_temp = self.base_temp
|
||||||
|
self._last_reading_time = self._start_time
|
||||||
|
|
||||||
|
|
||||||
|
# Register the simulated controller with the factory
|
||||||
|
register_controller('simulated', SimulatedTemperatureController)
|
||||||
Reference in New Issue
Block a user