Source code for pygeostat.data.h5_io

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
h5_io.py: Contains input/output functions for using HDF5 files within pygeostat
'''
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
import os
import numpy as np
import pandas as pd
import textwrap

from ..utility.logging import printerr


def _fixh5path(h5path):
    """Fix an h5path for use in h5py file objects"""
    if h5path in [None, '', ' ', '/']:
        h5path = '/'
    if h5path[0] != '/':
        h5path = '/' + h5path
    if h5path[-1] != '/':
        h5path = h5path + '/'

    return h5path


[docs] def h5_combine_data(flname, h5paths, datasets=None): """ Combine data into one DataFrame from multiple paths in a HDF5 file. Parameters: flname (str): Path of the HDF5 you wish to read from h5paths (list): A list of h5paths to combine. Forward slash (/) delimited path through the group hierarchy you wish to place the dataset(s) specified by the argument ``datasets`` into. The dataset name cannot be passed using this argument, it is interpreted as a group name. A value of ``None`` places the dataset into the root directory of the HDF5 file. datasets (list of lists): If only a specific set of datasets from each path are desired then pass a list of lists of equal length as the h5paths list. An empty list within the list will cause all datasets in the corresponding path to be readin. Returns: DataFrame Example: >>> flname = 'drilldata.h5' ... h5paths = ['/Orig_data/series4870/', 'NS/Declus/series4870/'] ... datasets = [['LOCATIONX', 'LOCATIONY', 'LOCATIONZ'], []] ... data = gs.h5_combine_data(flname, h5paths, datasets=datasets) """ import h5py # Handle some default parameters if isinstance(h5paths, list): if len(h5paths) > 1: for path in h5paths: if path in [None, True, '', ' ', '/']: path = '/' else: printerr("Multiple paths were not passed. If you are not combining data across paths" "use `gs.read_h5()`", errtype='error') else: printerr("Multiple paths were not passed. If you are not combining data across paths use" " `gs.read_h5()`", errtype='error') # Sanity checks if not h5py.is_hdf5(flname): raise IOError("The passed file path is not a valid HDF5 file.") for idx, path in enumerate(h5paths): if len(datasets[idx]) > 0: if idx == 0: data = read_h5(flname=flname, h5path=path, datasets=datasets[idx]) else: data2 = read_h5(flname=flname, h5path=path, datasets=datasets[idx]) data = pd.concat([data, data2], axis=1) else: if idx == 0: data = read_h5(flname=flname, h5path=path) else: data2 = read_h5(flname=flname, h5path=path) data = pd.concat([data, data2], axis=1) return data
[docs] def write_h5(data, flname, h5path=None, datasets=None, dtype=None, gridstr=None, trim_variable=None, var_min=-998.0): """Write data to an HDF5 file using the python package H5PY. The file is appended to and in the case that a dataset already exists, it is overwritten. Parameters: data: A 1-D np.array/pd.Series or a ``pd.DataFrame`` containing different variables as columns flname (str): Path of the HDF5 you wish to write to or create h5path (str): Forward slash (/) delimited path through the group hierarchy you wish to place the dataset(s) specified by the argument ``datasets`` into. The dataset name cannot be passed using this argument, it is interpreted as a group name. A value of ``None`` places the dataset into the root directory of the HDF5 file. datasets (str or list): Name of the dataset(s) to write out. If a ``pd.DataFrame`` is passed, the values passed by the argument ``datasets`` must match the DataFrame's columns. dtype (str): The data type to write. Currently, only the following values are permitted: ``['int32', 'float32', 'float64']``. If a ``pd.DataFrame`` is passed and this argument is left to it's default value of ``None``, the DataFrame's dtypes must be of the types listed above. gridstr (str): Grid definition string that is saved to the HDF5 file as an attribute of the group defined by the parameter ``h5path``. trim_variable (str): Variable to use for trimming the data. An index will be written to the h5file and will be used to rebuild dataset while only nontrimmed data will be written out var_min (float): minimum trimming limit usedif trim_variable is passed Examples: Write a single pd.Series or np.array to an HDF5 file: >>> gs.write_h5(array, 'file.h5', h5path='Modeled/Var1', datasets='Realization_0001') Write a whole ``pd.DataFrame`` in group (folder) 'OriginalData' that contains a dataset for every column in the ``pd.DataFrame``: >>> gs.write_h5('file.h5', DataFrame, h5path='OriginalData') """ import h5py # Sanity checks if not isinstance(data, (np.ndarray, pd.Series, pd.DataFrame)): raise TypeError("The data passed must be a np.ndarray, pd.Series, or pd.DataFrame") if isinstance(data, (np.ndarray, pd.Series)) and ((isinstance(datasets, list)) and (len(datasets) > 1)): raise TypeError("The passed data is 1-D yet more than one variable is specified") # Sort out some defaults if h5path in [None, '', ' ', '/']: h5path = '/' if datasets is None: if isinstance(data, pd.DataFrame): datasets = list(data.columns) elif isinstance(data, pd.Series): datasets = [data.name] else: datasets = ['data'] else: if not isinstance(datasets, list): datasets = [datasets] # More sanity checks if isinstance(data, pd.DataFrame) and datasets is not None: errcols = set(datasets) - set(data.columns) if len(errcols) > 0: raise KeyError("The following datasets cannot be found in the pd.DataFrame: %s" % errcols) # Open up the h5 file with h5py.File(flname, 'w') as h5file: # Create the groups if h5path == '/': group = h5file else: group = h5file.require_group(h5path) trim = False if trim_variable: if isinstance(data, pd.DataFrame): array = data[trim_variable] trim = True elif isinstance(data, pd.Series): array = data trim = True else: printerr('Data needs to be either pd.DataFrame, or pd.Series to trim' 'Data will be written out untrimmed', errtype='warning') if trim: h5_index = array.index[array > var_min].values.astype('int32') # Make sure the last cell is always included last_index = len(array) - 1 if h5_index.max() < last_index: h5_index = np.append(h5_index, [last_index]) array = np.atleast_2d(h5_index).T group.require_dataset(name='h5_index', data=array, shape=array.shape, dtype='int32') # Iterate over the data if required and write it to the HDF5 data for dataset in datasets: if isinstance(data, pd.DataFrame): array = data[[dataset]] dtype = str(array.dtypes[0]) else: array = data if trim: array = array.loc[h5_index] # Determine default data type if required if dtype is None: dtype = str(array.dtype) if dtype == 'int64' or dtype == 'int32' or dtype == 'float32' or dtype == 'float64': if dtype == 'int64': printerr("The data type 'int64' is not supported by the Fortran module" " hdf5_io. The data will be written as 'int32'.", errtype='warning') array = array.astype('int32') array = np.atleast_2d(array).T group.require_dataset(name=dataset, data=array, shape=array.shape, dtype=dtype) else: raise NotImplementedError("The data type `%s` is not supported by the Fortran" " module hdf5_io. Supported data types: ['int32'," "'float32', 'float64']" % dtype) # Write the griddef if required if isinstance(gridstr, bytes): h5file[h5path].attrs['griddef'] = '%s' % gridstr
[docs] def read_h5(flname, h5path=None, datasets=None, fill_value=-999): """ Return a 1-D array from an HDF5 file or build a ``pd.DataFrame()`` from a list of datasets in a single group. The argument ``h5path`` must be a path to a group. If 1 or more specific variables are desired to be loaded, pass a list to ``datasets`` to specify which to read. Parameters: flname (str): Path of the HDF5 you wish to write to or create h5path (str): Forward slash (/) delimited path through the group hierarchy you wish to read the dataset(s) specified by the argument ``datasets`` from. The dataset name cannot be passed using this argument, it is interpreted as a group name only. A value of ``None`` places the dataset into the root directory of the HDF5 file. A value of ``False`` loads a blank pd.DataFrame(). datasets (str or list): Name of the dataset(s) to read from the group specified by ``h5path``. Does nothing if ``h5path`` points to a dataset. fill_value (float or np.NaN): value to fill in grid with if trimmed data was written out. default is -999 Returns: data (pd.DataFrame): DataFrame containing one or more columns, each containing a single 1-D array of a variable. """ import h5py # Handle some default parameters trim = False if h5path in [None, True, '', ' ', '/']: h5path = '/' elif h5path is False: return pd.DataFrame() else: h5path = '/' + h5path if isinstance(datasets, str): datasets = [datasets] # Sanity checks if not h5py.is_hdf5(flname): raise IOError("The passed file path is not a valid HDF5 file.") # Open the file and read the data with h5py.File(flname, 'r') as h5store: # Sort out some defaults and make sure all the datasets required are present if isinstance(h5store[h5path], h5py.Group): group = True h5dsets = [x for x in list(h5store[h5path].keys()) if isinstance(h5store['%s/%s' % (h5path, x)], h5py.Dataset)] if 'h5_index' in h5dsets: trim = True h5dsets.remove('h5_index') if datasets is None: datasets = h5dsets else: error = set(datasets) - set(h5dsets) if len(error) > 0: raise KeyError("The following datasets cannot be found in h5path specified: %s" % error) elif isinstance(h5store[h5path], h5py.Dataset): group = False if h5path.endswith('/'): h5path = h5path[:-1] datasets = [h5path.rsplit('/', 1)[1]] if 'h5_index' in datasets: trim = True datasets.remove('h5_index') else: raise ValueError("The `h5path` does not point to a dataset or a group.") # Read the data data = [] if trim: if group: path = h5path + '/h5_index' else: path = '/h5_index' # python h5_index = h5store[path][:].flatten() for dataset in datasets: if group: path = h5path + '/' + dataset else: path = h5path # python data.append(h5store[path][:].flatten()) if len(data) > 0: data = np.column_stack(data) if trim: data = pd.DataFrame(data, columns=datasets, index=h5_index) data = data.reindex(index=range(h5_index.max() + 1), fill_value=fill_value) else: data = pd.DataFrame(data, columns=datasets) else: printerr("No data was found within the root directory. A value of `None` has been" " returned", errtype='warning') data = None return data
[docs] def ish5dataset(h5fl, dataset, h5path=None): """ Check to see if a dataset exits within an HDF5 file The argument ``h5path`` must be a path to a group and cannot contain the dataset name. Can only check for one dataset at a time. Parameters: flname (str): Path of the HDF5 you wish to check h5path (str): Forward slash (/) delimited path through the group hierarchy you wish to check for the specified dataset. The dataset name cannot be passed using this argument, it is interpreted as a group name only. A value of ``None`` places the dataset into the root directory of the HDF5 file. dataset (str): Name of the dataset to check for in the group specified by ``h5path``. Returns: exists (bool): Indicator if the specified dataset exists """ import h5py # Handle some default parameters if h5path in [None, True, '', ' ', '/']: h5path = '/' else: h5path = '/' + h5path + '/' h5path = h5path + dataset # Sanity checks if os.path.isfile(h5fl): if not h5py.is_hdf5(h5fl): raise IOError("The passed file exists but s not a valid HDF5 file.") # Check for the dataset with h5py.File(h5fl, 'r') as h5store: if dataset in [x for x in h5store.keys()]: exists = isinstance(h5store[h5path], h5py.Dataset) else: exists = False else: exists = False return exists
[docs] class H5Store: """ A simple class within pygeostat to manage and use HDF5 files. :ivar str flname: Path to a HDF5 file to create or use :ivar ``h5py.File`` h5data: h5py File object :ivar dict paths: Dictionary containing all of the groups found in the HDF5 file that contain datasets Parameters: flname (str): Path to a HDF5 file to create or use Usage: Write a np.array or pd.Series to the HDF5 file: >>> H5Store['Group1/Group2/Var1'] = np.array() Write all the columns in a ``pd.DataFrame`` to the HDF5 file: >>> H5Store['Group1/Group2'] = pd.DataFrame() Retrieve a single 1-D array: >>> array = H5Store['Group1/Group2/Var1'] Retrieve a single 1-D array within the root directory of the HDF5 file: >>> array = H5Store['Var1'] Retrieve the first value from the array: >>> value = H5Store['Var1', 0] Retrieve a slice of values from the array: >>> values = H5Store['Var1', 10:15] """ import h5py def __init__(self, flname, replace=False): self.flname = flname if self.h5py.is_hdf5(flname): if replace: os.remove(flname) self.h5data = None self.paths = None else: self._loadfile() else: self.h5data = None self.paths = None
[docs] def __str__(self): """ Print a nice list of groups and the datasets found within them using the variable ``self.paths``. Example: Print any groups found within the HDF5 file and the datasets within: >>> print(H5Store) """ printout = (' ' * 21) + 'Groups and datasets found in HDF5 file\n' + ('-' * 80) + '\n' for group in sorted(self.paths): printout = printout + ('Group Path: \'%s\'' % group) + '\n' printout = printout + textwrap.fill('Datasets: %s' % self.paths[group], width=80, subsequent_indent=' ') + '\n' printout = printout + ('-' * 80) + '\n' return printout
[docs] def __getitem__(self, key): """ Retrieve an array using the self[key] notation. The passed key is the path used to access the array desired and included direction through groups if required and the dataset name. The array may be selectively queried allowing a specific value or range of values to be loaded into the systems memory and not the whole array. Example: Retrieve a single 1-D array: >>> array = H5Store['Group1/Group2/Var1'] Retrieve a single 1-D array within the root directory of the HDF5 file: >>> array = H5Store['Var1'] Retrieve the first value from the array: >>> value = H5Store['Var1', 0] Retrieve a slice of values from the array: >>> values = H5Store['Var1', 10:15] """ # Manage the input rng = None if isinstance(key, tuple): key, rng = key if key in ['', ' ']: key = '/' if not key.endswith("/"): key += "/" # Sanity checks if self.h5data is None: raise FileNotFoundError("No HDF5 file exists yet") if rng is not None: if not isinstance(self.h5data[key], self.h5py.Dataset): raise KeyError("The passed key must lead to a H5 dataset if a slice is desired") # Load the full group or just a single array if isinstance(self.h5data[key], self.h5py.Group): datasets = [x for x in list(self.h5data[key].keys()) if isinstance(self.h5data['%s%s' % (key, x)], self.h5py.Dataset)] data = [] for dataset in datasets: data.append(self.h5data['%s/%s' % (key, dataset)][:].flatten()) return pd.DataFrame(np.column_stack(data), columns=datasets) elif rng is not None: return self.h5data[key][rng] else: return self.h5data[key][:]
[docs] def __setitem__(self, key, value): """ Write the the HDF5 file using the self[key] notation. If a pd.Series or np.array is passed, the last entry in the path is used as the dataset name. If a ``pd.DataFrame`` is passed, all columns are written to the path specified to datasets with their names retrieved from the ``pd.DataFrame``'s columns. If more flexible usage is required, please use :func:`gs.write_h5()<pygeostat.data.h5_io.write_h5>`. Example: Write a np.array or pd.Series to the HDF5 file: >>> H5Store['Group1/Group2/Var1'] = np.array() Write all the columns in a ``pd.DataFrame`` to the HDF5 file: >>> H5Store['Group1/Group2'] = pd.DataFrame() """ # Handle different types of input objects if isinstance(value, pd.DataFrame): h5path = key variables = None elif isinstance(value, pd.Series): h5path = key variables = [value.name] else: key = key.rsplit('/', 1) if len(key) > 1: h5path, variables = key else: h5path = None variables = key[0] # Write the data to the HDF5 file write_h5(value, self.flname, h5path, variables) # Reload the file if isinstance(self.h5data, self.h5py.File): self.h5data.close() self._loadfile()
def __enter__(self): " required for `with gs.H5Store(file) as h5store:` pattern " return self def __exit__(self, type, value, traceback): " required for `with gs.H5Store(file) as h5store:` pattern " self.close() def _loadfile(self): """ Load the HDF5 file as h5py object and get the dataset paths """ self.h5data = self.h5py.File(self.flname, 'r+') self.paths = self._get_paths() def _get_paths(self): """ Build lookup dictionary of all the paths to the datasets found in a HDF5 file. """ done = False paths = {} groups = ['/'] while not done: for group in groups: children = ['%s%s/' % (group, x)for x in list(self.h5data[group].keys()) if isinstance(self.h5data['%s%s' % (group, x)], self.h5py.Group)] groups.extend(children) datasets = [x for x in list(self.h5data[group].keys()) if isinstance(self.h5data['%s%s' % (group, x)], self.h5py.Dataset)] if len(datasets) > 0: paths[group] = datasets groups.remove(group) if len(groups) == 0: done = True return paths
[docs] def close(self): """ Release the open HDF5 file from python. """ try: self.h5data.close() except: print("The H5 file wasn't open")
[docs] def datasets(self, h5path=None): """ Return the datasets found in the specified group. Keyword Arguments: h5path (str): Forward slash (/) delimited path through the group hierarchy you wish to retrieve the lists of datasets from. A dataset name cannot be passed using this argument, it is interpreted as a group name. A value of ``None`` places the dataset into the root directory of the HDF5 file. Returns: datasets (list): List of the datasets found within the specified `h5path` """ h5path = _fixh5path(h5path) return self.paths[h5path]
[docs] def iteritems(self, h5path=None, datasets=None, wildcard=None): """ Produces an iterator that can be used to iterate over HDF5 datasets. Can use the parameter ``h5path`` to indicate which group to retrieve the datasets from. If a set of specific datasets are required, the parameter ``datasets`` will restrict the iterator to those. The parameter ``wildcard`` allows a string wild-card value to restrict which datasets are iterated over. Keyword Arguments: h5path (str): Forward slash (/) delimited path through the group hierarchy you wish to retrieve datasets from. A dataset name cannot be passed using this argument, it is interpreted as a group name. A value of ``None`` places the dataset into the root directory of the HDF5 file. datasets (list): List of specific dataset names found within the specified group to iterator over wildcard (str): String to search for within the names of the datasets found within the specified group to iterate over Examples: Load a HDF5 file to pygeostat: >>> data = gs.H5Store('data.h5') Iterate over all datasets within the root directory of a HDF5 file: >>> for dataset in data.iteritems(): >>> gs.histplt(dataset) Iterate over the datasets within a specific group that are realizations: >>> for dataset in data.iteritems(h5path='Simulation/NS_AU', wildcard='Realization'): >>> gs.histplt(dataset) """ # Handle some parameters h5path = _fixh5path(h5path) if isinstance(datasets, str): datasets = [datasets] # Sanity checks if datasets and wildcard: raise ValueError("Only `datasets` or `wildcard` can be passed") if datasets: error = set(datasets) - set(self.paths[h5path]) if len(error) > 0: raise ValueError("The following datasets were specified but not found in the H5" " group: %s" % error) elif wildcard: datasets = [] for dataset in self.paths[h5path]: if wildcard in dataset: datasets.append(dataset) if len(datasets) == 0: raise ValueError("No datasets with the specified `wildcard` were found") else: datasets = self.paths[h5path] if len(datasets) == 0: raise ValueError("No datasets were found in the specified `h5path`") # Produce the generator for dataset in datasets: yield self.__getitem__(dataset)