Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
801e31e84f | ||
|
|
5f558e4f03 | ||
|
|
f562255c75 | ||
|
|
42a4f9e2b9 | ||
|
|
ed5ddde2e3 | ||
|
|
959f17ee33 | ||
|
|
63da240504 | ||
|
|
9d60eeabf6 | ||
|
|
12d7323851 | ||
|
|
f86dafeda7 | ||
|
|
9e42205a4d | ||
|
|
1f6763f9de | ||
|
|
6668ac5b2b | ||
|
|
ee453c03a7 | ||
|
|
1fb56e20f9 | ||
|
|
ef11824f8b |
No files matched your search
Vendored
+20
@@ -1,3 +1,23 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
* added phasing slider
|
||||||
|
* added persistent ADC overflow indicator
|
||||||
|
|
||||||
|
-- Markus Rosenstihl <markus.rosenstihl@pkm.tu-darmstadt.de> Thu, 22 Oct 2026 01:00:32 +0200
|
||||||
|
|
||||||
python3-damaris (0.30) stable; urgency=medium
|
python3-damaris (0.30) stable; urgency=medium
|
||||||
|
|
||||||
* replaced temperature with REST source the data from a temperature proxy
|
* replaced temperature with REST source the data from a temperature proxy
|
||||||
|
|||||||
+16
-3
@@ -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
|
||||||
=================
|
=================
|
||||||
@@ -105,8 +106,20 @@ There are now two possibilities to export the figure:
|
|||||||
1. Saving the plot as a figure (EPS, PNG, JPEG and more) by pressing the save button in the toolbar below the plot area
|
1. Saving the plot as a figure (EPS, PNG, JPEG and more) by pressing the save button in the toolbar below the plot area
|
||||||
2. Saving the data of the plot as CSV file (comma separated values) by pressing the button on the right side of the toolbar under the plot
|
2. Saving the data of the plot as CSV file (comma separated values) by pressing the button on the right side of the toolbar under the plot
|
||||||
|
|
||||||
The plot can be navigated directly with the mouse: dragging with the left mouse button pans the view and the mouse wheel zooms in and out around the pointer position. Holding the right mouse button and dragging horizontally zooms only the x axis, dragging vertically zooms only the y axis (dragging right or up zooms in), just like the right button in the toolbar pan mode. This works on linear and logarithmic axes. The pan and zoom buttons in the toolbar below the plot are still available; while one of these toolbar modes is active, the direct mouse panning and right-button zooming are suspended so the toolbar behavior is unchanged. Note that with the autoscaling option enabled, the view is reset to the full data range whenever a new result arrives.
|
The plot can be navigated directly with the mouse: dragging with the left mouse button pans the view and the mouse wheel zooms in and out around the pointer position. Holding the right mouse button and dragging horizontally zooms only the x axis, dragging vertically zooms only the y axis (dragging right or up zooms in), just like the right button in the toolbar pan mode. This works on linear and logarithmic axes. The pan and zoom buttons in the toolbar below the plot are still available; while one of these toolbar modes is active, the direct mouse panning and right-button zooming are suspended so the toolbar behavior is unchanged. The crosshair button and the Pan button in the toolbar below the plot are mutually exclusive; activating one automatically deactivates the other. Note that with the autoscaling option enabled, the view is reset to the full data range whenever a new result arrives.
|
||||||
|
|
||||||
|
A horizontal slider (0-360°) with a "Phase:" label and a value read-out is available in the "Display" tab, located to the right of the "Source:" drop-down selector. This slider allows for a display-only interactive rotation of the displayed ADC_Result or Accumulation two-channel signal for visual phase correction. The rotation applies to all displayed scans and updates for the selected source.
|
||||||
|
|
||||||
|
This operation is display-only and is not written to HDF; it does not modify the result script data or affect CSV exports. The phase is reset to 0 when a new experiment or data pool is loaded. It is inert for data with a single channel or non-signal sources (e.g., TemperatureResult). Note that error bars and statistics are deliberately not rotated, so with the statistics option enabled and a non-zero phase, the error envelope will no longer bracket the rotated curve. For 90°, 180°, and 270° steps, the exact fast path in the phase calculation is used to avoid rounding noise.
|
||||||
|
|
||||||
|
The same rotation is available programmatically via the ``phase()`` method of the ``accu`` (Accumulation) and ``timesignal`` (ADC_Result) objects, which returns a rotated copy of the data without modifying the original object.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: Example of programmatic phase rotation
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
rotated = accu.phase(90) # returns a rotated copy, accu is unchanged
|
||||||
|
data["Rotated"] = rotated
|
||||||
|
|
||||||
Help Menu
|
Help Menu
|
||||||
=========
|
=========
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
## module contents
|
## module contents
|
||||||
#
|
#
|
||||||
__version__ = "0.30"
|
__version__ = "0.33"
|
||||||
__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:
|
||||||
|
|||||||
+197
-22
@@ -253,6 +253,7 @@ class DamarisGUI:
|
|||||||
"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_display_copy_png_button_clicked": self.monitor.copy_canvas_as_png,
|
"on_display_copy_png_button_clicked": self.monitor.copy_canvas_as_png,
|
||||||
"on_display_copy_svg_button_clicked": self.monitor.copy_canvas_as_svg,
|
"on_display_copy_svg_button_clicked": self.monitor.copy_canvas_as_svg,
|
||||||
|
"on_statusbar_overflow_label_clicked": self.monitor.clear_overflow_indicator,
|
||||||
"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
|
||||||
@@ -524,6 +525,8 @@ class DamarisGUI:
|
|||||||
logger.error("Experiment is still running or cleaning up!")
|
logger.error("Experiment is still running or cleaning up!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.monitor.clear_overflow_indicator( )
|
||||||
|
|
||||||
# get config values:
|
# get config values:
|
||||||
config_start = time.time()
|
config_start = time.time()
|
||||||
actual_config = self.config.get( )
|
actual_config = self.config.get( )
|
||||||
@@ -868,7 +871,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
|
||||||
@@ -2376,6 +2382,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
|
||||||
@@ -2482,7 +2509,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
|
||||||
@@ -2517,6 +2546,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...
|
||||||
@@ -2959,13 +2992,27 @@ class MonitorWidgets:
|
|||||||
self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" )
|
self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" )
|
||||||
self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" )
|
self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" )
|
||||||
|
|
||||||
self.display_descriptions_checkbutton = gtk.CheckButton(label="Show Desc.")
|
self.display_descriptions_checkbutton = gtk.CheckButton(label="Descr.")
|
||||||
self.display_descriptions_checkbutton.set_active(True)
|
self.display_descriptions_checkbutton.set_active(True)
|
||||||
self.display_descriptions_checkbutton.show()
|
self.display_descriptions_checkbutton.show()
|
||||||
self.display_settings_frame.set_property("n-columns", 5)
|
self.display_settings_frame.set_property("n-columns", 5)
|
||||||
self.display_settings_frame.attach(self.display_descriptions_checkbutton, 4, 5, 0, 1, gtk.AttachOptions.FILL, 0, 0, 0)
|
self.display_settings_frame.attach(self.display_descriptions_checkbutton, 4, 5, 0, 1, gtk.AttachOptions.FILL, 0, 0, 0)
|
||||||
self.display_descriptions_checkbutton.connect("toggled", self.display_descriptions_toggled)
|
self.display_descriptions_checkbutton.connect("toggled", self.display_descriptions_toggled)
|
||||||
|
|
||||||
|
# ADC overflow indicator: "ADC" button, green = OK, red = overflow,
|
||||||
|
# right click opens a menu to reset the state
|
||||||
|
css = gtk.CssProvider( )
|
||||||
|
css.load_from_data( b".adc_ok { background-image: none; background-color: #4caf50; }"
|
||||||
|
b".adc_error { background-image: none; background-color: #e53935; }" )
|
||||||
|
gtk.StyleContext.add_provider_for_screen( gdk.Screen.get_default( ), css, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION )
|
||||||
|
self.overflow_indicator_button = gtk.Button( label="ADC" )
|
||||||
|
self.overflow_indicator_button.set_tooltip_text( "ADC overflow indicator, red if an overflow occurred" )
|
||||||
|
self.overflow_job_ids = [ ]
|
||||||
|
self.overflow_indicator_button.connect( "button-press-event", self.overflow_indicator_button_press )
|
||||||
|
self.display_settings_frame.attach(self.overflow_indicator_button, 4, 5, 1, 2, gtk.AttachOptions.FILL, 0, 0, 0)
|
||||||
|
self.overflow_indicator_button.show( )
|
||||||
|
self.set_overflow_indicator( False )
|
||||||
|
|
||||||
# Matplot (Display_Table, 1st Row) --------------------------------------------------------
|
# Matplot (Display_Table, 1st Row) --------------------------------------------------------
|
||||||
|
|
||||||
# create new plot
|
# create new plot
|
||||||
@@ -2997,6 +3044,9 @@ class MonitorWidgets:
|
|||||||
#self.matplot_toolbar.DeleteToolByPos(1)
|
#self.matplot_toolbar.DeleteToolByPos(1)
|
||||||
#self.matplot_toolbar.remove(self.matplot_toolbar.get_nth_item(3))
|
#self.matplot_toolbar.remove(self.matplot_toolbar.get_nth_item(3))
|
||||||
self.matplot_toolbar.remove(self.matplot_toolbar.get_nth_item(6))
|
self.matplot_toolbar.remove(self.matplot_toolbar.get_nth_item(6))
|
||||||
|
# crosshair and Pan button are mutually exclusive
|
||||||
|
self.matplot_pan_button = self.matplot_toolbar._gtk_ids['Pan']
|
||||||
|
self.matplot_pan_button.connect( 'toggled', self.toggle_pan_button )
|
||||||
# now let's add a button to the toolbar
|
# now let's add a button to the toolbar
|
||||||
self.matplot_cursor_button = gtk.ToggleButton(label='')
|
self.matplot_cursor_button = gtk.ToggleButton(label='')
|
||||||
cursor_icon = GdkPixbuf.Pixbuf.new_from_xpm_data( crosshair_xpm )
|
cursor_icon = GdkPixbuf.Pixbuf.new_from_xpm_data( crosshair_xpm )
|
||||||
@@ -3040,6 +3090,11 @@ class MonitorWidgets:
|
|||||||
self.display_source_combobox.add_attribute( display_source_cell, 'text', 0 )
|
self.display_source_combobox.add_attribute( display_source_cell, 'text', 0 )
|
||||||
# GTK3 fix: Prevent blank space from child tree nodes in popup
|
# GTK3 fix: Prevent blank space from child tree nodes in popup
|
||||||
self.display_source_combobox.set_wrap_width(1)
|
self.display_source_combobox.set_wrap_width(1)
|
||||||
|
# interactive display phase rotation: display only, never stored in HDF
|
||||||
|
self.display_phase_scale = self.xml_gui.get_object( "display_phase_scale" )
|
||||||
|
self.display_phase_value_label = self.xml_gui.get_object( "display_phase_value_label" )
|
||||||
|
self.display_phase = 0
|
||||||
|
self.display_phase_scale.connect( "value-changed", self.display_phase_changed )
|
||||||
self.source_list_reset( )
|
self.source_list_reset( )
|
||||||
self.display_source_path_label = self.xml_gui.get_object( "display_source_path_label" )
|
self.display_source_path_label = self.xml_gui.get_object( "display_source_path_label" )
|
||||||
|
|
||||||
@@ -3069,9 +3124,11 @@ class MonitorWidgets:
|
|||||||
self.fitresultlines = [ ]
|
self.fitresultlines = [ ]
|
||||||
self.fit_source_keys = set( )
|
self.fit_source_keys = set( )
|
||||||
self.clipping_marker = None
|
self.clipping_marker = None
|
||||||
|
self.statusbar_overflow_label = self.xml_gui.get_object( "statusbar_overflow_label" )
|
||||||
self.source_list_cache = {}
|
self.source_list_cache = {}
|
||||||
|
|
||||||
def source_list_reset( self ):
|
def source_list_reset( self ):
|
||||||
|
self.display_phase_scale.set_value( 0 )
|
||||||
self.display_source_combobox.set_model(None)
|
self.display_source_combobox.set_model(None)
|
||||||
try:
|
try:
|
||||||
# OPTIMIZATION: Replacing the TreeStore is much faster than clear() for large trees
|
# OPTIMIZATION: Replacing the TreeStore is much faster than clear() for large trees
|
||||||
@@ -3440,6 +3497,25 @@ class MonitorWidgets:
|
|||||||
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 )
|
||||||
|
|
||||||
|
def display_phase_changed( self, widget, data=None ):
|
||||||
|
"""
|
||||||
|
set the display phase in degrees (0-360) for the plotted signal
|
||||||
|
|
||||||
|
Example: monitor.display_phase_scale.set_value(90)
|
||||||
|
"""
|
||||||
|
self.display_phase = widget.get_value( )
|
||||||
|
self.display_phase_value_label.set_label( "%.0f°" % self.display_phase )
|
||||||
|
source = self.displayed_data[ 1 ]
|
||||||
|
if source is not None and isinstance( source, (Accumulation, ADC_Result) ) \
|
||||||
|
and source.get_number_of_channels( ) == 2:
|
||||||
|
# coalesce like the other display updates, dragging the slider
|
||||||
|
# emits one event per pointer motion
|
||||||
|
self.update_counter_lock.acquire( )
|
||||||
|
self.update_counter += 1
|
||||||
|
self.update_counter_lock.release( )
|
||||||
|
glib.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
|
||||||
|
priority=glib.PRIORITY_DEFAULT_IDLE )
|
||||||
|
|
||||||
def display_autoscaling_toggled( self, widget, data=None ):
|
def display_autoscaling_toggled( self, widget, data=None ):
|
||||||
self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) )
|
self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) )
|
||||||
if self.displayed_data[ 0 ] is not None:
|
if self.displayed_data[ 0 ] is not None:
|
||||||
@@ -3619,6 +3695,11 @@ class MonitorWidgets:
|
|||||||
self._pan_start = None
|
self._pan_start = None
|
||||||
self._zoom_drag = None
|
self._zoom_drag = None
|
||||||
|
|
||||||
|
def toggle_pan_button(self, button):
|
||||||
|
"""Deactivate the crosshair button when the Pan mode is activated."""
|
||||||
|
if button.get_active( ) and self.matplot_cursor_button.get_active( ):
|
||||||
|
self.matplot_cursor_button.set_active( False )
|
||||||
|
|
||||||
def toggle_cursor(self, widget):
|
def toggle_cursor(self, widget):
|
||||||
"""Toggle matplotlib crosshair cursor"""
|
"""Toggle matplotlib crosshair cursor"""
|
||||||
if widget.get_active():
|
if widget.get_active():
|
||||||
@@ -3627,7 +3708,9 @@ class MonitorWidgets:
|
|||||||
self.matplot_cursor = CrosshairCursor(self.matplot_axes,
|
self.matplot_cursor = CrosshairCursor(self.matplot_axes,
|
||||||
color='red', linewidth=1)
|
color='red', linewidth=1)
|
||||||
self.matplot_canvas.draw()
|
self.matplot_canvas.draw()
|
||||||
# TODO: Deactivate the pane button
|
# leave the Pan mode, the two buttons are mutually exclusive
|
||||||
|
if self.matplot_pan_button.get_active( ):
|
||||||
|
self.matplot_pan_button.set_active( False )
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Deactivate cursor
|
# Deactivate cursor
|
||||||
@@ -3709,6 +3792,61 @@ class MonitorWidgets:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error copying figure to clipboard: %s", e)
|
logger.error("Error copying figure to clipboard: %s", e)
|
||||||
|
|
||||||
|
def set_overflow_indicator( self, clipped ):
|
||||||
|
"""
|
||||||
|
Set the background color of the ADC indicator button: green if
|
||||||
|
clipped is False, red if clipped is True.
|
||||||
|
|
||||||
|
Example: monitor.set_overflow_indicator(True)
|
||||||
|
"""
|
||||||
|
style = self.overflow_indicator_button.get_style_context( )
|
||||||
|
style.remove_class( "adc_ok" )
|
||||||
|
style.remove_class( "adc_error" )
|
||||||
|
style.add_class( "adc_error" if clipped else "adc_ok" )
|
||||||
|
|
||||||
|
def overflow_indicator_button_press( self, widget, event ):
|
||||||
|
"""
|
||||||
|
ADC indicator button press handler: left click shows a dialog with
|
||||||
|
the list of job ids where an ADC overflow occurred, right click
|
||||||
|
opens a menu to reset the overflow state.
|
||||||
|
|
||||||
|
Example: monitor.overflow_indicator_button_press(button, event)
|
||||||
|
"""
|
||||||
|
if event.button == 1:
|
||||||
|
if self.overflow_job_ids:
|
||||||
|
text = "\n".join( str( job_id ) for job_id in self.overflow_job_ids )
|
||||||
|
else:
|
||||||
|
text = "No ADC overflow recorded."
|
||||||
|
dialog = gtk.MessageDialog(parent=self.main_window,
|
||||||
|
type=gtk.MessageType.WARNING,
|
||||||
|
flags=gtk.DialogFlags.MODAL,
|
||||||
|
buttons=gtk.ButtonsType.OK)
|
||||||
|
dialog.set_markup("ADC OVERFLOW in jobs:")
|
||||||
|
dialog.format_secondary_text( text )
|
||||||
|
dialog.run( )
|
||||||
|
dialog.destroy( )
|
||||||
|
elif event.button == 3:
|
||||||
|
menu = gtk.Menu( )
|
||||||
|
item = gtk.MenuItem( label="Reset overflow state" )
|
||||||
|
item.connect("activate", self.clear_overflow_indicator)
|
||||||
|
menu.append( item )
|
||||||
|
menu.show_all( )
|
||||||
|
menu.popup_at_pointer( event )
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_overflow_indicator( self, widget=None, event=None ):
|
||||||
|
"""
|
||||||
|
Reset handler for the ADC overflow warning: sets the indicator back to
|
||||||
|
green and clears the status bar warning, without starting a new
|
||||||
|
experiment.
|
||||||
|
|
||||||
|
Example: monitor.clear_overflow_indicator()
|
||||||
|
"""
|
||||||
|
self.statusbar_overflow_label.set_text( "" )
|
||||||
|
self.set_overflow_indicator( False )
|
||||||
|
self.overflow_job_ids = [ ]
|
||||||
|
return False
|
||||||
|
|
||||||
##################### functions to feed display
|
##################### functions to feed display
|
||||||
def clear_display( self ):
|
def clear_display( self ):
|
||||||
"""
|
"""
|
||||||
@@ -3741,7 +3879,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()
|
||||||
@@ -3807,11 +3945,41 @@ class MonitorWidgets:
|
|||||||
return
|
return
|
||||||
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
|
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
|
||||||
|
|
||||||
xmin = in_result.get_xmin( )
|
plot_result = in_result
|
||||||
xmax = in_result.get_xmax( )
|
if self.display_phase and isinstance( in_result, (Accumulation, ADC_Result) ) \
|
||||||
|
and in_result.get_number_of_channels( ) == 2:
|
||||||
|
# display only: plot a rotated copy, the pool object itself and
|
||||||
|
# any HDF storage keep the unrotated data. The rotation reads
|
||||||
|
# the raw channel arrays, so take the snapshot while holding the
|
||||||
|
# lock that the result scripts use to update them in place.
|
||||||
|
in_result.lock.acquire( )
|
||||||
|
try:
|
||||||
|
plot_result = in_result.phase( self.display_phase )
|
||||||
|
finally:
|
||||||
|
in_result.lock.release( )
|
||||||
|
if isinstance( plot_result, Accumulation ):
|
||||||
|
# the copy's y_square is not rotated, so its own error
|
||||||
|
# calculation would produce nonsense limits
|
||||||
|
plot_result.use_error = False
|
||||||
|
|
||||||
ymin = in_result.get_ymin( )
|
# error bars are read from the pool object, i.e. they are not
|
||||||
ymax = in_result.get_ymax( )
|
# phase rotated, and the rotated copy would derive them from
|
||||||
|
# mismatched y_square anyway
|
||||||
|
show_errors = (self.display_statistics_checkbutton.get_active( ) and
|
||||||
|
in_result.uses_statistics( ) and in_result.ready_for_drawing_error( ))
|
||||||
|
|
||||||
|
xmin = plot_result.get_xmin( )
|
||||||
|
xmax = plot_result.get_xmax( )
|
||||||
|
|
||||||
|
ymin = plot_result.get_ymin( )
|
||||||
|
ymax = plot_result.get_ymax( )
|
||||||
|
if plot_result is not in_result and show_errors:
|
||||||
|
# the plot draws the unrotated pool errors on the rotated data,
|
||||||
|
# so the axis limits must use that same combination
|
||||||
|
for i in range( plot_result.get_number_of_channels( ) ):
|
||||||
|
err = in_result.get_yerr( i )
|
||||||
|
ymin = min( ymin, (plot_result.get_ydata( i ) - err).min( ) )
|
||||||
|
ymax = max( ymax, (plot_result.get_ydata( i ) + err).max( ) )
|
||||||
|
|
||||||
# Initial rescaling needed?
|
# Initial rescaling needed?
|
||||||
if self.__rescale:
|
if self.__rescale:
|
||||||
@@ -3822,7 +3990,7 @@ class MonitorWidgets:
|
|||||||
self.matplot_axes.set_xscale( "linear" )
|
self.matplot_axes.set_xscale( "linear" )
|
||||||
self.matplot_axes.set_xlim(xmin, xmax)
|
self.matplot_axes.set_xlim(xmin, xmax)
|
||||||
elif x_scale == "log10":
|
elif x_scale == "log10":
|
||||||
xminpos = in_result.get_xminpos( )
|
xminpos = plot_result.get_xminpos( )
|
||||||
self.matplot_axes.set_xscale("log", base=10, nonpositive="mask")
|
self.matplot_axes.set_xscale("log", base=10, nonpositive="mask")
|
||||||
self.matplot_axes.set_xlim(xminpos, xmax)
|
self.matplot_axes.set_xlim(xminpos, xmax)
|
||||||
elif x_scale == "ppm":
|
elif x_scale == "ppm":
|
||||||
@@ -3836,7 +4004,7 @@ class MonitorWidgets:
|
|||||||
self.matplot_axes.set_yscale( "linear" )
|
self.matplot_axes.set_yscale( "linear" )
|
||||||
self.matplot_axes.set_ylim(ymin, ymax)
|
self.matplot_axes.set_ylim(ymin, ymax)
|
||||||
elif y_scale == "log10":
|
elif y_scale == "log10":
|
||||||
yminpos = in_result.get_yminpos()
|
yminpos = plot_result.get_yminpos()
|
||||||
self.matplot_axes.set_yscale("log", base=10, nonpositive="mask")
|
self.matplot_axes.set_yscale("log", base=10, nonpositive="mask")
|
||||||
self.matplot_axes.set_ylim( yminpos, ymax )
|
self.matplot_axes.set_ylim( yminpos, ymax )
|
||||||
self.__rescale = False
|
self.__rescale = False
|
||||||
@@ -3876,14 +4044,14 @@ class MonitorWidgets:
|
|||||||
ylim_max > ymax + 0.2 * ydiff or ylim_min < ymin - 0.2 * ydiff):
|
ylim_max > ymax + 0.2 * ydiff or ylim_min < ymin - 0.2 * ydiff):
|
||||||
self.matplot_axes.set_ylim(ymin, ymax)
|
self.matplot_axes.set_ylim(ymin, ymax)
|
||||||
|
|
||||||
xdata = in_result.get_xdata( )
|
xdata = plot_result.get_xdata( )
|
||||||
chans = in_result.get_number_of_channels( )
|
chans = plot_result.get_number_of_channels( )
|
||||||
data = [ ]
|
data = [ ]
|
||||||
colors = [ (0, 0, 0.8, 1), (0.7, 0, 0, 1), (0, 0.7, 0, 1), (0.7, 0.5, 0, 1),
|
colors = [ (0, 0, 0.8, 1), (0.7, 0, 0, 1), (0, 0.7, 0, 1), (0.7, 0.5, 0, 1),
|
||||||
(0, 0, 0, 1) ] # rgba tuples: blue, red, green, yellow
|
(0, 0, 0, 1) ] # rgba tuples: blue, red, green, yellow
|
||||||
|
|
||||||
for i in range( chans ):
|
for i in range( chans ):
|
||||||
data.append( in_result.get_ydata( i ) )
|
data.append( plot_result.get_ydata( i ) )
|
||||||
|
|
||||||
moving_average = data_slice = None
|
moving_average = data_slice = None
|
||||||
if max_points_to_display > 0 and len( xdata ) > max_points_to_display:
|
if max_points_to_display > 0 and len( xdata ) > max_points_to_display:
|
||||||
@@ -3912,8 +4080,7 @@ class MonitorWidgets:
|
|||||||
self.graphen[ i ].set_data( xdata, data[ i ] )
|
self.graphen[ i ].set_data( xdata, data[ i ] )
|
||||||
|
|
||||||
# Statistics activated?
|
# Statistics activated?
|
||||||
if (self.display_statistics_checkbutton.get_active( ) and
|
if show_errors:
|
||||||
in_result.uses_statistics( ) and in_result.ready_for_drawing_error( )):
|
|
||||||
for i in range( chans ):
|
for i in range( chans ):
|
||||||
err = in_result.get_yerr( i )
|
err = in_result.get_yerr( i )
|
||||||
if moving_average is not None:
|
if moving_average is not None:
|
||||||
@@ -3986,6 +4153,14 @@ class MonitorWidgets:
|
|||||||
color="red", fontsize=20, fontweight="bold",
|
color="red", fontsize=20, fontweight="bold",
|
||||||
ha="center", va="top",
|
ha="center", va="top",
|
||||||
bbox=dict(facecolor='white', alpha=0.7, edgecolor='red'))
|
bbox=dict(facecolor='white', alpha=0.7, edgecolor='red'))
|
||||||
|
# the warning is only justified for results with a valid job_id;
|
||||||
|
# results without job id (e.g. backend errors) only get the plot marker
|
||||||
|
if getattr(in_result, "job_id", None) is not None:
|
||||||
|
self.set_overflow_indicator( True )
|
||||||
|
if in_result.job_id not in self.overflow_job_ids:
|
||||||
|
self.overflow_job_ids.append( in_result.job_id )
|
||||||
|
self.statusbar_overflow_label.set_markup( '<span foreground="red" weight="bold">ADC OVERFLOW</span>' )
|
||||||
|
logger.warning( "ADC overflow detected in job %s", in_result.job_id )
|
||||||
elif self.clipping_marker is not None:
|
elif self.clipping_marker is not None:
|
||||||
try:
|
try:
|
||||||
self.clipping_marker.remove()
|
self.clipping_marker.remove()
|
||||||
@@ -4001,17 +4176,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
|
||||||
@@ -4099,13 +4274,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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
<property name="step-increment">1</property>
|
<property name="step-increment">1</property>
|
||||||
<property name="page-increment">1</property>
|
<property name="page-increment">1</property>
|
||||||
</object>
|
</object>
|
||||||
|
<object class="GtkAdjustment" id="display_phase_adjustment">
|
||||||
|
<property name="upper">360</property>
|
||||||
|
<property name="value">0</property>
|
||||||
|
<property name="step-increment">1</property>
|
||||||
|
<property name="page-increment">10</property>
|
||||||
|
</object>
|
||||||
<object class="GtkMenu" id="documentation_menu">
|
<object class="GtkMenu" id="documentation_menu">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
@@ -1096,6 +1102,47 @@ Public License instead of this License.
|
|||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="display_phase_label">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes"> Phase:</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScale" id="display_phase_scale">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">True</property>
|
||||||
|
<property name="width-request">150</property>
|
||||||
|
<property name="draw-value">False</property>
|
||||||
|
<property name="digits">0</property>
|
||||||
|
<property name="round-digits">0</property>
|
||||||
|
<property name="adjustment">display_phase_adjustment</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="display_phase_value_label">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes">0°</property>
|
||||||
|
<property name="width-chars">4</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">4</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="y-options"/>
|
<property name="y-options"/>
|
||||||
@@ -1191,10 +1238,11 @@ Public License instead of this License.
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkCheckButton" id="display_autoscaling_checkbutton">
|
<object class="GtkCheckButton" id="display_autoscaling_checkbutton">
|
||||||
<property name="label" translatable="yes">Auto-Scaling</property>
|
<property name="label" translatable="yes">Autoscale</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
<property name="tooltip-text" translatable="yes">Rescale graph if new ymax is larger than the old one or shrinked more than 20%</property>
|
<property name="tooltip-text" translatable="yes">Rescale graph if new ymax is larger than the old one or shrinked more than 20%</property>
|
||||||
<property name="use-underline">True</property>
|
<property name="use-underline">True</property>
|
||||||
<property name="draw-indicator">True</property>
|
<property name="draw-indicator">True</property>
|
||||||
@@ -1211,10 +1259,11 @@ Public License instead of this License.
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkCheckButton" id="display_statistics_checkbutton">
|
<object class="GtkCheckButton" id="display_statistics_checkbutton">
|
||||||
<property name="label" translatable="yes">Show Statistics</property>
|
<property name="label" translatable="yes">Statistics</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="receives-default">False</property>
|
<property name="receives-default">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
<property name="tooltip-text" translatable="yes">Plot error-bars if result to plot uses statistic</property>
|
<property name="tooltip-text" translatable="yes">Plot error-bars if result to plot uses statistic</property>
|
||||||
<property name="use-underline">True</property>
|
<property name="use-underline">True</property>
|
||||||
<property name="draw-indicator">True</property>
|
<property name="draw-indicator">True</property>
|
||||||
@@ -2343,7 +2392,7 @@ Public License instead of this License.
|
|||||||
<object class="GtkTable" id="statusbar_table">
|
<object class="GtkTable" id="statusbar_table">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="n-columns">8</property>
|
<property name="n-columns">9</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVSeparator" id="vseparator1">
|
<object class="GtkVSeparator" id="vseparator1">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -2430,6 +2479,29 @@ Public License instead of this License.
|
|||||||
<property name="y-options"/>
|
<property name="y-options"/>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEventBox" id="statusbar_overflow_eventbox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="above-child">True</property>
|
||||||
|
<signal name="button-press-event" handler="on_statusbar_overflow_label_clicked" swapped="no"/>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="statusbar_overflow_label">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="label" translatable="yes"/>
|
||||||
|
<property name="use-markup">True</property>
|
||||||
|
<property name="tooltip-text" translatable="yes">ADC overflow detected. Click to acknowledge.</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left-attach">7</property>
|
||||||
|
<property name="right-attach">8</property>
|
||||||
|
<property name="x-options"/>
|
||||||
|
<property name="y-options"/>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label33">
|
<object class="GtkLabel" id="label33">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -2438,8 +2510,8 @@ Public License instead of this License.
|
|||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left-attach">7</property>
|
<property name="left-attach">8</property>
|
||||||
<property name="right-attach">8</property>
|
<property name="right-attach">9</property>
|
||||||
<property name="y-options"/>
|
<property name="y-options"/>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,6 @@ def result():
|
|||||||
accu = Accumulation(error=False)
|
accu = Accumulation(error=False)
|
||||||
|
|
||||||
for num,ts in enumerate(results):
|
for num,ts in enumerate(results):
|
||||||
data["accu"] = accu
|
|
||||||
data["data/ts %i"%num] = ts+0
|
data["data/ts %i"%num] = ts+0
|
||||||
ts.write_to_hdf(h5, "/", f"adc_{num}", f"adc_{num}")
|
ts.write_to_hdf(h5, "/", f"adc_{num}", f"adc_{num}")
|
||||||
data["fft"] = (ts+0).fft()
|
data["fft"] = (ts+0).fft()
|
||||||
@@ -32,6 +31,7 @@ def result():
|
|||||||
data["test/fit"] = fit
|
data["test/fit"] = fit
|
||||||
data["test"] = r
|
data["test"] = r
|
||||||
accu+=ts
|
accu+=ts
|
||||||
|
data["accu"] = accu+0
|
||||||
print(accu)
|
print(accu)
|
||||||
data["accu_fft"] = accu.fft().autophase()
|
data["accu_fft"] = accu.fft().autophase()
|
||||||
accu.write_to_hdf(h5, "/", "name", "title")
|
accu.write_to_hdf(h5, "/", "name", "title")
|
||||||
|
|||||||
@@ -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