6 Commits
Author SHA1 Message Date
markusro 42a4f9e2b9 Release v0.32
Build Debian Packages / build (bookworm, debian12) (push) Successful in 11m50s
Build Debian Packages / build (trixie, debian13) (push) Successful in 12m24s
2026-10-22 16:52:02 +02:00
markusro ed5ddde2e3 fix removal of plot artists: Axes.lines/collections are read-only ArtistLists since matplotlib 3.5 2026-10-22 16:51:20 +02:00
markusro 959f17ee33 Release v0.31
Build Debian Packages / build (bookworm, debian12) (push) Successful in 26m15s
Build Debian Packages / build (trixie, debian13) (push) Successful in 26m41s
2026-10-22 01:01:18 +02:00
markusro 63da240504 crosshair and pan toolbar buttons are mutually exclusive 2026-10-22 00:54:21 +02:00
markusro 9d60eeabf6 added phasng for signal 2026-10-22 00:17:01 +02:00
markusro 12d7323851 added persistent ADC overflow warning 2026-10-21 22:48:46 +02:00
6 changed files with 268 additions and 26 deletions

No files matched your search

+13
View File
@@ -1,3 +1,16 @@
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
* replaced temperature with REST source the data from a temperature proxy
+13 -1
View File
@@ -105,8 +105,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
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
=========
+1 -1
View File
@@ -15,7 +15,7 @@
## module contents
#
__version__ = "0.30"
__version__ = "0.32"
__all__=["experiments", "data", "gui"]
import sys
+163 -18
View File
@@ -253,6 +253,7 @@ class DamarisGUI:
"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_statusbar_overflow_label_clicked": self.monitor.clear_overflow_indicator,
"on_experiment_script_textview_event": self.sw.textviews_modified,
"on_main_notebook_switch_page": self.nopevent,
"main_window_close": self.nopevent
@@ -524,6 +525,8 @@ class DamarisGUI:
logger.error("Experiment is still running or cleaning up!")
return
self.monitor.clear_overflow_indicator( )
# get config values:
config_start = time.time()
actual_config = self.config.get( )
@@ -2959,13 +2962,27 @@ class MonitorWidgets:
self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" )
self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" )
self.display_descriptions_checkbutton = gtk.CheckButton(label="Show Desc.")
self.display_descriptions_checkbutton = gtk.CheckButton(label="Descr.")
self.display_descriptions_checkbutton.set_active(True)
self.display_descriptions_checkbutton.show()
self.display_settings_frame.set_property("n-columns", 5)
self.display_settings_frame.attach(self.display_descriptions_checkbutton, 4, 5, 0, 1, gtk.AttachOptions.FILL, 0, 0, 0)
self.display_descriptions_checkbutton.connect("toggled", self.display_descriptions_toggled)
# 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) --------------------------------------------------------
# create new plot
@@ -2997,6 +3014,9 @@ class MonitorWidgets:
#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(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
self.matplot_cursor_button = gtk.ToggleButton(label='')
cursor_icon = GdkPixbuf.Pixbuf.new_from_xpm_data( crosshair_xpm )
@@ -3040,6 +3060,11 @@ class MonitorWidgets:
self.display_source_combobox.add_attribute( display_source_cell, 'text', 0 )
# GTK3 fix: Prevent blank space from child tree nodes in popup
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.display_source_path_label = self.xml_gui.get_object( "display_source_path_label" )
@@ -3069,9 +3094,11 @@ class MonitorWidgets:
self.fitresultlines = [ ]
self.fit_source_keys = set( )
self.clipping_marker = None
self.statusbar_overflow_label = self.xml_gui.get_object( "statusbar_overflow_label" )
self.source_list_cache = {}
def source_list_reset( self ):
self.display_phase_scale.set_value( 0 )
self.display_source_combobox.set_model(None)
try:
# OPTIMIZATION: Replacing the TreeStore is much faster than clear() for large trees
@@ -3440,6 +3467,25 @@ class MonitorWidgets:
event = DataPool.Event( DataPool.Event.updated_value, new_data_name )
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 ):
self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) )
if self.displayed_data[ 0 ] is not None:
@@ -3619,6 +3665,11 @@ class MonitorWidgets:
self._pan_start = 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):
"""Toggle matplotlib crosshair cursor"""
if widget.get_active():
@@ -3627,7 +3678,9 @@ class MonitorWidgets:
self.matplot_cursor = CrosshairCursor(self.matplot_axes,
color='red', linewidth=1)
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:
# Deactivate cursor
@@ -3709,6 +3762,61 @@ class MonitorWidgets:
except Exception as 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
def clear_display( self ):
"""
@@ -3741,7 +3849,7 @@ class MonitorWidgets:
self.measurementresultline = None
elif self.measurementresultline is not None:
# clear line plot
self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] )
self.measurementresultline[ 0 ].remove( )
self.measurementresultline = None
if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None:
self.matplot_cursor.disconnect_events()
@@ -3807,11 +3915,41 @@ class MonitorWidgets:
return
if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ):
xmin = in_result.get_xmin( )
xmax = in_result.get_xmax( )
plot_result = in_result
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( )
ymax = in_result.get_ymax( )
# error bars are read from the pool object, i.e. they are not
# 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?
if self.__rescale:
@@ -3822,7 +3960,7 @@ class MonitorWidgets:
self.matplot_axes.set_xscale( "linear" )
self.matplot_axes.set_xlim(xmin, xmax)
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_xlim(xminpos, xmax)
elif x_scale == "ppm":
@@ -3836,7 +3974,7 @@ class MonitorWidgets:
self.matplot_axes.set_yscale( "linear" )
self.matplot_axes.set_ylim(ymin, ymax)
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_ylim( yminpos, ymax )
self.__rescale = False
@@ -3876,14 +4014,14 @@ class MonitorWidgets:
ylim_max > ymax + 0.2 * ydiff or ylim_min < ymin - 0.2 * ydiff):
self.matplot_axes.set_ylim(ymin, ymax)
xdata = in_result.get_xdata( )
chans = in_result.get_number_of_channels( )
xdata = plot_result.get_xdata( )
chans = plot_result.get_number_of_channels( )
data = [ ]
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
for i in range( chans ):
data.append( in_result.get_ydata( i ) )
data.append( plot_result.get_ydata( i ) )
moving_average = data_slice = None
if max_points_to_display > 0 and len( xdata ) > max_points_to_display:
@@ -3912,8 +4050,7 @@ class MonitorWidgets:
self.graphen[ i ].set_data( xdata, data[ i ] )
# Statistics activated?
if (self.display_statistics_checkbutton.get_active( ) and
in_result.uses_statistics( ) and in_result.ready_for_drawing_error( )):
if show_errors:
for i in range( chans ):
err = in_result.get_yerr( i )
if moving_average is not None:
@@ -3986,6 +4123,14 @@ class MonitorWidgets:
color="red", fontsize=20, fontweight="bold",
ha="center", va="top",
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:
try:
self.clipping_marker.remove()
@@ -4001,17 +4146,17 @@ class MonitorWidgets:
# remove lines and error bars
if self.measurementresultgraph is not None:
self.matplot_axes.lines.remove( self.measurementresultgraph[ 0 ] )
self.measurementresultgraph[ 0 ].remove( )
# remove caps
for l in self.measurementresultgraph[ 1 ]:
self.matplot_axes.lines.remove( l )
l.remove( )
# and columns
for l in self.measurementresultgraph[ 2 ]:
self.matplot_axes.collections.remove( l )
l.remove( )
self.measurementresultgraph = None
if self.measurementresultline is not None:
self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] )
self.measurementresultline[ 0 ].remove( )
self.measurementresultline = None
# remove fit overlay lines
+77 -5
View File
@@ -37,6 +37,12 @@
<property name="step-increment">1</property>
<property name="page-increment">1</property>
</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">
<property name="visible">True</property>
<property name="can-focus">False</property>
@@ -1096,6 +1102,47 @@ Public License instead of this License.
<property name="position">1</property>
</packing>
</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>
<packing>
<property name="y-options"/>
@@ -1191,10 +1238,11 @@ Public License instead of this License.
</child>
<child>
<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="can-focus">True</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="use-underline">True</property>
<property name="draw-indicator">True</property>
@@ -1211,10 +1259,11 @@ Public License instead of this License.
</child>
<child>
<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="can-focus">True</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="use-underline">True</property>
<property name="draw-indicator">True</property>
@@ -2343,7 +2392,7 @@ Public License instead of this License.
<object class="GtkTable" id="statusbar_table">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="n-columns">8</property>
<property name="n-columns">9</property>
<child>
<object class="GtkVSeparator" id="vseparator1">
<property name="visible">True</property>
@@ -2430,6 +2479,29 @@ Public License instead of this License.
<property name="y-options"/>
</packing>
</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>
<object class="GtkLabel" id="label33">
<property name="visible">True</property>
@@ -2438,8 +2510,8 @@ Public License instead of this License.
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">7</property>
<property name="right-attach">8</property>
<property name="left-attach">8</property>
<property name="right-attach">9</property>
<property name="y-options"/>
</packing>
</child>
+1 -1
View File
@@ -11,7 +11,6 @@ def result():
accu = Accumulation(error=False)
for num,ts in enumerate(results):
data["accu"] = accu
data["data/ts %i"%num] = ts+0
ts.write_to_hdf(h5, "/", f"adc_{num}", f"adc_{num}")
data["fft"] = (ts+0).fft()
@@ -32,6 +31,7 @@ def result():
data["test/fit"] = fit
data["test"] = r
accu+=ts
data["accu"] = accu+0
print(accu)
data["accu_fft"] = accu.fft().autophase()
accu.write_to_hdf(h5, "/", "name", "title")