Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7164531351 | ||
|
|
1bed381639 | ||
|
|
0135f9b7a3 | ||
|
|
0135504de2 | ||
|
|
d539a27423 | ||
|
|
34eb7b2728 |
No files matched your search
@@ -100,6 +100,8 @@ 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
|
||||
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.
|
||||
|
||||
|
||||
Help Menu
|
||||
=========
|
||||
|
||||
@@ -13,7 +13,7 @@ class StateSimple(StateBase):
|
||||
super(StateSimple, self).__init__()
|
||||
if time < 0:
|
||||
raise AssertionError("time for state is negative!")
|
||||
self.time = float(time)
|
||||
self.time = time
|
||||
self.content = content
|
||||
|
||||
def to_xml(self, indent: str = "") -> str:
|
||||
@@ -191,7 +191,7 @@ class Experiment:
|
||||
|
||||
Typically only used in very special cases.
|
||||
"""
|
||||
self.state_list.append('<state time="%s">\n' % repr(float(time)))
|
||||
self.state_list.append('<state time="%s">\n' % repr(time))
|
||||
self.total_time[-1] += time
|
||||
|
||||
## End of *state_start*
|
||||
@@ -404,10 +404,10 @@ class Experiment:
|
||||
The state length is 2 µs. Stabilisation time depends on RF source.
|
||||
|
||||
:param float frequency: frequency to set in Hz
|
||||
:param float phase: phase to set, normalized to the range [0, 360)
|
||||
:param float phase: phase to set
|
||||
:param int ttls: default=0, additional ttl lines to set (integer)
|
||||
"""
|
||||
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase % 360)
|
||||
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase)
|
||||
if ttls != 0:
|
||||
s_content += '<ttlout value="0x%06x"/>' % ttls
|
||||
self.state_list.append(StateSimple(2e-6, s_content))
|
||||
@@ -582,9 +582,8 @@ class Experiment:
|
||||
- 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 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 % 360)
|
||||
s_content = '<analogout phase="%f" />' % (phase)
|
||||
if ttls!=0:
|
||||
s_content += '<ttlout value="%d"/>' % ttls
|
||||
self.state_list.append(StateSimple(0.5e-6, s_content))
|
||||
@@ -681,19 +680,19 @@ class Experiment:
|
||||
value=""
|
||||
elif type(value) is float or isinstance(value, numpy.floating):
|
||||
type_string="Float"
|
||||
value=repr(float(value))
|
||||
value=repr(value)
|
||||
elif type(value) is int or isinstance(value, numpy.integer):
|
||||
type_string="Int"
|
||||
value=repr(int(value))
|
||||
value=repr(value)
|
||||
elif type(value) is int:
|
||||
type_string="Long"
|
||||
value=repr(value)
|
||||
elif type(value) is complex or isinstance(value, numpy.complexfloating):
|
||||
type_string="Complex"
|
||||
value=repr(complex(value))
|
||||
value=repr(value)
|
||||
elif type(value) is bool or isinstance(value, numpy.bool_):
|
||||
type_string="Boolean"
|
||||
value=repr(bool(value))
|
||||
value=repr(value)
|
||||
elif type(value) in (str,):
|
||||
type_string="String"
|
||||
else:
|
||||
|
||||
+157
-76
@@ -253,6 +253,8 @@ class DamarisGUI:
|
||||
"on_display_y_scaling_combobox_changed": self.monitor.display_scaling_changed,
|
||||
"on_display_save_data_as_text_button_clicked": self.monitor.save_display_data_as_text,
|
||||
"on_display_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_svg_button_clicked": self.monitor.copy_canvas_as_svg,
|
||||
"on_experiment_script_textview_event": self.sw.textviews_modified,
|
||||
"on_main_notebook_switch_page": self.nopevent,
|
||||
"main_window_close": self.nopevent
|
||||
@@ -333,6 +335,9 @@ class DamarisGUI:
|
||||
"""
|
||||
expecting quit event for main application
|
||||
"""
|
||||
if self.state == DamarisGUI.Quit_State:
|
||||
# quit already handled, e.g. window closed twice
|
||||
return True
|
||||
if self.state in [ DamarisGUI.Edit_State, DamarisGUI.Quit_State ]:
|
||||
self.state = DamarisGUI.Quit_State
|
||||
# do a cleanup...
|
||||
@@ -3024,6 +3029,18 @@ class BlitSafeCanvas( FigureCanvas ):
|
||||
screen. Painting the region right away keeps every regular paint a
|
||||
complete one and stays a partial update.
|
||||
"""
|
||||
def _update_device_pixel_ratio( self, *args, **kwargs ):
|
||||
"""Render at logical resolution, ignoring the HiDPI scale factor.
|
||||
|
||||
The stock canvas renders one buffer pixel per physical pixel,
|
||||
four times the pixel count on a scaled screen, which makes the
|
||||
interactive plot sluggish. Capping the ratio at 1 renders a
|
||||
quarter of the pixels; the window system scales the output up.
|
||||
"""
|
||||
if self._set_device_pixel_ratio( 1 ):
|
||||
self.queue_resize( )
|
||||
self.queue_draw( )
|
||||
|
||||
def blit( self, bbox=None ):
|
||||
if bbox is None:
|
||||
bbox = self.figure.bbox
|
||||
@@ -3255,9 +3272,6 @@ class MonitorWidgets:
|
||||
# add figure to canvas
|
||||
self.matplot_canvas = BlitSafeCanvas( self.matplot_figure )
|
||||
|
||||
# add ctrl + right-click context menu to canvas
|
||||
self.matplot_canvas.connect("button-press-event", self.on_canvas_button_press)
|
||||
|
||||
self.display_table = self.xml_gui.get_object( "display_table" )
|
||||
self.display_table.attach( self.matplot_canvas, 0, 6, 0, 1, gtk.AttachOptions.EXPAND | gtk.AttachOptions.FILL, gtk.AttachOptions.EXPAND | gtk.AttachOptions.FILL, 0, 0 )
|
||||
self.matplot_canvas.show( )
|
||||
@@ -3292,6 +3306,16 @@ class MonitorWidgets:
|
||||
self.display_table.attach( self.matplot_toolbar, 0, 1, 1, 2, gtk.AttachOptions.FILL | gtk.AttachOptions.EXPAND, 0, 0, 0 )
|
||||
self.matplot_toolbar.show( )
|
||||
|
||||
# mouse pan (left button drag), single-axis zoom (middle button
|
||||
# drag) and wheel zoom, active whenever no toolbar pan/zoom
|
||||
# mode is on
|
||||
self._pan_start = None
|
||||
self._zoom_drag = None
|
||||
self.matplot_canvas.mpl_connect( "scroll_event", self.plot_wheel_zoom )
|
||||
self.matplot_canvas.mpl_connect( "button_press_event", self.plot_button_press )
|
||||
self.matplot_canvas.mpl_connect( "motion_notify_event", self.plot_button_motion )
|
||||
self.matplot_canvas.mpl_connect( "button_release_event", self.plot_button_release )
|
||||
|
||||
# select display source
|
||||
self.display_source_combobox = self.xml_gui.get_object( "display_source_combobox" )
|
||||
self.display_source_treestore = gtk.TreeStore( gobject.TYPE_STRING )
|
||||
@@ -3728,7 +3752,7 @@ class MonitorWidgets:
|
||||
# nothing to save
|
||||
return
|
||||
if not hasattr( data_to_save[ 1 ], "write_to_csv" ):
|
||||
log( "do not know how to save %s of class/type %s" % (data_to_save[ 0 ], type( data_to_save[ 1 ] )) )
|
||||
logger.warning( "do not know how to save %s of class/type %s" % (data_to_save[ 0 ], type( data_to_save[ 1 ] )) )
|
||||
return
|
||||
|
||||
# save them to a temporary file (in memory)
|
||||
@@ -3788,6 +3812,98 @@ class MonitorWidgets:
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _zoomed_limits( limits, center, factor, logarithmic ):
|
||||
"""Return the limits zoomed by factor around center.
|
||||
|
||||
On logarithmic axes the scaling happens in log space.
|
||||
"""
|
||||
if logarithmic:
|
||||
limits = numpy.log10( limits )
|
||||
center = numpy.log10( center )
|
||||
new_limits = [ center + ( limit - center ) * factor for limit in limits ]
|
||||
if logarithmic:
|
||||
new_limits = [ 10 ** limit for limit in new_limits ]
|
||||
return tuple( new_limits )
|
||||
|
||||
@staticmethod
|
||||
def _panned_limits( limits, grabbed, current, logarithmic ):
|
||||
"""Return the limits shifted so that current moves to grabbed.
|
||||
|
||||
On logarithmic axes the shift happens in log space.
|
||||
"""
|
||||
if logarithmic:
|
||||
shift = 10 ** ( numpy.log10( grabbed ) - numpy.log10( current ) )
|
||||
return tuple( limit * shift for limit in limits )
|
||||
shift = grabbed - current
|
||||
return tuple( limit + shift for limit in limits )
|
||||
|
||||
def plot_wheel_zoom( self, event ):
|
||||
"""Zoom the plot around the pointer position with the mouse wheel."""
|
||||
if event.inaxes is not self.matplot_axes or event.xdata is None or event.ydata is None:
|
||||
return
|
||||
factor = 1.0 / 1.5 if event.step > 0 else 1.5
|
||||
axes = self.matplot_axes
|
||||
axes.set_xlim( self._zoomed_limits( axes.get_xlim( ), event.xdata, factor,
|
||||
axes.get_xscale( ) == "log" ) )
|
||||
axes.set_ylim( self._zoomed_limits( axes.get_ylim( ), event.ydata, factor,
|
||||
axes.get_yscale( ) == "log" ) )
|
||||
self.matplot_canvas.draw_idle( )
|
||||
|
||||
def plot_button_press( self, event ):
|
||||
"""Start panning the plot with a left button drag.
|
||||
|
||||
A right button drag starts zooming single axes, like the right
|
||||
button in the toolbar pan mode: horizontal motion zooms the x
|
||||
axis, vertical motion the y axis. Both are inactive while a
|
||||
toolbar pan/zoom mode is on, so the toolbar behavior stays
|
||||
unchanged.
|
||||
"""
|
||||
if event.inaxes is not self.matplot_axes or event.xdata is None or event.ydata is None:
|
||||
return
|
||||
if event.button == 3 and not self.matplot_toolbar.mode:
|
||||
self._zoom_drag = ( event.x, event.y, event.xdata, event.ydata )
|
||||
elif event.button == 1 and not self.matplot_toolbar.mode:
|
||||
self._pan_start = ( event.xdata, event.ydata )
|
||||
|
||||
def plot_button_motion( self, event ):
|
||||
"""Pan the plot so the grabbed data point follows the pointer.
|
||||
|
||||
With the right button held, zoom single axes around the
|
||||
pressed position instead: horizontal motion zooms the x axis,
|
||||
vertical motion the y axis, dragging right or up zooms in.
|
||||
"""
|
||||
if self._zoom_drag is not None and event.x is not None and event.y is not None:
|
||||
x0, y0, xdata0, ydata0 = self._zoom_drag
|
||||
axes = self.matplot_axes
|
||||
dx = event.x - x0
|
||||
dy = event.y - y0
|
||||
if dx:
|
||||
factor = 10 ** ( -dx / axes.bbox.width )
|
||||
axes.set_xlim( self._zoomed_limits( axes.get_xlim( ), xdata0, factor,
|
||||
axes.get_xscale( ) == "log" ) )
|
||||
if dy:
|
||||
factor = 10 ** ( -dy / axes.bbox.height )
|
||||
axes.set_ylim( self._zoomed_limits( axes.get_ylim( ), ydata0, factor,
|
||||
axes.get_yscale( ) == "log" ) )
|
||||
self.matplot_canvas.draw_idle( )
|
||||
return
|
||||
if self._pan_start is None or event.xdata is None or event.ydata is None:
|
||||
return
|
||||
x0, y0 = self._pan_start
|
||||
axes = self.matplot_axes
|
||||
axes.set_xlim( self._panned_limits( axes.get_xlim( ), x0, event.xdata,
|
||||
axes.get_xscale( ) == "log" ) )
|
||||
axes.set_ylim( self._panned_limits( axes.get_ylim( ), y0, event.ydata,
|
||||
axes.get_yscale( ) == "log" ) )
|
||||
self._pan_start = ( event.xdata, event.ydata )
|
||||
self.matplot_canvas.draw_idle( )
|
||||
|
||||
def plot_button_release( self, event ):
|
||||
"""Stop panning or single-axis zooming the plot."""
|
||||
self._pan_start = None
|
||||
self._zoom_drag = None
|
||||
|
||||
def toggle_cursor(self, widget):
|
||||
"""Toggle matplotlib crosshair cursor"""
|
||||
if widget.get_active():
|
||||
@@ -3810,40 +3926,6 @@ class MonitorWidgets:
|
||||
self.matplot_cursor = None
|
||||
self.matplot_canvas.draw()
|
||||
|
||||
def on_canvas_button_press(self, widget, event):
|
||||
"""Handle button press events on matplotlib canvas"""
|
||||
if event.button == 2: # middle click
|
||||
self.show_canvas_context_menu(event)
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_canvas_context_menu(self, event):
|
||||
"""Show context menu for matplotlib canvas"""
|
||||
menu = gtk.Menu()
|
||||
|
||||
# toggle cursor
|
||||
toggle_cursor = gtk.CheckMenuItem(label="Crosshair")
|
||||
# Set the current state based on whether cursor exists
|
||||
if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None:
|
||||
toggle_cursor.set_active(True)
|
||||
else:
|
||||
toggle_cursor.set_active(False)
|
||||
toggle_cursor.connect("toggled", lambda w: self.matplot_cursor_button.set_active(w.get_active()))
|
||||
menu.append(toggle_cursor)
|
||||
|
||||
# Copy as PNG
|
||||
item_copy_png = gtk.MenuItem(label="Copy as PNG")
|
||||
item_copy_png.connect("activate", self.copy_canvas_as_png)
|
||||
menu.append(item_copy_png)
|
||||
|
||||
# Copy as SVG
|
||||
item_copy_svg = gtk.MenuItem(label="Copy as SVG")
|
||||
item_copy_svg.connect("activate", self.copy_canvas_as_svg)
|
||||
menu.append(item_copy_svg)
|
||||
|
||||
menu.show_all()
|
||||
menu.popup_at_pointer(event)
|
||||
|
||||
def copy_canvas_as_png(self, widget):
|
||||
"""Copy canvas to clipboard as PNG"""
|
||||
self.copy_display_data_to_clipboard(widget)
|
||||
@@ -3854,25 +3936,20 @@ class MonitorWidgets:
|
||||
return
|
||||
|
||||
try:
|
||||
# Save the original figure size
|
||||
original_size = self.matplot_figure.get_size_inches()
|
||||
# Save figure to a buffer as SVG
|
||||
import io as iomodule
|
||||
buf = iomodule.StringIO()
|
||||
self.matplot_figure.set_size_inches(6, 4)
|
||||
self.matplot_figure.savefig(buf, format='svg', bbox_inches='tight')
|
||||
buf.seek(0)
|
||||
|
||||
# Copy SVG text to clipboard
|
||||
clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(buf.read(), -1)
|
||||
clipboard.store()
|
||||
self.matplot_figure.set_size_inches(original_size)
|
||||
|
||||
log("Figure copied to clipboard as SVG")
|
||||
logger.info("Figure copied to clipboard as SVG")
|
||||
|
||||
except Exception as e:
|
||||
log(f"Error copying figure as SVG: {e}")
|
||||
logger.error("Error copying figure as SVG: %s", e)
|
||||
|
||||
def copy_display_data_to_clipboard( self, widget, data=None ):
|
||||
"""
|
||||
@@ -3880,38 +3957,42 @@ class MonitorWidgets:
|
||||
Also keep text data copy functionality as fallback
|
||||
"""
|
||||
# First, try to copy the matplotlib figure as image
|
||||
# Save the original figure size
|
||||
original_size = self.matplot_figure.get_size_inches()
|
||||
if self.matplot_figure is not None and self.matplot_canvas is not None:
|
||||
try:
|
||||
# Save figure to a temporary buffer as PNG
|
||||
import io as iomodule
|
||||
buf = iomodule.BytesIO()
|
||||
self.matplot_figure.set_size_inches(6, 4)
|
||||
self.matplot_figure.savefig(buf, format='png', dpi=300, bbox_inches='tight')
|
||||
buf.seek(0)
|
||||
|
||||
# Load the PNG data into a GdkPixbuf
|
||||
from gi.repository import GdkPixbuf
|
||||
loader = GdkPixbuf.PixbufLoader.new_with_type('png')
|
||||
loader.write(buf.read())
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
# Render off-screen at a fixed size (12:8 inches, 800 px
|
||||
# wide) on a temporary Agg canvas. Snapshotting the GTK
|
||||
# canvas buffer instead would copy the window size, and
|
||||
# re-rendering through the GTK canvas would replace its
|
||||
# renderer and leave the on-screen plot blank.
|
||||
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
||||
figure = self.matplot_figure
|
||||
orig_canvas = figure.canvas
|
||||
orig_size = figure.get_size_inches( )
|
||||
orig_dpi = figure.dpi
|
||||
try:
|
||||
agg_canvas = FigureCanvasAgg( figure )
|
||||
figure.set_size_inches( 12, 8 )
|
||||
figure.dpi = 800 / 12
|
||||
agg_canvas.draw( )
|
||||
rgba = numpy.asarray( agg_canvas.buffer_rgba( ) )
|
||||
finally:
|
||||
figure.set_canvas( orig_canvas )
|
||||
figure.set_size_inches( *orig_size )
|
||||
figure.dpi = orig_dpi
|
||||
height, width = rgba.shape[ :2 ]
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
|
||||
glib.Bytes.new( rgba.tobytes( ) ), GdkPixbuf.Colorspace.RGB, True, 8,
|
||||
width, height, width * 4 )
|
||||
|
||||
# Copy to clipboard
|
||||
clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_image(pixbuf)
|
||||
clipboard.store()
|
||||
# Restore original figure size
|
||||
self.matplot_figure.set_size_inches(original_size)
|
||||
# Redraw the canvas to ensure it stays visible
|
||||
self.matplot_canvas.draw()
|
||||
clipboard = gtk.Clipboard.get( gdk.SELECTION_CLIPBOARD )
|
||||
clipboard.set_image( pixbuf )
|
||||
|
||||
log("Figure copied to clipboard as PNG image")
|
||||
logger.info("Figure copied to clipboard as PNG image")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
log("Error copying figure to clipboard: %s" % str(e))
|
||||
logger.error("Error copying figure to clipboard: %s", e)
|
||||
|
||||
##################### functions to feed display
|
||||
def clear_display( self ):
|
||||
@@ -3945,7 +4026,7 @@ class MonitorWidgets:
|
||||
self.measurementresultline = None
|
||||
elif self.measurementresultline is not None:
|
||||
# clear line plot
|
||||
self.measurementresultline[ 0 ].remove( )
|
||||
self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] )
|
||||
self.measurementresultline = None
|
||||
if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None:
|
||||
self.matplot_cursor.disconnect_events()
|
||||
@@ -4205,17 +4286,17 @@ class MonitorWidgets:
|
||||
|
||||
# remove lines and error bars
|
||||
if self.measurementresultgraph is not None:
|
||||
self.measurementresultgraph[ 0 ].remove( )
|
||||
self.matplot_axes.lines.remove( self.measurementresultgraph[ 0 ] )
|
||||
# remove caps
|
||||
for l in self.measurementresultgraph[ 1 ]:
|
||||
l.remove( )
|
||||
self.matplot_axes.lines.remove( l )
|
||||
# and columns
|
||||
for l in self.measurementresultgraph[ 2 ]:
|
||||
l.remove( )
|
||||
self.matplot_axes.collections.remove( l )
|
||||
self.measurementresultgraph = None
|
||||
|
||||
if self.measurementresultline is not None:
|
||||
self.measurementresultline[ 0 ].remove( )
|
||||
self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] )
|
||||
self.measurementresultline = None
|
||||
|
||||
# remove fit overlay lines
|
||||
@@ -4303,13 +4384,13 @@ class MonitorWidgets:
|
||||
|
||||
# finally decide about x log plot
|
||||
if x_scale == "log10" and xmin > 0:
|
||||
self.matplot_axes.set_xscale( "log", base=10.0 )
|
||||
self.matplot_axes.set_xscale( "log", basex=10.0 )
|
||||
self.matplot_axes.fmt_xdata = lambda x: "%g" % x
|
||||
#elif x_scale=="log" and xmin>0:
|
||||
# e scaling implementation not really useful
|
||||
# self.matplot_axes.set_xscale("log", basex=numpy.e)
|
||||
if y_scale == "log10" and ymin > 0:
|
||||
self.matplot_axes.set_yscale( "log", base=10.0 )
|
||||
self.matplot_axes.set_yscale( "log", basex=10.0 )
|
||||
self.matplot_axes.fmt_ydata = lambda x: "%g" % x
|
||||
|
||||
self.__rescale = False
|
||||
|
||||
@@ -135,8 +135,7 @@ class ResultReader:
|
||||
elif t == "Complex":
|
||||
self.result_description[k]=complex(v)
|
||||
elif t == "Boolean":
|
||||
# bool("False") would be True, compare the text instead
|
||||
self.result_description[k]=(v == "True")
|
||||
self.result_description[k]=bool(v)
|
||||
elif t == "String":
|
||||
self.result_description[k]=v
|
||||
else:
|
||||
@@ -503,8 +502,7 @@ class ResultReader:
|
||||
elif t == "Complex":
|
||||
self.result_description[k]=complex(v)
|
||||
elif t == "Boolean":
|
||||
# bool("False") would be True, compare the text instead
|
||||
self.result_description[k]=(v == "True")
|
||||
self.result_description[k]=bool(v)
|
||||
elif t == "String":
|
||||
self.result_description[k]=v
|
||||
else:
|
||||
|
||||
@@ -1414,10 +1414,56 @@ Public License instead of this License.
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="GtkButton" id="display_copy_png_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Copy plot as PNG image to clipboard</property>
|
||||
<signal name="clicked" handler="on_display_copy_png_button_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_copy_png">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="xpad">10</property>
|
||||
<property name="label" translatable="yes"><span foreground="#0000ff">PNG</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">3</property>
|
||||
<property name="right-attach">4</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options"/>
|
||||
<property name="y-options">GTK_FILL</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="GtkButton" id="display_copy_svg_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Copy plot as SVG to clipboard</property>
|
||||
<signal name="clicked" handler="on_display_copy_svg_button_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_copy_svg">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="xpad">10</property>
|
||||
<property name="label" translatable="yes"><span foreground="#0000ff">SVG</span></property>
|
||||
<property name="use-markup">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="right-attach">3</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="bottom-attach">2</property>
|
||||
<property name="x-options"/>
|
||||
<property name="y-options">GTK_FILL</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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()
|
||||
Reference in new issue
Block a user