diff --git a/src/damaris/data/DataPool.py b/src/damaris/data/DataPool.py
index e5e8c5a..9fd436f 100644
--- a/src/damaris/data/DataPool.py
+++ b/src/damaris/data/DataPool.py
@@ -7,6 +7,7 @@ import collections
import threading
import traceback
import io
+import numpy
class DataPool(collections.abc.MutableMapping):
"""
@@ -107,7 +108,7 @@ class DataPool(collections.abc.MutableMapping):
self.__registered_listeners=None
def write_hdf5(self,hdffile,where="/",name="data_pool", complib=None, complevel=None):
- if type(hdffile) is bytes:
+ if isinstance(hdffile, (bytes, str)):
dump_file=tables.open_file(hdffile, mode="a")
elif isinstance(hdffile,tables.File):
dump_file=hdffile
@@ -156,7 +157,7 @@ class DataPool(collections.abc.MutableMapping):
value=self.__mydict[key]
self.__dictlock.release()
# now write data, assuming, the object is constant during write operation
- if "write_to_hdf" in dir(value):
+ if hasattr(value, "write_to_hdf"):
try:
value.write_to_hdf(hdffile=dump_file,
where=dump_dir,
@@ -170,16 +171,255 @@ class DataPool(collections.abc.MutableMapping):
traceback.print_tb(sys.exc_info()[2], None, traceback_file)
print("detailed traceback: %s\n"%str(e)+traceback_file.getvalue())
traceback_file=None
+ elif isinstance(value, (numpy.ndarray, int, float, str, bytes)):
+ try:
+ obj_to_save = value
+ if isinstance(value, str):
+ obj_to_save = value.encode('utf-8')
+ dump_file.create_array(dump_dir,
+ name=group_keyname,
+ obj=obj_to_save,
+ title=key)
+ except Exception as e:
+ print("failed to write data_pool[\"%s\"]: %s"%(key,str(e)))
else:
- print("don't know how to store data_pool[\"%s\"]"%key)
+ print("don't know how to store data_pool[\"%s\"] (type: %s)"%(key, type(value)))
value=None
finally:
dump_group=None
- if type(hdffile) is bytes:
+ if isinstance(hdffile, (bytes, str)):
dump_file.close()
dump_file=None
+ def read_from_hdf(self, hdffile, where="/data_pool"):
+ """
+ Read data from HDF5 file and populate the DataPool.
+
+ Parameters:
+ - hdffile: HDF5 file object or filename (bytes/str)
+ - where: Location within HDF5 file to read from (default: "/data_pool")
+ """
+ # Handle both filename and file object
+ if type(hdffile) is bytes or isinstance(hdffile, str):
+ hdf_file = tables.open_file(hdffile, mode="r")
+ close_file = True
+ elif isinstance(hdffile, tables.File):
+ hdf_file = hdffile
+ close_file = False
+ else:
+ raise Exception("expecting hdffile or string")
+
+ try:
+ # Navigate to the data pool group
+ if where not in hdf_file:
+ raise Exception(f"HDF5 location '{where}' not found")
+
+ data_pool_group = hdf_file.get_node(where)
+
+ # Recursively process the group structure
+ self._read_hdf_group(data_pool_group, "")
+
+ finally:
+ if close_file:
+ hdf_file.close()
+
+ def _read_hdf_group(self, group, path_prefix):
+ """
+ Recursively read HDF5 group and populate DataPool.
+
+ Parameters:
+ - group: HDF5 group object
+ - path_prefix: Current path in DataPool hierarchy
+ """
+ # Process all nodes in this group
+ for node in group:
+ # Get the node name (last part of the path)
+ node_name = node._v_name
+
+ # Try to get the original name from title
+ title = node._v_title
+
+ if title:
+ # For DAMARIS objects, the title is usually the full path
+ # For intermediate groups, it's just the part name
+ if "/" in title:
+ full_path = title
+ else:
+ if path_prefix:
+ full_path = f"{path_prefix}/{title}"
+ else:
+ full_path = title
+ else:
+ # Strip dir_ and dict_ prefixes from node name
+ clean_name = self._clean_hdf_name(node_name)
+
+ # Build the full path in DataPool hierarchy
+ if path_prefix:
+ full_path = f"{path_prefix}/{clean_name}"
+ else:
+ full_path = clean_name
+
+ full_path = full_path.replace("//", "/")
+
+ if isinstance(node, (tables.Group, tables.Table)):
+ # Check if this is a known DAMARIS object type
+ if hasattr(node._v_attrs, 'damaris_type'):
+ damaris_type = node._v_attrs.damaris_type
+
+ # Handle different DAMARIS object types
+ if damaris_type == "Accumulation":
+ from damaris.data.Accumulation import read_from_hdf
+ obj = read_from_hdf(node)
+ if obj is not None:
+ self[full_path] = obj
+
+ elif damaris_type == "ADC_Result":
+ from damaris.data.ADC_Result import read_from_hdf
+ obj = read_from_hdf(node)
+ if obj is not None:
+ self[full_path] = obj
+
+ elif damaris_type == "MeasurementResult":
+ from damaris.data.MeasurementResult import read_from_hdf
+ obj = read_from_hdf(node)
+ if obj is not None:
+ self[full_path] = obj
+
+ # Skip further processing of this group since we handled it
+ continue
+
+ # Recursively process sub-groups
+ if isinstance(node, tables.Group):
+ self._read_hdf_group(node, full_path)
+
+ elif isinstance(node, (tables.Array, tables.CArray, tables.EArray, tables.Table)):
+ # Handle simple array data
+ try:
+ data = node.read()
+ # If it's a scalar array of bytes, decode it to string
+ if hasattr(data, 'decode'):
+ try:
+ data = data.decode('utf-8')
+ except:
+ pass
+ elif isinstance(data, numpy.ndarray) and data.dtype.kind in ('S', 'V'):
+ # For numpy byte arrays, try to decode if they are scalar or small
+ if data.size == 1:
+ try:
+ decoded = data.item().decode('utf-8')
+ data = decoded
+ except:
+ pass
+ self[full_path] = data
+ except Exception as e:
+ print(f"Warning: Could not read array {full_path}: {e}")
+
+ # Note: Other node types (Table, etc.) would be handled here as needed
+
+ def _clean_hdf_name(self, name):
+ """
+ Clean HDF5 node name by removing dir_ and dict_ prefixes.
+
+ Parameters:
+ - name: HDF5 node name (may have dir_ or dict_ prefix)
+
+ Returns:
+ - Cleaned name with prefixes removed
+ """
+ if name.startswith("dir_"):
+ return name[4:] # Remove "dir_" prefix
+ elif name.startswith("dict_"):
+ return name[5:] # Remove "dict_" prefix
+ else:
+ return name
+
+ @classmethod
+ def load_hdf5(cls, filename):
+ """
+ Load a DAMARIS HDF5 file and return DataPool and metadata.
+
+ Parameters:
+ - filename: Path to HDF5 file
+
+ Returns:
+ - tuple: (DataPool instance, metadata dict)
+
+ metadata dict contains:
+ - 'scripts': dict with experiment scripts and spool info
+ - 'log': log data if available
+ - 'timeline': timeline data if available
+ """
+ # Validate file
+ if not tables.is_pytables_file(filename):
+ raise Exception(f"File {filename} is not a valid PyTables HDF5 file")
+
+ # Create DataPool and load data
+ data_pool = cls()
+
+ try:
+ hdf_file = tables.open_file(filename, mode="r")
+
+ # Load main data pool
+ if "/data_pool" in hdf_file:
+ data_pool.read_from_hdf(hdf_file, where="/data_pool")
+
+ # Extract metadata
+ metadata = {}
+
+ # Load scripts
+ if "/scripts" in hdf_file:
+ scripts_group = hdf_file.get_node("/scripts")
+ scripts_data = {}
+ # Use _v_children to get node names safely
+ if hasattr(scripts_group, '_v_children'):
+ for node_name, node in scripts_group._v_children.items():
+ try:
+ if hasattr(node, 'read'):
+ data = node.read()
+ # Handle numpy string arrays and other types
+ if hasattr(data, 'decode'):
+ # bytes or numpy bytes
+ scripts_data[node_name] = data.decode('utf-8', errors='replace')
+ elif hasattr(data, 'tolist'):
+ # numpy array
+ scripts_data[node_name] = str(data.tolist())
+ elif isinstance(data, (str, bytes)):
+ # string or bytes
+ if isinstance(data, bytes):
+ scripts_data[node_name] = data.decode('utf-8', errors='replace')
+ else:
+ scripts_data[node_name] = data
+ else:
+ scripts_data[node_name] = str(data)
+ except Exception as e:
+ print(f"Warning: Could not read script {node_name}: {e}")
+ metadata['scripts'] = scripts_data
+
+ # Load log
+ if "/log" in hdf_file:
+ try:
+ log_node = hdf_file.get_node("/log")
+ metadata['log'] = log_node.read()
+ except Exception as e:
+ print(f"Warning: Could not read log: {e}")
+
+ # Load timeline
+ if "/timeline" in hdf_file:
+ try:
+ timeline_node = hdf_file.get_node("/timeline")
+ metadata['timeline'] = timeline_node.read()
+ except Exception as e:
+ print(f"Warning: Could not read timeline: {e}")
+
+ return data_pool, metadata
+
+ except Exception as e:
+ raise Exception(f"Failed to load HDF5 file {filename}: {e}")
+ finally:
+ if 'hdf_file' in locals():
+ hdf_file.close()
+
def register_listener(self, listening_function):
self.__registered_listeners.append(listening_function)
diff --git a/src/damaris/gui/DamarisGUI.py b/src/damaris/gui/DamarisGUI.py
index eb3d2d3..abe73e0 100644
--- a/src/damaris/gui/DamarisGUI.py
+++ b/src/damaris/gui/DamarisGUI.py
@@ -169,7 +169,7 @@ class DamarisGUI:
self.log = LogWindow( self.xml_gui , self)
- self.sw = ScriptWidgets( self.xml_gui )
+ self.sw = ScriptWidgets( self.xml_gui, self )
self.toolbar_init( )
@@ -218,6 +218,7 @@ class DamarisGUI:
"on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event,
"on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event,
"on_toolbar_open_file_button_clicked": self.sw.open_file,
+ "on_toolbar_open_hdf5_button_clicked": self.sw.open_hdf5,
"on_toolbar_new_button_clicked": self.sw.new_file,
"on_toolbar_save_as_button_clicked": self.sw.save_file_as,
"on_toolbar_save_file_button_clicked": self.sw.save_file,
@@ -297,6 +298,14 @@ class DamarisGUI:
self.config = None
self.xml_gui = None
+ def observe_data_pool( self, data_pool ):
+ """
+ Delegate to monitor widgets
+ """
+ self.data = data_pool
+ if hasattr(self, "monitor"):
+ self.monitor.observe_data_pool(data_pool)
+
# event handling: the real acitons in gui programming
# first global events
@@ -1103,11 +1112,12 @@ class LogWindow:
class ScriptWidgets:
- def __init__( self, xml_gui ):
+ def __init__( self, xml_gui, outer_space ):
"""
initialize text widgets with text
"""
self.xml_gui = xml_gui
+ self.outer_space = outer_space
# my states
# editing enabled/disabled
@@ -1572,7 +1582,7 @@ get_ylabel set_ylabel"""
elif response == gtk.ResponseType.CANCEL:
return 0
- def response( self, response_id, script_widget ):
+ def response( dialog, response_id, script_widget ):
if response_id == gtk.ResponseType.OK:
file_name = dialog.get_filename( )
if file_name is None:
@@ -1580,7 +1590,13 @@ get_ylabel set_ylabel"""
script_filename = os.path.abspath( file_name )
if not os.access( script_filename, os.R_OK ):
- self.outer_space.show_error_dialog( "File I/O Error", "Cannot read from file %s" % script_filename )
+ error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
+ type=gtk.MessageType.ERROR,
+ flags=gtk.DialogFlags.MODAL,
+ buttons=("_OK", gtk.ResponseType.OK))
+ error_dialog.set_markup("Cannot read from file %s" % script_filename)
+ error_dialog.run()
+ error_dialog.destroy()
return True
script_string = script_widget.load_file_as_unicode( script_filename )
@@ -1613,6 +1629,103 @@ get_ylabel set_ylabel"""
return True
+ def open_hdf5( self, widget, Data=None ):
+ """
+ Open and load an HDF5 file written by DAMARIS.
+ """
+ # ignore
+ if not self.editing_state:
+ return 0
+
+ parent_window = self.xml_gui.get_object( "main_window" )
+ dialog = gtk.FileChooserDialog( title="Open HDF5 File...",
+ parent=parent_window,
+ action=gtk.FileChooserAction.OPEN,
+ buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
+ )
+ dialog.set_default_response( gtk.ResponseType.OK )
+ dialog.set_select_multiple( False )
+
+ # Add filter for HDF5 files
+ filter_hdf5 = gtk.FileFilter()
+ filter_hdf5.set_name("HDF5 files")
+ filter_hdf5.add_pattern("*.h5")
+ filter_hdf5.add_pattern("*.hdf5")
+ filter_hdf5.add_pattern("*.HDF5")
+ filter_hdf5.add_pattern("*.H5")
+ dialog.add_filter(filter_hdf5)
+
+ # Also allow all files
+ filter_all = gtk.FileFilter()
+ filter_all.set_name("All files")
+ filter_all.add_pattern("*")
+ dialog.add_filter(filter_all)
+
+ def response( dialog, response_id, script_widget ):
+ if response_id == gtk.ResponseType.OK:
+ file_name = dialog.get_filename( )
+ if file_name is None:
+ return
+
+ file_name = os.path.abspath( file_name )
+ if not os.access( file_name, os.R_OK ):
+ error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
+ type=gtk.MessageType.ERROR,
+ flags=gtk.DialogFlags.MODAL,
+ buttons=("_OK", gtk.ResponseType.OK))
+ error_dialog.set_markup("Cannot read from file %s" % file_name)
+ error_dialog.run()
+ error_dialog.destroy()
+ return True
+
+ try:
+ # Load DataPool and metadata from HDF5
+ data_pool, metadata = DataPool.load_hdf5(file_name)
+
+ # Set the loaded data pool in the outer space (main application)
+ if hasattr(script_widget.outer_space, 'observe_data_pool'):
+ script_widget.outer_space.observe_data_pool(data_pool)
+ elif hasattr(script_widget.outer_space, 'monitor'):
+ # Fallback if delegate not present
+ script_widget.outer_space.data = data_pool
+ script_widget.outer_space.monitor.observe_data_pool(data_pool)
+
+ # If there are scripts in metadata, load them
+ if 'scripts' in metadata:
+ scripts = metadata['scripts']
+ exp_script = scripts.get('experiment_script', None)
+ res_script = scripts.get('result_script', None)
+ if exp_script or res_script:
+ script_widget.set_scripts(exp_script, res_script)
+
+ # Show success message
+ success_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
+ type=gtk.MessageType.INFO,
+ flags=gtk.DialogFlags.MODAL,
+ buttons=("_OK", gtk.ResponseType.OK))
+ success_dialog.set_markup("Successfully loaded data from %s" % os.path.basename(file_name))
+ success_dialog.run()
+ success_dialog.destroy()
+
+ except Exception as e:
+ error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"),
+ type=gtk.MessageType.ERROR,
+ flags=gtk.DialogFlags.MODAL,
+ buttons=("_OK", gtk.ResponseType.OK))
+ error_dialog.set_markup("Failed to load HDF5 file: %s" % str(e))
+ error_dialog.run()
+ error_dialog.destroy()
+
+ return True
+
+ # Event-Handler for response-signal (when one of the button is pressed)
+ dialog.connect( "response", response, self )
+
+ dialog.run( )
+ dialog.destroy( )
+
+ return True
+
def save_file( self, widget=None, Data=None ):
"""
save file to associated filename
@@ -2411,7 +2524,7 @@ class MonitorWidgets:
def source_list_find_one( self, model, iter, what ):
"""find node in subcategory"""
while iter is not None:
- if model.get( iter, 0 )[ 0 ] == what:
+ if str( model.get( iter, 0 )[ 0 ] ) == what:
return iter
iter = model.iter_next( iter )
return iter
@@ -2482,31 +2595,50 @@ class MonitorWidgets:
register a listener and save reference to data
assume to be in gtk/gdk lock
"""
- if self.data_pool is not None:
- # maybe some extra cleanup needed
- print("ToDo: cleanup widgets")
- if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
- self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
- self.displayed_data[ 1 ] = None
- self.displayed_data = [ None, None ]
- self.data_pool.unregister_listener( self.datapool_listener )
- self.data_pool = None
+ gdk.threads_enter( )
+ try:
+ if self.data_pool is not None:
+ # maybe some extra cleanup needed
+ print("ToDo: cleanup widgets")
+ if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
+ self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
+ self.displayed_data[ 1 ] = None
+ self.displayed_data = [ None, None ]
+ self.data_pool.unregister_listener( self.datapool_listener )
+ self.data_pool = None
+ self.update_counter_lock.acquire( )
+ self.update_counter = 0
+ self.update_counter_lock.release( )
+
+ # display states
self.source_list_reset( )
- self.update_counter_lock.acquire( )
- self.update_counter = 0
- self.update_counter_lock.release( )
+ self.__rescale = True
+ self.displayed_data = [ None, None ]
+ self.display_source_path_label.set_label( "" )
+ self.clear_display( )
- # display states
- self.__rescale = True
- self.displayed_data = [ None, None ]
- self.display_source_path_label.set_label( "" )
- self.clear_display( )
-
- if data_pool is not None:
- # keep track of data
- self.data_pool = data_pool
- self.data_pool.register_listener( self.datapool_listener )
+ if data_pool is not None:
+ # keep track of data
+ self.data_pool = data_pool
+ self.data_pool.register_listener( self.datapool_listener )
+
+ # Populate source list with existing data since the listener
+ # was registered after the data was loaded
+ for key in sorted(data_pool.keys()):
+ if key != "__builtins__": # Skip internal keys
+ self.source_list_add(key)
+
+ # Select first data item if available
+ keys = [k for k in data_pool.keys() if not k.startswith("__")]
+ if keys:
+ keys.sort()
+ first_key = keys[0]
+ first_iter = self.source_list_find(first_key.split("/"))
+ if first_iter:
+ self.display_source_combobox.set_active_iter(first_iter)
+ finally:
+ gdk.threads_leave( )
#################### observing data structures and produce idle events
@@ -2643,7 +2775,7 @@ class MonitorWidgets:
gdk.threads_leave( )
elif event.what == DataPool.Event.destroy:
gdk.threads_enter( )
- self.source_list_reset( 'None' )
+ self.source_list_reset( )
self.displayed_data = [ None, None ]
self.clear_display( )
gdk.threads_leave( )
diff --git a/src/damaris/gui/damaris3.glade b/src/damaris/gui/damaris3.glade
index 42bbde0..a91e401 100644
--- a/src/damaris/gui/damaris3.glade
+++ b/src/damaris/gui/damaris3.glade
@@ -491,6 +491,21 @@ Public License instead of this License.
True
+
+
+
+ False
+ True
+
+