Merge branch 'main' of gitea.pkm.physik.tu-darmstadt.de:IPKM/python3-damaris
This commit is contained in:
markusro committed 2026-07-14 23:08:12 +02:00
commit 8bd088d97f
15 files changed
+2700 -232

No files matched your search

+287 -155
View File
@@ -70,36 +70,9 @@ debug = False
# version info
from damaris import __version__
class logstream:
gui_log = None
text_log = sys.__stdout__
def write( self, message ):
# default for stdout and stderr
if self.gui_log is not None:
self.gui_log( message )
if debug or self.gui_log is None:
self.text_log.write( message )
self.text_log.flush( )
def __call__( self, message ):
self.write( message )
def flush(self):
pass
def __del__( self ):
pass
global log
log = logstream( )
sys.stdout = log
sys.stderr = log
ExperimentHandling.log = log
ResultHandling.log = log
ResultReader.log = log
ExperimentWriter.log = log
BackendDriver.log = log
DataPool.log = log
# Unified logging
import logging
logger = logging.getLogger("damaris.gui")
class LockFile:
@@ -109,17 +82,17 @@ class LockFile:
self.conn = sqlite3.connect(self.file)
self.cursor = self.conn.cursor()
self.id = None
print("Connected to db..")
logger.debug("Connected to db..")
def add_experiment(self):
print("Add experiment..")
logger.debug("Add experiment..")
self.id = str(uuid.uuid4())
self.cursor.execute("INSERT INTO damaris VALUES (?, ?)",(self.id, "started"))
self.conn.commit()
return self.id
def del_experiment(self, id):
print("Delete experiment..")
logger.debug("Delete experiment..")
self.cursor.execute('DELETE FROM damaris WHERE uuid=?', (id,))
self.conn.commit()
return True
@@ -166,6 +139,13 @@ class DamarisGUI:
self.log = LogWindow( self.xml_gui , self)
# Initialize unified logging with GUI output
from damaris.gui import setup_logging
self._logger = setup_logging(
level=logging.DEBUG if debug else logging.INFO,
gui_callback=self.log,
)
self.sw = ScriptWidgets( self.xml_gui, self )
self.toolbar_init( )
@@ -502,23 +482,23 @@ class DamarisGUI:
def start_experiment( self, widget, data=None ):
import time
overall_start = time.time()
print(f"DEBUG: start_experiment called at {overall_start}")
logger.debug("start_experiment called at %s", overall_start)
# something running?
if self.si is not None or self.state != DamarisGUI.Edit_State:
print("Experiment is still running or cleaning up!")
logger.error("Experiment is still running or cleaning up!")
return
# get config values:
config_start = time.time()
actual_config = self.config.get( )
print(f"DEBUG: Config load took {time.time() - config_start:.3f}s")
logger.debug("Config load took %.3fs", time.time() - config_start)
# get scripts and start script interface
gui_start = time.time()
self.sw.disable_editing( )
exp_script, res_script = self.sw.get_scripts( )
print(f"DEBUG: GUI operations took {time.time() - gui_start:.3f}s")
logger.debug("GUI operations took %.3fs", time.time() - gui_start)
if not actual_config[ "start_result_script" ]:
res_script = ""
if not actual_config[ "start_experiment_script" ]:
@@ -528,7 +508,7 @@ class DamarisGUI:
backend = ""
if (backend == "" and exp_script == "" and res_script == ""):
print("nothing to do...so doing nothing!")
logger.info("Nothing to do...so doing nothing!")
self.sw.enable_editing( )
return
@@ -546,11 +526,11 @@ class DamarisGUI:
ln = 0
if type( lo ) is not int:
lo = 0
print("Experiment Script: %s at line %d, col %d:" % (e.__class__.__name__, ln, lo))
logger.error("Experiment Script: %s at line %d, col %d", e.__class__.__name__, ln, lo)
if e.text != "":
print(e.text.rstrip())
print(" "*(e.offset-1)+"^")
print(e)
logger.error(e.text.rstrip())
logger.error(" "*(e.offset-1)+"^")
logger.error("%s", e)
res_code = None
if res_script != "":
@@ -564,14 +544,14 @@ class DamarisGUI:
ln = 0
if type( lo ) is not int:
lo = 0
print("Result script: %s at line %d, col %d:" % (e.__class__.__name__, ln, lo))
logger.error("Result script: %s at line %d, col %d", e.__class__.__name__, ln, lo)
if e.text != "":
print("\"%s\"" % e.text)
# print " "*(e.offset+1)+"^" # nice idea, but needs monospaced fonts
logger.error("\"%s\"", e.text)
# logger.error(" "*(e.offset+1)+"^" # nice idea, but needs monospaced fonts
pass
print(e)
logger.error("%s", e)
print(f"DEBUG: Script compilation took {time.time() - compile_start:.3f}s")
logger.debug("Script compilation took %.3fs", time.time() - compile_start)
# detect error
if (exp_script != "" and exp_code is None) or \
@@ -604,10 +584,10 @@ class DamarisGUI:
pass
# Use observe_data_pool(None) which is now optimized with fast TreeStore replacement
print(f"DEBUG: Data pool cleanup ({listener_count} listeners, {data_items} items)")
logger.debug("Data pool cleanup (%d listeners, %d items)", listener_count, data_items)
self.monitor.observe_data_pool( None )
print(f"DEBUG: Data cleanup took {time.time() - data_cleanup_start:.3f}s")
logger.debug("Data cleanup took %.3fs", time.time() - data_cleanup_start)
# set the text mark for hdf logging
if self.log.textbuffer.get_mark( "lastdumped" ) is None:
@@ -618,14 +598,12 @@ class DamarisGUI:
interval = 0.1
self.stop_experiment_flag.clear()
# Add timing debug info
import time
start_time = time.time()
print(f"DEBUG: Starting experiment at {start_time}")
logger.debug("Starting experiment at %s", start_time)
self.id = self.lockfile.add_experiment()
print(f"DEBUG: Lock file operation took {time.time() - start_time:.3f}s")
logger.debug("Lock file operation took %.3fs", time.time() - start_time)
# Add more timing points
lock_wait_start = time.time()
while not self.lockfile.am_i_next():
@@ -638,9 +616,9 @@ class DamarisGUI:
loop_run = 0
loop_run += interval
print(f"DEBUG: Lock wait took {time.time() - lock_wait_start:.3f}s")
logger.debug("Lock wait took %.3fs", time.time() - lock_wait_start)
if self.stop_experiment_flag.isSet():
if self.stop_experiment_flag.is_set():
#Experiment has been stopped, clean up and leave
self.experiment_script_statusbar_label.set_text("Experiment stopped")
self.state = DamarisGUI.Edit_State
@@ -662,9 +640,10 @@ class DamarisGUI:
res_code,
backend,
self.spool_dir,
log_callback=self.log,
clear_jobs=actual_config[ "del_jobs_after_execution" ],
clear_results=actual_config[ "del_results_after_processing" ] )
print(f"DEBUG: ScriptInterface creation took {time.time() - script_interface_start:.3f}s")
logger.debug("ScriptInterface creation took %.3fs", time.time() - script_interface_start)
self.data = self.si.data
# run frontend and script engines
@@ -672,18 +651,18 @@ class DamarisGUI:
# Start temperature monitoring if configured (before scripts start)
temp_start = time.time()
self.start_temperature_monitoring(actual_config)
print(f"DEBUG: Temperature monitoring startup took {time.time() - temp_start:.3f}s")
logger.debug("Temperature monitoring startup took %.3fs", time.time() - temp_start)
scripts_start = time.time()
self.si.runScripts( )
print(f"DEBUG: Script execution startup took {time.time() - scripts_start:.3f}s")
logger.debug("Script execution startup took %.3fs", time.time() - scripts_start)
except Exception as e:
#print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
#print "Full traceback:"
#logger.debug("ToDo evaluate exception %s at %s", str(e), traceback.extract_tb(sys.exc_info()[2])[-1][1:3])
#logger.debug("Full traceback:")
traceback_file = io.StringIO( )
traceback.print_tb( sys.exc_info( )[ 2 ], None, traceback_file )
self.main_notebook.set_current_page( DamarisGUI.Log_Display )
print("Error while executing scripts: %s\n" % str( e ) + traceback_file.getvalue( ))
logger.error("Error while executing scripts: %s", str( e ) + traceback_file.getvalue( ))
traceback_file = None
self.data = None
@@ -691,14 +670,14 @@ class DamarisGUI:
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f]
for r in still_running:
r.quit_flag.set( )
print("waiting for threads stoping...", end=' ')
logger.info("Waiting for threads to stop...")
still_running = [x for x in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if x is not None and x.is_alive( )]
for t in still_running:
while t.is_alive():
while gtk.events_pending():
gtk.main_iteration_do(False)
t.join( 0.05 )
print("done")
logger.info("Threads stopped")
# cleanup
self.si = None
@@ -785,17 +764,18 @@ class DamarisGUI:
if not self.si.exp_handling.is_alive( ):
self.si.exp_handling.join( )
if self.si.exp_handling.raised_exception:
print("experiment script failed at line %d (function %s): %s" % (self.si.exp_handling.location[ 0 ],
self.si.exp_handling.location[ 1 ],
self.si.exp_handling.raised_exception))
print("Full traceback", self.si.exp_handling.traceback)
logger.error("Experiment script failed at line %d (function %s): %s",
self.si.exp_handling.location[ 0 ],
self.si.exp_handling.location[ 1 ],
self.si.exp_handling.raised_exception)
logger.error("Full traceback: %s", self.si.exp_handling.traceback)
e_text = "Experiment Script Failed (%d)" % e
else:
e_text = "Experiment Script Finished (%d)" % e
print("experiment script finished")
logger.info("Experiment script finished")
self.si.exp_handling = None
else:
#print self.si.back_driver.get_messages()
# Backend messages are now handled by FileTailerHandler
e_text = "Experiment Script Running (%d)" % e
e_text += experimenttimetext
@@ -803,10 +783,11 @@ class DamarisGUI:
if not self.si.res_handling.is_alive( ):
self.si.res_handling.join( )
if self.si.res_handling.raised_exception:
print("result script failed at line %d (function %s): %s" % (self.si.res_handling.location[ 0 ],
self.si.res_handling.location[ 1 ],
self.si.res_handling.raised_exception))
print("Full traceback", self.si.res_handling.traceback)
logger.error("Result script failed at line %d (function %s): %s",
self.si.res_handling.location[ 0 ],
self.si.res_handling.location[ 1 ],
self.si.res_handling.raised_exception)
logger.error("Full traceback: %s", self.si.res_handling.traceback)
r_text = "Result Script Failed (%d)" % r
else:
r_text = "Result Script Finished (%d)" % r
@@ -830,16 +811,16 @@ class DamarisGUI:
if self.dump_thread is not None:
if self.dump_thread.is_alive( ):
sys.stdout.write( "." )
sys.stderr.write( "." )
self.dump_dots += 1
if self.dump_dots > 80:
print()
sys.stderr.write("\n")
self.dump_dots = 0
else:
self.dump_thread.join( )
self.dump_thread = None
dump_size = os.stat( self.dump_filename ).st_size / 1e6
print("done (%.1f s, %.1f MB)" % (time.time( ) - self.dump_start_time, dump_size))
logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size)
gdk.threads_enter( )
if e_text:
@@ -855,12 +836,12 @@ class DamarisGUI:
# Stop temperature monitoring when experiment finishes
self.stop_temperature_monitoring()
if self.save_thread is None and self.dump_filename != "":
print("all subprocesses ended, saving data pool")
logger.info("All subprocesses ended, saving data pool")
# Disable run button during data saving to prevent UI confusion
gdk.threads_enter()
self.toolbar_run_button.set_sensitive(False)
gdk.threads_leave()
# thread to save data...
self.save_thread = threading.Thread( target=self.dump_states, name="dump states" )
self.save_thread.start( )
@@ -874,21 +855,21 @@ class DamarisGUI:
self.toolbar_stop_button.set_sensitive( False )
gdk.threads_leave( )
if len( still_running ) != 0:
print("subprocess(es) still running: " + ', '.join( [s.getName( ) for s in still_running] ))
logger.warning("Subprocess(es) still running: %s", ', '.join( [s.getName( ) for s in still_running] ))
return True
else:
if self.save_thread is not None:
if self.save_thread.is_alive( ):
sys.stdout.write( "." )
sys.stderr.write( "." )
self.dump_dots += 1
if self.dump_dots > 80:
print()
sys.stderr.write("\n")
self.dump_dots = 0
return True
self.save_thread.join( )
self.save_thread = None
dump_size = os.stat( self.dump_filename ).st_size / 1e6
print("done (%.1f s, %.1f MB)" % (time.time( ) - self.dump_start_time, dump_size))
logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size)
# keep data to display but throw away everything else
self.si = None
@@ -909,7 +890,7 @@ class DamarisGUI:
dump_interval = self.dump_timeinterval if self.dump_timeinterval > 0 else 2.0
if self.dump_thread is None and self.dump_filename != "" and \
self.last_dumped + dump_interval < time.time( ):
print("dumping data pool")
logger.info("Dumping data pool")
self.dump_start_time = time.time( )
self.dump_dots = 0
# thread to save data...
@@ -944,13 +925,13 @@ class DamarisGUI:
try:
self.dump_filename = actual_config.get( "data_pool_name" ) % extensions_dict
except ValueError:
print("invalid formating character in '" + actual_config.get( "data_pool_name" ) + "'")
logger.error("Invalid formatting character in '%s'", actual_config.get( "data_pool_name" ))
self.dump_filename = "DAMARIS_data_pool.h5"
print("reseting dumpfile to " + self.dump_filename)
logger.info("Resetting dumpfile to %s", self.dump_filename)
del extensions_dict
if os.path.split( self.dump_filename )[ 1 ] == "" or os.path.isdir( self.dump_filename ):
print("The dump filename is a directory, using filename 'DAMARIS_data_pool.h5'")
logger.warning("Dump filename is a directory, using filename 'DAMARIS_data_pool.h5'")
self.dump_filename += os.sep + "DAMARIS_data_pool.h5"
# compression
@@ -967,7 +948,7 @@ class DamarisGUI:
try:
self.dump_timeinterval = int( 60 * float( actual_config[ "data_pool_write_interval" ] ) )
except ValueError as e:
print("configuration provides non-number for dump interval: " + str( e ))
logger.warning("Configuration provides non-number for dump interval: %s", str( e ))
# if existent, move away old files
if os.path.isfile( self.dump_filename ):
@@ -989,15 +970,15 @@ class DamarisGUI:
last_backup -= 1
os.rename( self.dump_filename, dump_filename_pattern % 0 )
if cummulated_size > (1 << 30):
print("Warning: the cumulated backups size of '%s' is %d MByte" % (self.dump_filename,
cummulated_size / (1 << 20)))
logger.warning("The cumulated backups size of '%s' is %d MByte", self.dump_filename,
cummulated_size / (1 << 20))
# init is finnished now
# now it's time to create the hdf file
dump_file = None
if not os.path.isfile( self.dump_filename ):
if not init:
print("dump file \"%s\" vanished unexpectedly, creating new one" % self.dump_filename)
logger.warning("Dump file \"%s\" vanished unexpectedly, creating new one", self.dump_filename)
# have a look to the path and create necessary directories
dir_stack = [ ]
dir_trunk = os.path.dirname( os.path.abspath( self.dump_filename ) )
@@ -1011,8 +992,7 @@ class DamarisGUI:
continue
os.mkdir( dir_trunk )
except OSError as e:
print(e)
print("could not create dump directory '%s', so hdf5 dumps disabled" % dir_trunk)
logger.error("Could not create dump directory '%s', so hdf5 dumps disabled: %s", dir_trunk, e)
self.dump_filename = ""
self.dump_timeinterval = 0
return True
@@ -1055,7 +1035,7 @@ class DamarisGUI:
if dump_file is None:
# exit!
print("could not create dump directory '%s', so hdf5 dumps disabled" % dir_trunk)
logger.error("Could not create dump directory '%s', so hdf5 dumps disabled", dir_trunk)
self.dump_filename = ""
self.dump_timeinterval = 0
return True
@@ -1199,24 +1179,24 @@ class DamarisGUI:
temp_baudrate = config.get("temperature_baudrate", 19200)
if not temp_device:
print("Temperature monitoring: No device configured for Eurotherm")
logger.warning("Temperature monitoring: No device configured for Eurotherm")
return
self.temp_controller = create_controller(
"eurotherm", temp_device, baudrate=temp_baudrate
)
print(f"Temperature monitoring started with Eurotherm on {temp_device} (interval: {temp_interval}s)")
logger.info("Temperature monitoring started with Eurotherm on %s (interval: %ss)", temp_device, temp_interval)
elif controller_type == "simulated":
# Simulated controller doesn't need device configuration
# Use default parameters: 290K base, 10K amplitude, 60s period, 5% noise, 10% missing
self.temp_controller = create_controller("simulated")
print(f"Temperature monitoring started with simulated controller (interval: {temp_interval}s)")
logger.info("Temperature monitoring started with simulated controller (interval: %ss)", temp_interval)
else:
print(f"Temperature monitoring: Unknown controller type: {controller_type}")
logger.warning("Temperature monitoring: Unknown controller type: %s", controller_type)
return
# Create and start monitor thread
self.temp_monitor = TemperatureMonitor(
controller=self.temp_controller,
@@ -1225,9 +1205,9 @@ class DamarisGUI:
data_key="temperature"
)
self.temp_monitor.start()
except Exception as e:
print(f"Failed to start temperature monitoring: {e}")
logger.error("Failed to start temperature monitoring: %s", e)
import traceback
traceback.print_exc()
self.temp_controller = None
@@ -1240,17 +1220,17 @@ class DamarisGUI:
if self.temp_monitor is not None:
try:
self.temp_monitor.stop()
print("Temperature monitoring stopped")
logger.info("Temperature monitoring stopped")
except Exception as e:
print(f"Error stopping temperature monitoring: {e}")
logger.error("Error stopping temperature monitoring: %s", e)
finally:
self.temp_monitor = None
if self.temp_controller is not None:
try:
self.temp_controller.close()
except Exception as e:
print(f"Error closing temperature controller: {e}")
logger.error("Error closing temperature controller: %s", e)
finally:
self.temp_controller = None
@@ -1281,7 +1261,7 @@ class DamarisGUI:
if not self.doc_browser.is_alive( ):
self.doc_browser.join( )
if self.doc_browser.my_webbrowser is not None:
print("new browser tab")
logger.debug("New browser tab")
self.doc_browser.my_webbrowser.open_new_tab( self.doc_urls[ requested_doc ] )
else:
del self.doc_browser
@@ -1291,7 +1271,7 @@ class DamarisGUI:
self.doc_browser = start_browser( self.doc_urls[ requested_doc ] )
self.doc_browser.start( )
else:
print("missing docs for '%s'" % (requested_doc))
logger.warning("Missing docs for '%s'", requested_doc)
show_manual = show_doc_menu
@@ -1315,11 +1295,11 @@ class start_browser( threading.Thread ):
# this is what it should be everywhere!
self.my_webbrowser = webbrowser.get( )
if self.my_webbrowser is not None:
print("starting web browser (module webbrowser)")
logger.info("Starting web browser (module webbrowser)")
self.my_webbrowser.open( self.start_url )
return True
# last resort
print("starting web browser (webbrowser.py)")
logger.info("Starting web browser (webbrowser.py)")
self.my_webbrowser_process = os.spawnl( os.P_NOWAIT,
sys.executable,
os.path.basename( sys.executable ),
@@ -1329,8 +1309,23 @@ class start_browser( threading.Thread ):
class LogWindow:
"""
writes messages to the log window
"""Manages the application log window.
Provides a timestamped, scrollable text view for application messages.
Messages are dispatched via ``gobject.idle_add`` to keep the GTK main
loop responsive.
Args:
xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets.
damaris_gui: Reference to the main :class:`DamarisGUI` application.
Attributes:
textview (:class:`gtk.TextView`): The monospaced text view widget.
textbuffer (:class:`gtk.TextBuffer`): The buffer backing the text view.
Examples:
>>> log = LogWindow(xml_gui, damaris_gui)
>>> log("Application started")
"""
def __init__( self, xml_gui, damaris_gui ):
@@ -1342,8 +1337,6 @@ class LogWindow:
font_desc = pango.FontDescription("Monospace 10")
self.textview.modify_font(font_desc)
self.textbuffer = self.textview.get_buffer( )
self.logstream = log
self.logstream.gui_log = self
self.last_timetag = None
self( "Started in directory %s\n" % os.getcwd( ) )
@@ -1373,14 +1366,59 @@ class LogWindow:
return False
def __del__( self ):
self.logstream.gui_log = None
self.logstream = None
pass
class ScriptWidgets:
"""Manages experiment and result script editing widgets.
Provides two syntax-highlighted, auto-completing text views for editing
DAMARIS experiment and result scripts. Handles file open/save dialogs,
toolbar state management, line/column indicators, and search functionality.
Args:
xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets.
outer_space: Reference to the parent application (typically
:class:`DamarisGUI`) that owns this widget set.
Attributes:
editing_state (bool): ``True`` if scripts are editable, ``False`` during
experiment execution.
exp_script_filename (str or None): Path to the currently loaded
experiment script file.
res_script_filename (str or None): Path to the currently loaded
result script file.
experiment_script_textview (:class:`gtksourceview2.View`): Text view
for the experiment script.
data_handling_textview (:class:`gtksourceview2.View`): Text view for
the result script.
Public Methods:
set_scripts: Replace script buffer contents.
get_scripts: Retrieve current script text from both buffers.
enable_editing: Re-enable script editing.
disable_editing: Lock scripts during experiment execution.
open_file: Open a script file via file chooser dialog.
open_hdf5: Load data and scripts from an HDF5 file.
save_file: Save the current script to its associated file.
save_file_as: Save the current script with a new filename.
save_all_files: Save both experiment and result scripts.
new_file: Clear the current script buffer.
check_script: Compile-check the current script for syntax errors.
undo / redo: Undo/redo text edits.
search: Open the search-and-replace dialog.
"""
def __init__( self, xml_gui, outer_space ):
"""
initialize text widgets with text
"""Initialize text widgets with syntax highlighting and completion.
Sets up two :class:`gtksourceview2.View` instances with Python
syntax highlighting, keyword auto-completion, line numbers, and
smart indentation. Connects text buffer signals to toolbar and
status bar update handlers.
Args:
xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets.
outer_space: Reference to the parent application.
"""
self.xml_gui = xml_gui
self.outer_space = outer_space
@@ -1641,8 +1679,8 @@ get_ylabel set_ylabel"""
ln = 0
if type( lo ) is not int:
lo = 0
print("Syntax Error:\n%s in %s at line %d, offset %d" % (
str( se ), se.filename, ln, lo) + "\n(ToDo: Dialog)")
logger.error("Syntax Error:\n%s in %s at line %d, offset %d",
str( se ), se.filename, ln, lo)
if ln > 0 and ln <= tb.get_line_count( ):
new_place = tb.get_iter_at_line_offset( ln - 1, 0 )
if lo > 0 and lo <= new_place.get_chars_in_line( ):
@@ -1650,7 +1688,7 @@ get_ylabel set_ylabel"""
tb.place_cursor( new_place )
tv.scroll_to_iter( new_place, 0.2, False, 0, 0 )
except Exception as e:
print("Compilation Error:\n" + str( e ) + "\n(ToDo: Dialog)")
logger.error("Compilation Error:\n%s", str( e ))
def notebook_page_switched( self, notebook, page, pagenumber ):
self.set_toolbuttons_status( )
@@ -1668,7 +1706,7 @@ get_ylabel set_ylabel"""
newline = self.data_handling_line_indicator.get_value_as_int( ) - 1
newcol = column_indicator.get_value_as_int( ) - 1
else:
print("unknown line/column selector")
logger.error("Unknown line/column selector")
return False
textbuffer = textview.get_buffer( )
@@ -2474,13 +2512,13 @@ pygobject version %(pygobject)s
elif isinstance( model, gtk.TreeStore ):
model.append( None, [ libname ] )
else:
print("cannot append compression lib name to %s" % model.__class__.__name__)
logger.warning("Cannot append compression lib name to %s", model.__class__.__name__)
# debug message
if debug:
print("DAMARIS", __version__)
print(components_text % components_versions)
logger.debug("DAMARIS %s", __version__)
logger.debug(components_text % components_versions)
# set no compression as default...
self.config_data_pool_complib.set_active( 0 )
@@ -2496,7 +2534,7 @@ pygobject version %(pygobject)s
if os.access( self.system_default_filename, os.R_OK ):
self.load_config( self.system_default_filename )
else:
print("can not read system defaults from %s, ask your instrument responsible if required" % self.system_default_filename)
logger.warning("Can not read system defaults from %s, ask your instrument responsible if required", self.system_default_filename)
self.config_from_system = self.get( )
self.load_config( )
@@ -2619,7 +2657,7 @@ pygobject version %(pygobject)s
iter = model.iter_next( iter )
# if this compression method is not supported, warn and do nothing
if iter is None:
print("compression method %s is not supported" % config[ "data_pool_complib" ])
logger.warning("Compression method %s is not supported", config[ "data_pool_complib" ])
def on_adc_bit_depth_changed(self, widget):
ADC_Result.default_bit_depth = widget.get_value_as_int()
@@ -2783,7 +2821,7 @@ pygobject version %(pygobject)s
readfile = open( filename, "rb" )
except Exception as e:
if debug:
print("Could not open %s: %s" % (filename, str( e )))
logger.debug("Could not open %s: %s", filename, str( e ))
return
# parser functions
@@ -2835,7 +2873,7 @@ pygobject version %(pygobject)s
if not os.path.isdir( dirs ):
os.makedirs( dirs )
print("save config to: "+filename)
logger.info("Save config to: %s", filename)
configfile = open( filename, "w" )
configfile.write( "<?xml version='1.0'?>\n" )
@@ -2844,10 +2882,10 @@ pygobject version %(pygobject)s
if k in self.config_from_system \
and self.config_from_system[ k ] == v:
if debug:
print("Ignoring for write, because system value for %r is %r equal to %r" % \
(k, self.config_from_system[ k ], v))
logger.debug("Ignoring for write, because system value for %r is %r equal to %r",
k, self.config_from_system[ k ], v)
continue
print(k,v, type(v))
logger.debug("Config key: %s = %r (%s)", k, v, type(v).__name__)
val = ""
typename = ""
if type( v ) is bool:
@@ -2870,9 +2908,54 @@ pygobject version %(pygobject)s
class MonitorWidgets:
"""Manages the matplotlib-based data visualization panel.
Provides an interactive plot canvas with zoom, pan, scaling controls,
a data source selector, and real-time updates from the DAMARIS data pool.
Supports :class:`Accumulation`, :class:`ADC_Result`, and
:class:`MeasurementResult` data types with automatic axis rescaling,
error bars, and statistics overlays.
Args:
xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets.
Attributes:
matplot_figure (:class:`matplotlib.figure.Figure`): The underlying
matplotlib figure.
matplot_axes (:class:`matplotlib.axes.Axes`): The plot axes.
matplot_canvas (:class:`matplotlib.backends.backend_gtk3.FigureCanvas`):
The GTK canvas embedding the figure.
matplot_toolbar (:class:`matplotlib.backends.backend_gtk3.NavigationToolbar2GTK3`):
The navigation toolbar with pan/zoom/cursor controls.
data_pool (:class:`damaris.data.DataPool` or None): The data pool
being observed for real-time updates.
displayed_data (list): ``[data_name, data_object]`` of the currently
selected data, or ``[None, None]``.
Public Methods:
observe_data_pool: Register a listener and begin observing a data pool.
source_list_reset: Reset the data source selector tree.
source_list_add: Add a data source entry to the selector tree.
source_list_remove: Remove a data source entry from the selector tree.
source_list_current: Get the currently selected data source name.
clear_display: Clear all plot elements and reset axes.
update_display: Redraw the plot with current data.
renew_display: Clear and redraw the plot with current data.
save_display_data_as_text: Export displayed data to a CSV file.
toggle_cursor: Toggle the matplotlib crosshair cursor.
copy_canvas_as_png: Copy the plot as a PNG image to the clipboard.
copy_canvas_as_svg: Copy the plot as SVG text to the clipboard.
"""
def __init__( self, xml_gui ):
"""
initialize matplotlib widgets and stuff around
"""Initialize matplotlib widgets, canvas, toolbar, and source selector.
Creates a :class:`matplotlib.figure.Figure` with linear x/y axes,
embeds it in a GTK canvas, attaches a navigation toolbar with a
custom crosshair toggle button, and builds a hierarchical source
selector :class:`gtk.TreeStore`.
Args:
xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets.
"""
self.xml_gui = xml_gui
@@ -3096,11 +3179,11 @@ class MonitorWidgets:
pwd = namelist[ : ]
iter = self.source_list_find( namelist )
if iter is None or len( namelist ) > 0:
print("source_list_remove: WARNING: Not found")
logger.warning("source_list_remove: Not found")
return
model = self.display_source_treestore
if model.iter_has_child( iter ):
print("source_list_remove: WARNING: Request to delete a tree")
logger.warning("source_list_remove: Request to delete a tree")
return
while True:
parent = model.iter_parent( iter )
@@ -3185,7 +3268,7 @@ class MonitorWidgets:
if event.subject.startswith( "__" ):
return
if debug and self.update_counter < 0:
print("negative event count!", self.update_counter)
logger.debug("Negative event count: %d", self.update_counter)
# Throttling removed as it blocks data production thread.
# GUI updates are already decoupled via idle_add.
@@ -3231,10 +3314,10 @@ class MonitorWidgets:
do fast work selecting important events
"""
if debug and self.update_counter < 0:
print("negative event count!", self.update_counter)
logger.debug("Negative event count: %d", self.update_counter)
if self.update_counter > 5:
if debug:
print("sleeping to find time for graphics updates")
logger.debug("Sleeping to find time for graphics updates")
threading.Event( ).wait( 0.05 )
while self.update_counter > 15:
threading.Event( ).wait( 0.05 )
@@ -3263,7 +3346,7 @@ class MonitorWidgets:
if self.displayed_data[ 1 ] is new_data_struct:
# update display only
if self.update_counter > 10:
print("update queue too long (%d>10): skipping one update" % self.update_counter)
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
else:
gdk.threads_enter( )
try:
@@ -3281,7 +3364,7 @@ class MonitorWidgets:
new_data_struct.register_listener( self.datastructures_listener )
self.displayed_data[ 1 ] = new_data_struct
if self.update_counter > 10:
print("update queue too long (%d>10): skipping one update" % self.update_counter)
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
else:
gdk.threads_enter( )
try:
@@ -3322,7 +3405,7 @@ class MonitorWidgets:
self.update_counter_lock.release( )
# print "update display", self.update_counter
if self.update_counter > 10:
print("update queue too long (%d>10): skipping one update" % self.update_counter)
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
return
if self.displayed_data[ 0 ] is None or subject != self.displayed_data[ 0 ]:
return
@@ -3750,7 +3833,7 @@ class MonitorWidgets:
moving_average = data_slice = None
if max_points_to_display > 0 and len( xdata ) > max_points_to_display:
print("decimating data to %d points by moving average (prevent crash of matplotlib)" % max_points_to_display)
logger.warning("Decimating data to %d points by moving average (prevent crash of matplotlib)", max_points_to_display)
n = numpy.ceil( len( xdata ) / max_points_to_display )
moving_average = numpy.ones( n, dtype="float" ) / n
data_slice = numpy.array( numpy.floor( numpy.arange( max_points_to_display, dtype="float" ) \
@@ -3975,14 +4058,55 @@ class MonitorWidgets:
class ScriptInterface:
"""
texts or code objects are executed as experiment and result script the backend is started with sufficient arguments
"""Executes experiment and result scripts, coordinating with the DAMARIS backend.
Manages the lifecycle of experiment and result script execution. Depending
on whether a backend executable is provided, it either uses
:class:`BackendDriver.BackendDriver` to spawn an external process or falls
back to in-process :class:`ExperimentWriter` and :class:`ResultReader`
components. A shared :class:`DataPool` is used for communication between
scripts and the GUI.
Args:
exp_script (str or None): Python source code for the experiment script.
res_script (str or None): Python source code for the result script.
backend_executable (str or None): Path to the DAMARIS backend executable.
If ``None``, in-process execution is used.
spool_dir (str): Directory for inter-process communication spool files.
Defaults to ``"spool"``.
clear_jobs (bool): Clear pending jobs on startup. Defaults to ``True``.
clear_results (bool): Clear pending results on startup. Defaults to ``True``.
log_callback (callable or None): Optional callback for log messages.
Attributes:
data (:class:`damaris.data.DataPool`): Shared data pool for
experiment/result communication.
exp_handling (:class:`ExperimentHandling.ExperimentHandling` or None):
The running experiment handler thread.
res_handling (:class:`ResultHandling.ResultHandling` or None):
The running result handler thread.
back_driver (:class:`BackendDriver.BackendDriver` or None):
The backend process driver, if a backend executable was provided.
Public Methods:
runScripts: Start experiment, result, and backend handlers.
"""
def __init__( self, exp_script=None, res_script=None, backend_executable=None, spool_dir="spool", clear_jobs=True,
clear_results=True ):
"""
run experiment scripts and result scripts
clear_results=True, log_callback=None ):
"""Initialize the script execution environment.
Sets up the data pool, experiment writer, result reader, and optional
backend driver based on the provided configuration.
Args:
exp_script (str or None): Python source code for the experiment script.
res_script (str or None): Python source code for the result script.
backend_executable (str or None): Path to the DAMARIS backend executable.
spool_dir (str): Directory for inter-process communication spool files.
clear_jobs (bool): Clear pending jobs on startup.
clear_results (bool): Clear pending results on startup.
log_callback (callable or None): Optional callback for log messages.
"""
self.exp_script = exp_script
@@ -3992,6 +4116,7 @@ class ScriptInterface:
self.clear_jobs = clear_jobs
self.clear_results = clear_results
self.exp_handling = self.res_handling = None
self._log_callback = log_callback
self.exp_writer = self.res_reader = self.back_driver = None
if self.backend_executable is not None and self.backend_executable != "":
@@ -4016,13 +4141,20 @@ class ScriptInterface:
self.data = DataPool( )
def runScripts( self ):
"""Start experiment, result, and backend handler threads.
Creates and starts :class:`ExperimentHandling` and
:class:`ResultHandling` threads, and optionally starts the backend
driver process. Waits for the backend to report a valid PID before
proceeding.
"""
try:
# get script engines
self.exp_handling = self.res_handling = None
if self.exp_script and self.exp_writer:
self.exp_handling = ExperimentHandling.ExperimentHandling( self.exp_script, self.exp_writer, self.data )
self.exp_handling = ExperimentHandling.ExperimentHandling( self.exp_script, self.exp_writer, self.data, self._log_callback )
if self.res_script and self.res_reader:
self.res_handling = ResultHandling.ResultHandling( self.res_script, self.res_reader, self.data )
self.res_handling = ResultHandling.ResultHandling( self.res_script, self.res_reader, self.data, self._log_callback )
# start them
if self.back_driver is not None:
@@ -4031,7 +4163,7 @@ class ScriptInterface:
(self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)):
while gtk.events_pending():
gtk.main_iteration_do(False)
self.back_driver.quit_flag.wait( 0.1 )
self.back_driver.quit_flag.wait( 0.05 )
if self.exp_handling:
self.exp_handling.start( )
if self.res_handling: