Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dfd564a9b | ||
|
|
0d2d3122e3 | ||
|
|
801e31e84f | ||
|
|
5f558e4f03 | ||
|
|
f562255c75 | ||
|
|
42a4f9e2b9 | ||
|
|
ed5ddde2e3 | ||
|
|
f86dafeda7 | ||
|
|
9e42205a4d | ||
|
|
1f6763f9de | ||
|
|
6668ac5b2b | ||
|
|
ee453c03a7 | ||
|
|
1fb56e20f9 | ||
|
|
ef11824f8b |
No files matched your search
Vendored
+19
@@ -1,3 +1,22 @@
|
|||||||
|
python3-damaris (0.34) stable; urgency=medium
|
||||||
|
|
||||||
|
* internal cleanup: modernize threading idioms (with-block lock handling, time.sleep, super()) without behaviour change
|
||||||
|
|
||||||
|
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Sat, 24 Oct 2026 00:24:10 +0200
|
||||||
|
|
||||||
|
python3-damaris (0.33) stable; urgency=medium
|
||||||
|
|
||||||
|
* restore numpy scalar job XML serialization, broken since v0.28: lin_range and log_range values were written as unparsable np.float64(...)
|
||||||
|
* make temperature proxy host and port configurable in the configuration tab
|
||||||
|
|
||||||
|
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Fri, 23 Oct 2026 11:13:39 +0200
|
||||||
|
|
||||||
|
python3-damaris (0.32) stable; urgency=medium
|
||||||
|
|
||||||
|
* fix removal of plot artists: Axes.lines/collections are read-only ArtistLists since matplotlib 3.5
|
||||||
|
|
||||||
|
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Thu, 22 Oct 2026 16:51:50 +0200
|
||||||
|
|
||||||
python3-damaris (0.31) stable; urgency=medium
|
python3-damaris (0.31) stable; urgency=medium
|
||||||
|
|
||||||
* added phasing slider
|
* added phasing slider
|
||||||
|
|||||||
+3
-2
@@ -51,12 +51,13 @@ Following is a short description of the options:
|
|||||||
10. Filename of the data pool file
|
10. Filename of the data pool file
|
||||||
11. Write interval after which the data pool is rewritten
|
11. Write interval after which the data pool is rewritten
|
||||||
12. Compression settings
|
12. Compression settings
|
||||||
13. Authors, License and version numbers of used components
|
13. Host and port of the temperature proxy service
|
||||||
|
14. Authors, License and version numbers of used components
|
||||||
|
|
||||||
Temperature History
|
Temperature History
|
||||||
===================
|
===================
|
||||||
|
|
||||||
DAMARIS automatically retrieves one temperature history for the complete experiment from the local ``tempproxy`` service at ``http://localhost:8000/v1/readings`` after the experiment finishes. The proxy controls the temperature device; no controller, serial device, or polling interval needs to be configured in DAMARIS. Successful readings are stored under the ``temperature`` DataPool key with elapsed seconds on the x-axis and the proxy's ``pv`` values unchanged on the y-axis, so units depend on the configured controller. If the proxy is unavailable or has no readings for the experiment, the history is omitted and the experiment's other data is still saved.
|
DAMARIS automatically retrieves one temperature history for the complete experiment from the ``tempproxy`` service at ``http://<host>:<port>/v1/readings`` after the experiment finishes. Host and port are configured in the Configuration tab, the defaults are ``127.0.0.1`` and ``18007``. The proxy controls the temperature device; no controller, serial device, or polling interval needs to be configured in DAMARIS. Successful readings are stored under the ``temperature`` DataPool key with elapsed seconds on the x-axis and the proxy's ``pv`` values unchanged on the y-axis, so units depend on the configured controller. If the proxy is unavailable or has no readings for the experiment, the history is omitted and the experiment's other data is still saved.
|
||||||
|
|
||||||
Experiment Script
|
Experiment Script
|
||||||
=================
|
=================
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
## module contents
|
## module contents
|
||||||
#
|
#
|
||||||
__version__ = "0.31"
|
__version__ = "0.34"
|
||||||
__all__=["experiments", "data", "gui"]
|
__all__=["experiments", "data", "gui"]
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class StateSimple(StateBase):
|
|||||||
super(StateSimple, self).__init__()
|
super(StateSimple, self).__init__()
|
||||||
if time < 0:
|
if time < 0:
|
||||||
raise AssertionError("time for state is negative!")
|
raise AssertionError("time for state is negative!")
|
||||||
self.time = time
|
self.time = float(time)
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
def to_xml(self, indent: str = "") -> str:
|
def to_xml(self, indent: str = "") -> str:
|
||||||
@@ -191,7 +191,7 @@ class Experiment:
|
|||||||
|
|
||||||
Typically only used in very special cases.
|
Typically only used in very special cases.
|
||||||
"""
|
"""
|
||||||
self.state_list.append('<state time="%s">\n' % repr(time))
|
self.state_list.append('<state time="%s">\n' % repr(float(time)))
|
||||||
self.total_time[-1] += time
|
self.total_time[-1] += time
|
||||||
|
|
||||||
## End of *state_start*
|
## End of *state_start*
|
||||||
@@ -404,10 +404,10 @@ class Experiment:
|
|||||||
The state length is 2 µs. Stabilisation time depends on RF source.
|
The state length is 2 µs. Stabilisation time depends on RF source.
|
||||||
|
|
||||||
:param float frequency: frequency to set in Hz
|
:param float frequency: frequency to set in Hz
|
||||||
:param float phase: phase to set
|
:param float phase: phase to set, normalized to the range [0, 360)
|
||||||
:param int ttls: default=0, additional ttl lines to set (integer)
|
:param int ttls: default=0, additional ttl lines to set (integer)
|
||||||
"""
|
"""
|
||||||
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase)
|
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase % 360)
|
||||||
if ttls != 0:
|
if ttls != 0:
|
||||||
s_content += '<ttlout value="0x%06x"/>' % ttls
|
s_content += '<ttlout value="0x%06x"/>' % ttls
|
||||||
self.state_list.append(StateSimple(2e-6, s_content))
|
self.state_list.append(StateSimple(2e-6, s_content))
|
||||||
@@ -582,8 +582,9 @@ class Experiment:
|
|||||||
- The state length for this command is 0.5 µs.
|
- The state length for this command is 0.5 µs.
|
||||||
- The stabilization time for the phase change is RF source dependent. A typical value for PTS310 is 2 µs.
|
- The stabilization time for the phase change is RF source dependent. A typical value for PTS310 is 2 µs.
|
||||||
- The phase is relative to the initial phase set at start of the experiment.
|
- The phase is relative to the initial phase set at start of the experiment.
|
||||||
|
- Negative phases are normalized to the range [0, 360) before writing the job file.
|
||||||
"""
|
"""
|
||||||
s_content = '<analogout phase="%f" />' % (phase)
|
s_content = '<analogout phase="%f" />' % (phase % 360)
|
||||||
if ttls!=0:
|
if ttls!=0:
|
||||||
s_content += '<ttlout value="%d"/>' % ttls
|
s_content += '<ttlout value="%d"/>' % ttls
|
||||||
self.state_list.append(StateSimple(0.5e-6, s_content))
|
self.state_list.append(StateSimple(0.5e-6, s_content))
|
||||||
@@ -680,19 +681,19 @@ class Experiment:
|
|||||||
value=""
|
value=""
|
||||||
elif type(value) is float or isinstance(value, numpy.floating):
|
elif type(value) is float or isinstance(value, numpy.floating):
|
||||||
type_string="Float"
|
type_string="Float"
|
||||||
value=repr(value)
|
value=repr(float(value))
|
||||||
elif type(value) is int or isinstance(value, numpy.integer):
|
elif type(value) is int or isinstance(value, numpy.integer):
|
||||||
type_string="Int"
|
type_string="Int"
|
||||||
value=repr(value)
|
value=repr(int(value))
|
||||||
elif type(value) is int:
|
elif type(value) is int:
|
||||||
type_string="Long"
|
type_string="Long"
|
||||||
value=repr(value)
|
value=repr(value)
|
||||||
elif type(value) is complex or isinstance(value, numpy.complexfloating):
|
elif type(value) is complex or isinstance(value, numpy.complexfloating):
|
||||||
type_string="Complex"
|
type_string="Complex"
|
||||||
value=repr(value)
|
value=repr(complex(value))
|
||||||
elif type(value) is bool or isinstance(value, numpy.bool_):
|
elif type(value) is bool or isinstance(value, numpy.bool_):
|
||||||
type_string="Boolean"
|
type_string="Boolean"
|
||||||
value=repr(value)
|
value=repr(bool(value))
|
||||||
elif type(value) in (str,):
|
elif type(value) in (str,):
|
||||||
type_string="String"
|
type_string="String"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ This class handles the backend driver
|
|||||||
class BackendDriver(threading.Thread):
|
class BackendDriver(threading.Thread):
|
||||||
|
|
||||||
def __init__(self, executable, spool, clear_jobs=False, clear_results=False):
|
def __init__(self, executable, spool, clear_jobs=False, clear_results=False):
|
||||||
threading.Thread.__init__(self, name="Backend Driver")
|
super().__init__(name="Backend Driver")
|
||||||
self.core_pid = None
|
self.core_pid = None
|
||||||
self.core_input = None
|
self.core_input = None
|
||||||
self.core_output = None
|
self.core_output = None
|
||||||
|
|||||||
@@ -717,6 +717,8 @@ class DamarisGUI:
|
|||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
self.si = None
|
self.si = None
|
||||||
|
if self.monitor.overflow_job_ids:
|
||||||
|
logger.warning( "ADC overflow occurred in jobs: %s", ', '.join( [str( j ) for j in self.monitor.overflow_job_ids] ))
|
||||||
self.state = DamarisGUI.Edit_State
|
self.state = DamarisGUI.Edit_State
|
||||||
self.sw.enable_editing( )
|
self.sw.enable_editing( )
|
||||||
self.toolbar_run_button.set_sensitive( True )
|
self.toolbar_run_button.set_sensitive( True )
|
||||||
@@ -871,7 +873,10 @@ class DamarisGUI:
|
|||||||
temperature_history_start = self.temperature_history_start
|
temperature_history_start = self.temperature_history_start
|
||||||
self.temperature_history_start = None
|
self.temperature_history_start = None
|
||||||
self.data.pop("temperature", None)
|
self.data.pop("temperature", None)
|
||||||
temperature = TemperatureProxyClient().get_history(
|
proxy_config = self.config.get( )
|
||||||
|
temperature = TemperatureProxyClient(
|
||||||
|
host=proxy_config["tempproxy_host"],
|
||||||
|
port=proxy_config["tempproxy_port"]).get_history(
|
||||||
temperature_history_start, time.time())
|
temperature_history_start, time.time())
|
||||||
if temperature is not None:
|
if temperature is not None:
|
||||||
self.data["temperature"] = temperature
|
self.data["temperature"] = temperature
|
||||||
@@ -920,6 +925,8 @@ class DamarisGUI:
|
|||||||
self.lockfile.del_experiment(self.id)
|
self.lockfile.del_experiment(self.id)
|
||||||
|
|
||||||
# now everything is stopped
|
# now everything is stopped
|
||||||
|
if self.monitor.overflow_job_ids:
|
||||||
|
logger.warning( "ADC overflow occurred in jobs: %s", ', '.join( [str( j ) for j in self.monitor.overflow_job_ids] ))
|
||||||
self.state = DamarisGUI.Edit_State
|
self.state = DamarisGUI.Edit_State
|
||||||
self.sw.enable_editing( )
|
self.sw.enable_editing( )
|
||||||
self.toolbar_run_button.set_sensitive( True )
|
self.toolbar_run_button.set_sensitive( True )
|
||||||
@@ -1259,7 +1266,7 @@ class DamarisGUI:
|
|||||||
|
|
||||||
class start_browser( threading.Thread ):
|
class start_browser( threading.Thread ):
|
||||||
def __init__( self, url ):
|
def __init__( self, url ):
|
||||||
threading.Thread.__init__( self, name="manual browser" )
|
super().__init__(name="manual browser")
|
||||||
self.my_webbrowser = None
|
self.my_webbrowser = None
|
||||||
self.my_webbrowser_process = None
|
self.my_webbrowser_process = None
|
||||||
self.start_url = url
|
self.start_url = url
|
||||||
@@ -2379,6 +2386,27 @@ 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 proxy settings
|
||||||
|
self.config_tempproxy_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
self.config_tempproxy_host_label = gtk.Label(label="Temp proxy host:")
|
||||||
|
self.config_tempproxy_host_entry = gtk.Entry()
|
||||||
|
self.config_tempproxy_host_entry.set_text("127.0.0.1")
|
||||||
|
self.config_tempproxy_host_entry.set_tooltip_text("Host of the tempproxy service reading the temperature controller")
|
||||||
|
self.config_tempproxy_host_entry.connect("key-press-event", self.config_keypress)
|
||||||
|
self.config_tempproxy_port_label = gtk.Label(label="port:")
|
||||||
|
self.config_tempproxy_port_spinbutton = gtk.SpinButton.new_with_range(1, 65535, 1)
|
||||||
|
self.config_tempproxy_port_spinbutton.set_value(18007)
|
||||||
|
self.config_tempproxy_port_spinbutton.set_tooltip_text("Port of the tempproxy service, /v1/readings is requested there")
|
||||||
|
self.config_tempproxy_port_spinbutton.connect("key-press-event", self.config_keypress)
|
||||||
|
self.config_tempproxy_hbox.pack_start(self.config_tempproxy_host_label, False, False, 0)
|
||||||
|
self.config_tempproxy_hbox.pack_start(self.config_tempproxy_host_entry, False, False, 0)
|
||||||
|
self.config_tempproxy_hbox.pack_start(self.config_tempproxy_port_label, False, False, 0)
|
||||||
|
self.config_tempproxy_hbox.pack_start(self.config_tempproxy_port_spinbutton, False, False, 0)
|
||||||
|
frontend_vbox = self.xml_gui.get_object("vbox7")
|
||||||
|
if frontend_vbox:
|
||||||
|
frontend_vbox.pack_start(self.config_tempproxy_hbox, False, False, 0)
|
||||||
|
self.config_tempproxy_hbox.show_all()
|
||||||
|
|
||||||
# insert version informations
|
# insert version informations
|
||||||
components_text = """
|
components_text = """
|
||||||
operating system %(os)s
|
operating system %(os)s
|
||||||
@@ -2485,7 +2513,9 @@ 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(),
|
||||||
|
"tempproxy_host": self.config_tempproxy_host_entry.get_text( ),
|
||||||
|
"tempproxy_port": self.config_tempproxy_port_spinbutton.get_value_as_int( )
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -2520,6 +2550,10 @@ 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
|
||||||
|
if "tempproxy_host" in config:
|
||||||
|
self.config_tempproxy_host_entry.set_text( str( config[ "tempproxy_host" ] ) )
|
||||||
|
if "tempproxy_port" in config:
|
||||||
|
self.config_tempproxy_port_spinbutton.set_value( float( config[ "tempproxy_port" ] ) )
|
||||||
|
|
||||||
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...
|
||||||
@@ -3246,9 +3280,8 @@ class MonitorWidgets:
|
|||||||
self.data_pool.unregister_listener( self.datapool_listener )
|
self.data_pool.unregister_listener( self.datapool_listener )
|
||||||
self.data_pool = None
|
self.data_pool = None
|
||||||
|
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter = 0
|
self.update_counter = 0
|
||||||
self.update_counter_lock.release( )
|
|
||||||
|
|
||||||
# display states
|
# display states
|
||||||
self.source_list_reset( )
|
self.source_list_reset( )
|
||||||
@@ -3300,24 +3333,21 @@ class MonitorWidgets:
|
|||||||
displayed_object = self.displayed_data[ 1 ]
|
displayed_object = self.displayed_data[ 1 ]
|
||||||
object_to_display = self.data_pool.get( event.subject )
|
object_to_display = self.data_pool.get( event.subject )
|
||||||
if displayed_object is None or object_to_display is None:
|
if displayed_object is None or object_to_display is None:
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter += 1
|
self.update_counter += 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
|
glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
else:
|
else:
|
||||||
if event.what == DataPool.Event.updated_value and \
|
if event.what == DataPool.Event.updated_value and \
|
||||||
(
|
(
|
||||||
displayed_object is object_to_display or displayed_object.__class__ is object_to_display.__class__):
|
displayed_object is object_to_display or displayed_object.__class__ is object_to_display.__class__):
|
||||||
# oh, another category
|
# oh, another category
|
||||||
self.update_counter_lock.acquire()
|
with self.update_counter_lock:
|
||||||
self.update_counter += 1
|
self.update_counter += 1
|
||||||
self.update_counter_lock.release()
|
|
||||||
glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
||||||
priority=glib.PRIORITY_DEFAULT_IDLE )
|
priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
else:
|
else:
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter += 1
|
self.update_counter += 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
|
glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
|
|
||||||
if event.what in [ DataPool.Event.updated_value, DataPool.Event.new_key ]:
|
if event.what in [ DataPool.Event.updated_value, DataPool.Event.new_key ]:
|
||||||
@@ -3337,14 +3367,13 @@ class MonitorWidgets:
|
|||||||
if self.update_counter > 5:
|
if self.update_counter > 5:
|
||||||
if debug:
|
if debug:
|
||||||
logger.debug("Sleeping to find time for graphics updates")
|
logger.debug("Sleeping to find time for graphics updates")
|
||||||
threading.Event( ).wait( 0.05 )
|
time.sleep(0.05)
|
||||||
while self.update_counter > 15:
|
while self.update_counter > 15:
|
||||||
threading.Event( ).wait( 0.05 )
|
time.sleep(0.05)
|
||||||
if event.origin is not self.displayed_data[ 1 ]:
|
if event.origin is not self.displayed_data[ 1 ]:
|
||||||
return
|
return
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter += 1
|
self.update_counter += 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
||||||
priority=glib.PRIORITY_DEFAULT_IDLE )
|
priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
|
|
||||||
@@ -3354,9 +3383,8 @@ class MonitorWidgets:
|
|||||||
"""
|
"""
|
||||||
here dictionary changes are done
|
here dictionary changes are done
|
||||||
"""
|
"""
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter -= 1
|
self.update_counter -= 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
|
|
||||||
if event.what == DataPool.Event.updated_value:
|
if event.what == DataPool.Event.updated_value:
|
||||||
if (self.displayed_data[ 0 ] is not None and
|
if (self.displayed_data[ 0 ] is not None and
|
||||||
@@ -3414,9 +3442,8 @@ class MonitorWidgets:
|
|||||||
|
|
||||||
def update_display_idle_event( self, subject=None ):
|
def update_display_idle_event( self, subject=None ):
|
||||||
|
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter -= 1
|
self.update_counter -= 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
# print "update display", self.update_counter
|
# print "update display", self.update_counter
|
||||||
if self.update_counter > 10:
|
if self.update_counter > 10:
|
||||||
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
|
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
|
||||||
@@ -3461,9 +3488,8 @@ class MonitorWidgets:
|
|||||||
self.display_source_path_label.set_label( "" )
|
self.display_source_path_label.set_label( "" )
|
||||||
self.clear_display( )
|
self.clear_display( )
|
||||||
# renew display via idle event
|
# renew display via idle event
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter += 1
|
self.update_counter += 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
event = DataPool.Event( DataPool.Event.updated_value, new_data_name )
|
event = DataPool.Event( DataPool.Event.updated_value, new_data_name )
|
||||||
glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
|
glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
|
|
||||||
@@ -3480,9 +3506,8 @@ class MonitorWidgets:
|
|||||||
and source.get_number_of_channels( ) == 2:
|
and source.get_number_of_channels( ) == 2:
|
||||||
# coalesce like the other display updates, dragging the slider
|
# coalesce like the other display updates, dragging the slider
|
||||||
# emits one event per pointer motion
|
# emits one event per pointer motion
|
||||||
self.update_counter_lock.acquire( )
|
with self.update_counter_lock:
|
||||||
self.update_counter += 1
|
self.update_counter += 1
|
||||||
self.update_counter_lock.release( )
|
|
||||||
glib.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
glib.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
||||||
priority=glib.PRIORITY_DEFAULT_IDLE )
|
priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
|
|
||||||
@@ -3849,7 +3874,7 @@ class MonitorWidgets:
|
|||||||
self.measurementresultline = None
|
self.measurementresultline = None
|
||||||
elif self.measurementresultline is not None:
|
elif self.measurementresultline is not None:
|
||||||
# clear line plot
|
# clear line plot
|
||||||
self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] )
|
self.measurementresultline[ 0 ].remove( )
|
||||||
self.measurementresultline = None
|
self.measurementresultline = None
|
||||||
if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None:
|
if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None:
|
||||||
self.matplot_cursor.disconnect_events()
|
self.matplot_cursor.disconnect_events()
|
||||||
@@ -4146,17 +4171,17 @@ class MonitorWidgets:
|
|||||||
|
|
||||||
# remove lines and error bars
|
# remove lines and error bars
|
||||||
if self.measurementresultgraph is not None:
|
if self.measurementresultgraph is not None:
|
||||||
self.matplot_axes.lines.remove( self.measurementresultgraph[ 0 ] )
|
self.measurementresultgraph[ 0 ].remove( )
|
||||||
# remove caps
|
# remove caps
|
||||||
for l in self.measurementresultgraph[ 1 ]:
|
for l in self.measurementresultgraph[ 1 ]:
|
||||||
self.matplot_axes.lines.remove( l )
|
l.remove( )
|
||||||
# and columns
|
# and columns
|
||||||
for l in self.measurementresultgraph[ 2 ]:
|
for l in self.measurementresultgraph[ 2 ]:
|
||||||
self.matplot_axes.collections.remove( l )
|
l.remove( )
|
||||||
self.measurementresultgraph = None
|
self.measurementresultgraph = None
|
||||||
|
|
||||||
if self.measurementresultline is not None:
|
if self.measurementresultline is not None:
|
||||||
self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] )
|
self.measurementresultline[ 0 ].remove( )
|
||||||
self.measurementresultline = None
|
self.measurementresultline = None
|
||||||
|
|
||||||
# remove fit overlay lines
|
# remove fit overlay lines
|
||||||
@@ -4244,13 +4269,13 @@ class MonitorWidgets:
|
|||||||
|
|
||||||
# finally decide about x log plot
|
# finally decide about x log plot
|
||||||
if x_scale == "log10" and xmin > 0:
|
if x_scale == "log10" and xmin > 0:
|
||||||
self.matplot_axes.set_xscale( "log", basex=10.0 )
|
self.matplot_axes.set_xscale( "log", base=10.0 )
|
||||||
self.matplot_axes.fmt_xdata = lambda x: "%g" % x
|
self.matplot_axes.fmt_xdata = lambda x: "%g" % x
|
||||||
#elif x_scale=="log" and xmin>0:
|
#elif x_scale=="log" and xmin>0:
|
||||||
# e scaling implementation not really useful
|
# e scaling implementation not really useful
|
||||||
# self.matplot_axes.set_xscale("log", basex=numpy.e)
|
# self.matplot_axes.set_xscale("log", basex=numpy.e)
|
||||||
if y_scale == "log10" and ymin > 0:
|
if y_scale == "log10" and ymin > 0:
|
||||||
self.matplot_axes.set_yscale( "log", basex=10.0 )
|
self.matplot_axes.set_yscale( "log", base=10.0 )
|
||||||
self.matplot_axes.fmt_ydata = lambda x: "%g" % x
|
self.matplot_axes.fmt_ydata = lambda x: "%g" % x
|
||||||
|
|
||||||
self.__rescale = False
|
self.__rescale = False
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class ExperimentHandling(threading.Thread):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, script, exp_writer, data, log_callback=None):
|
def __init__(self, script, exp_writer, data, log_callback=None):
|
||||||
threading.Thread.__init__(self, name="experiment handler")
|
super().__init__(name="experiment handler")
|
||||||
self.script=script
|
self.script=script
|
||||||
self.writer=exp_writer
|
self.writer=exp_writer
|
||||||
self.data=data
|
self.data=data
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class ResultHandling(threading.Thread):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, script_data, result_iterator, data_pool, log_callback=None):
|
def __init__(self, script_data, result_iterator, data_pool, log_callback=None):
|
||||||
threading.Thread.__init__(self,name="result handler")
|
super().__init__(name="result handler")
|
||||||
self.script=script_data
|
self.script=script_data
|
||||||
self.results=result_iterator
|
self.results=result_iterator
|
||||||
self.data_space=data_pool
|
self.data_space=data_pool
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ class ResultReader:
|
|||||||
elif t == "Complex":
|
elif t == "Complex":
|
||||||
self.result_description[k]=complex(v)
|
self.result_description[k]=complex(v)
|
||||||
elif t == "Boolean":
|
elif t == "Boolean":
|
||||||
self.result_description[k]=bool(v)
|
# bool("False") would be True, compare the text instead
|
||||||
|
self.result_description[k]=(v == "True")
|
||||||
elif t == "String":
|
elif t == "String":
|
||||||
self.result_description[k]=v
|
self.result_description[k]=v
|
||||||
else:
|
else:
|
||||||
@@ -502,7 +503,8 @@ class ResultReader:
|
|||||||
elif t == "Complex":
|
elif t == "Complex":
|
||||||
self.result_description[k]=complex(v)
|
self.result_description[k]=complex(v)
|
||||||
elif t == "Boolean":
|
elif t == "Boolean":
|
||||||
self.result_description[k]=bool(v)
|
# bool("False") would be True, compare the text instead
|
||||||
|
self.result_description[k]=(v == "True")
|
||||||
elif t == "String":
|
elif t == "String":
|
||||||
self.result_description[k]=v
|
self.result_description[k]=v
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Read experiment temperature history from the local tempproxy service.
|
"""Read experiment temperature history from the tempproxy service.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
client = TemperatureProxyClient()
|
client = TemperatureProxyClient() # defaults to http://127.0.0.1:18007
|
||||||
temperature = client.get_history(start_epoch, end_epoch)
|
temperature = client.get_history(start_epoch, end_epoch)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -18,11 +18,18 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class TemperatureProxyClient:
|
class TemperatureProxyClient:
|
||||||
"""Retrieve experiment-bounded readings from ``localhost:8000``."""
|
"""Retrieve experiment-bounded readings from a configurable tempproxy host/port."""
|
||||||
|
|
||||||
URL = "http://localhost:8000/v1/readings"
|
|
||||||
TIMEOUT = 5
|
TIMEOUT = 5
|
||||||
|
|
||||||
|
def __init__(self, host: str = "127.0.0.1", port: int = 18007):
|
||||||
|
"""Create a client for ``http://<host>:<port>/v1/readings``.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
client = TemperatureProxyClient(host="192.168.1.5", port=18007)
|
||||||
|
"""
|
||||||
|
self.url = f"http://{host}:{port}/v1/readings"
|
||||||
|
|
||||||
def get_history(self, start_epoch: float, end_epoch: float) -> TemperatureResult | None:
|
def get_history(self, start_epoch: float, end_epoch: float) -> TemperatureResult | None:
|
||||||
"""Return temperature history relative to the experiment start, or ``None``.
|
"""Return temperature history relative to the experiment start, or ``None``.
|
||||||
|
|
||||||
@@ -33,7 +40,7 @@ class TemperatureProxyClient:
|
|||||||
temperature = TemperatureProxyClient().get_history(1000.0, 1060.0)
|
temperature = TemperatureProxyClient().get_history(1000.0, 1060.0)
|
||||||
"""
|
"""
|
||||||
query = urlencode({"start": start_epoch, "end": end_epoch})
|
query = urlencode({"start": start_epoch, "end": end_epoch})
|
||||||
url = f"{self.URL}?{query}"
|
url = f"{self.url}?{query}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with urlopen(url, timeout=self.TIMEOUT) as response:
|
with urlopen(url, timeout=self.TIMEOUT) as response:
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from damaris.experiments.Experiment import Experiment
|
||||||
|
|
||||||
|
|
||||||
|
def phase_of_state(state):
|
||||||
|
"extracts the phase value from a state's XML content"
|
||||||
|
match = re.search(r'phase="([-0-9.]+)"', state.content)
|
||||||
|
assert match, "no phase in state: %s" % state.content
|
||||||
|
return float(match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
class TestExperimentPhase(unittest.TestCase):
|
||||||
|
def test_set_phase_negative_normalized(self):
|
||||||
|
"""Test that set_phase() normalizes negative phases to [0, 360)."""
|
||||||
|
e = Experiment()
|
||||||
|
for degrees, expected in ((-90, 270.0), (-45.5, 314.5),
|
||||||
|
(-270, 90.0), (-360, 0.0)):
|
||||||
|
e.set_phase(degrees)
|
||||||
|
self.assertEqual(phase_of_state(e.state_list.list[-1]), expected)
|
||||||
|
|
||||||
|
def test_set_phase_positive_unchanged(self):
|
||||||
|
"""Test that set_phase() passes positive phases through unchanged."""
|
||||||
|
e = Experiment()
|
||||||
|
for degrees, expected in ((0, 0.0), (90, 90.0), (270, 270.0),
|
||||||
|
(45.5, 45.5)):
|
||||||
|
e.set_phase(degrees)
|
||||||
|
self.assertEqual(phase_of_state(e.state_list.list[-1]), expected)
|
||||||
|
|
||||||
|
def test_set_frequency_negative_phase_normalized(self):
|
||||||
|
"""Test that set_frequency() normalizes a negative phase."""
|
||||||
|
e = Experiment()
|
||||||
|
e.set_frequency(85e6, -90)
|
||||||
|
state = e.state_list.list[-1]
|
||||||
|
self.assertEqual(phase_of_state(state), 270.0)
|
||||||
|
self.assertIn('f="85000000.000000"', state.content)
|
||||||
|
|
||||||
|
def test_rf_pulse_negative_phase_normalized(self):
|
||||||
|
"""Test that rf_pulse() normalizes a negative pulse phase."""
|
||||||
|
e = Experiment(gating=1e-6, rf_sources=[1], rf_gates=[2])
|
||||||
|
e.rf_pulse(length=1e-6, phase=-90, source=0)
|
||||||
|
self.assertEqual(phase_of_state(e.state_list.list[0]), 270.0)
|
||||||
|
|
||||||
|
def test_numpy_scalars_in_description(self):
|
||||||
|
"""Test that description items with numpy scalars (e.g. from lin_range)
|
||||||
|
are written as plain numbers so the result script can parse them again."""
|
||||||
|
import numpy as np
|
||||||
|
from damaris.tools.ranges import lin_range
|
||||||
|
e = Experiment()
|
||||||
|
e.set_description("tau", lin_range(1e-6, 3e-6, 1e-6)[0])
|
||||||
|
e.set_description("n", np.int64(42))
|
||||||
|
xml = e.write_xml_string()
|
||||||
|
items = {key: (typ, text)
|
||||||
|
for key, typ, text in
|
||||||
|
re.findall(r'<item key="([^"]+)" type="([^"]+)">([^<]*)</item>', xml)}
|
||||||
|
self.assertEqual(items["tau"][0], "Float")
|
||||||
|
self.assertEqual(items["n"][0], "Int")
|
||||||
|
self.assertEqual(float(items["tau"][1]), 1e-6)
|
||||||
|
self.assertEqual(int(items["n"][1]), 42)
|
||||||
|
|
||||||
|
def test_numpy_scalar_state_times(self):
|
||||||
|
"""Test that numpy scalar times do not end up as np.float64(...) in the XML."""
|
||||||
|
import numpy as np
|
||||||
|
e = Experiment()
|
||||||
|
e.wait(np.float64(1e-6))
|
||||||
|
e.state_start(np.float64(2e-6))
|
||||||
|
xml = e.write_xml_string()
|
||||||
|
self.assertNotIn("np.float64", xml)
|
||||||
|
self.assertIn('<state time="1e-06"/>', xml)
|
||||||
|
self.assertIn('<state time="2e-06">', xml)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -22,7 +22,7 @@ class TestTemperatureProxyClient(unittest.TestCase):
|
|||||||
return io.BytesIO(json.dumps(payload).encode("utf-8"))
|
return io.BytesIO(json.dumps(payload).encode("utf-8"))
|
||||||
|
|
||||||
@patch("damaris.gui.TemperatureProxyClient.urlopen")
|
@patch("damaris.gui.TemperatureProxyClient.urlopen")
|
||||||
def test_get_history_uses_fixed_endpoint_and_converts_readings(self, mock_urlopen):
|
def test_get_history_uses_default_endpoint_and_converts_readings(self, mock_urlopen):
|
||||||
mock_urlopen.return_value = self.response({
|
mock_urlopen.return_value = self.response({
|
||||||
"readings": [
|
"readings": [
|
||||||
{"t": 101.5, "pv": 293.15},
|
{"t": 101.5, "pv": 293.15},
|
||||||
@@ -35,13 +35,25 @@ class TestTemperatureProxyClient(unittest.TestCase):
|
|||||||
args, kwargs = mock_urlopen.call_args
|
args, kwargs = mock_urlopen.call_args
|
||||||
parsed_url = urlparse(args[0])
|
parsed_url = urlparse(args[0])
|
||||||
self.assertEqual(parsed_url.scheme, "http")
|
self.assertEqual(parsed_url.scheme, "http")
|
||||||
self.assertEqual(parsed_url.netloc, "localhost:8000")
|
self.assertEqual(parsed_url.netloc, "127.0.0.1:18007")
|
||||||
self.assertEqual(parsed_url.path, "/v1/readings")
|
self.assertEqual(parsed_url.path, "/v1/readings")
|
||||||
self.assertEqual(parse_qs(parsed_url.query), {"start": ["100.0"], "end": ["110.0"]})
|
self.assertEqual(parse_qs(parsed_url.query), {"start": ["100.0"], "end": ["110.0"]})
|
||||||
self.assertEqual(kwargs, {"timeout": TemperatureProxyClient.TIMEOUT})
|
self.assertEqual(kwargs, {"timeout": TemperatureProxyClient.TIMEOUT})
|
||||||
self.assertEqual(result.x, [1.5, 4.0])
|
self.assertEqual(result.x, [1.5, 4.0])
|
||||||
self.assertEqual(result.y, [293.15, 294.25])
|
self.assertEqual(result.y, [293.15, 294.25])
|
||||||
|
|
||||||
|
@patch("damaris.gui.TemperatureProxyClient.urlopen")
|
||||||
|
def test_custom_host_and_port_are_used(self, mock_urlopen):
|
||||||
|
mock_urlopen.return_value = self.response({"readings": [{"t": 101.0, "pv": 293.0}]})
|
||||||
|
|
||||||
|
client = TemperatureProxyClient(host="proxy.example.org", port=9000)
|
||||||
|
client.get_history(100.0, 110.0)
|
||||||
|
|
||||||
|
args, _ = mock_urlopen.call_args
|
||||||
|
parsed_url = urlparse(args[0])
|
||||||
|
self.assertEqual(parsed_url.netloc, "proxy.example.org:9000")
|
||||||
|
self.assertEqual(parsed_url.path, "/v1/readings")
|
||||||
|
|
||||||
@patch("damaris.gui.TemperatureProxyClient.urlopen")
|
@patch("damaris.gui.TemperatureProxyClient.urlopen")
|
||||||
def test_empty_readings_return_none(self, mock_urlopen):
|
def test_empty_readings_return_none(self, mock_urlopen):
|
||||||
mock_urlopen.return_value = self.response({"readings": []})
|
mock_urlopen.return_value = self.response({"readings": []})
|
||||||
@@ -62,7 +74,7 @@ class TestTemperatureProxyClient(unittest.TestCase):
|
|||||||
OSError("connection refused"),
|
OSError("connection refused"),
|
||||||
TimeoutError("request timed out"),
|
TimeoutError("request timed out"),
|
||||||
http.client.BadStatusLine("not an HTTP response"),
|
http.client.BadStatusLine("not an HTTP response"),
|
||||||
HTTPError(TemperatureProxyClient.URL, 500, "server error", {}, None),
|
HTTPError(self.client.url, 500, "server error", {}, None),
|
||||||
)
|
)
|
||||||
for failure in failures:
|
for failure in failures:
|
||||||
with self.subTest(failure=failure):
|
with self.subTest(failure=failure):
|
||||||
|
|||||||
Reference in new issue
Block a user