Source code for openpnm.core._mixins

import numpy as np
from openpnm.utils import PrintableList


__all__ = [
    'ParserMixin',
    'LabelMixin',
]


[docs] class ParserMixin: def _parse_indices(self, indices): r""" This private method accepts a list of pores or throats and returns a properly structured Numpy array of indices. Parameters ---------- indices : int or array_like This argument can accept numerous different data types including boolean masks, integers and arrays. Returns ------- A Numpy array of indices. Notes ----- This method should only be called by the method that is actually using the locations, to avoid calling it multiple times. """ if indices is None: indices = np.array([], ndmin=1, dtype=int) locs = np.array(indices, ndmin=1) # If boolean array, convert to indices if locs.dtype == bool: if np.size(locs) == self.Np: locs = self.Ps[locs] elif np.size(locs) == self.Nt: locs = self.Ts[locs] else: raise Exception('Mask of locations must be either ' + 'Np nor Nt long') locs = locs.astype(dtype=int) return locs def _parse_element(self, element, single=False): r""" This private method is used to parse the keyword \'element\' in many of the above methods. Parameters ---------- element : str or List[str] The element argument to check. If is None is recieved, then a list containing both \'pore\' and \'throat\' is returned. single : bool (default is False) When set to True only a single element is allowed and it will also return a string containing the element. Returns ------- When ``single`` is ``False`` (default) a list containing the element(s) is returned. When ``single`` is ``True`` a bare string containing the element is returned. """ if element is None: element = ['pore', 'throat'] # Convert element to a list for subsequent processing if isinstance(element, str): element = [element] # Convert 'pore.prop' and 'throat.prop' into just 'pore' and 'throat' element = [item.split('.', 1)[0] for item in element] # Make sure all are lowercase element = [item.lower() for item in element] # Deal with an plurals element = [item.rsplit('s', maxsplit=1)[0] for item in element] for item in element: if item not in ['pore', 'throat']: raise Exception('All keys must start with either pore or throat') # Remove duplicates if any _ = [element.remove(L) for L in element if element.count(L) > 1] if single: if len(element) > 1: raise Exception('Both elements recieved when single element ' + 'allowed') element = element[0] return element def _parse_labels(self, labels, element): r""" This private method is used for converting \'labels\' to a proper format, including dealing with wildcards (\*). Parameters ---------- labels : str or List[str] The label or list of labels to be parsed. Note that the \* can be used as a wildcard. Returns ------- A list of label strings, with all wildcard matches included if applicable. """ if labels is None: raise Exception('Labels cannot be None') if isinstance(labels, str): labels = [labels] # Parse the labels list parsed_labels = [] for label in labels: # Remove element from label, if present if element in label: label = label.split('.', 1)[-1] # Deal with wildcards if '*' in label: Ls = [L.split('.', 1)[-1] for L in self.labels(element=element)] if label.startswith('*'): temp = [L for L in Ls if L.endswith(label.strip('*'))] if label.endswith('*'): temp = [L for L in Ls if L.startswith(label.strip('*'))] temp = [element+'.'+L for L in temp] elif element+'.'+label in self.keys(): temp = [element+'.'+label] else: temp = [element+'.'+label] parsed_labels.extend(temp) # Remove duplicates if any _ = [parsed_labels.remove(L) for L in parsed_labels if parsed_labels.count(L) > 1] return parsed_labels def _parse_mode(self, mode, allowed=None, single=False): r""" This private method is for checking the \'mode\' used in the calling method. Parameters ---------- mode : str or List[str] The mode(s) to be parsed allowed : List[str] A list containing the allowed modes. This list is defined by the calling method. If any of the received modes are not in the allowed list an exception is raised. single : bool (default is False) Indicates if only a single mode is allowed. If this argument is True than a string is returned rather than a list of strings, which makes it easier to work with in the caller method. Returns ------- A list containing the received modes as strings, checked to ensure they are all within the allowed set (if provoided). Also, if the ``single`` argument was True, then a string is returned. """ if isinstance(mode, str): mode = [mode] for item in mode: if (allowed is not None) and (item not in allowed): raise Exception('\'mode\' must be one of the following: ' + allowed.__str__()) # Remove duplicates, if any _ = [mode.remove(L) for L in mode if mode.count(L) > 1] if single: if len(mode) > 1: raise Exception('Multiple modes received when only one mode ' + 'is allowed by this method') mode = mode[0] return mode def _parse_prop(self, propname, element): element = self._parse_element(element, single=True) if propname.split('.', 1)[0] in ['pore', 'throat']: propname = propname.split('.', 1)[-1] return element + '.' + propname
[docs] class LabelMixin: """r This mixin adds functionality to the Base2 class so that boolean arrays are treated as labels """ def _get_labels(self, element, locations, mode): r""" This is the actual label getter method, but it should not be called directly. Use ``labels`` instead. """ # Parse inputs locations = self._parse_indices(locations) element = self._parse_element(element=element) # Collect list of all pore OR throat labels labels = [i for i in self.keys(mode='labels') if i.split('.', 1)[0] in element] labels.sort() labels = np.array(labels) # Convert to ndarray for following checks # Make an 2D array with locations in rows and labels in cols arr = np.vstack([self[item][locations] for item in labels]).T num_hits = np.sum(arr, axis=0) # Number of locations with each label if mode in ['or', 'union', 'any']: temp = labels[num_hits > 0] elif mode in ['and', 'intersection']: temp = labels[num_hits == locations.size] elif mode in ['xor', 'exclusive_or']: temp = labels[num_hits == 1] elif mode in ['nor', 'not', 'none']: temp = labels[num_hits == 0] elif mode in ['nand']: temp = labels[num_hits == (locations.size - 1)] elif mode in ['xnor', 'nxor']: temp = labels[num_hits > 1] else: raise Exception('Unrecognized mode:'+str(mode)) return PrintableList(temp)
[docs] def labels(self, pores=[], throats=[], element=None, mode='union'): r""" Returns a list of labels present on the object Additionally, this function can return labels applied to a specified set of pores or throats Parameters ---------- element : str Controls whether pore or throat labels are returned. If empty then both are returned (default). pores (or throats) : array_like The pores (or throats) whose labels are sought. If left empty a list containing all pore and throat labels is returned. mode : str, optional Controls how the query should be performed. Only applicable when ``pores`` or ``throats`` are specified: ============== =================================================== mode meaning ============== =================================================== 'or' Returns the labels that are assigned to *any* of the given locations. Also accepts 'union' and 'any' 'and' Labels that are present on all the given locations. also accepts 'intersection' and 'all' 'xor' Labels that are present on *only one* of the given locations.Also accepts 'exclusive_or' 'nor' Labels that are *not* present on any of the given locations. Also accepts 'not' and 'none' 'nand' Labels that are present on *all but one* of the given locations 'xnor' Labels that are present on *more than one* of the given locations. ============== =================================================== Returns ------- A list containing the labels on the object. If ``pores`` or ``throats`` are given, the results are filtered according to the specified ``mode``. See Also -------- props keys Notes ----- Technically, *'nand'* and *'xnor'* should also return pores with *none* of the labels but these are not included. This makes the returned list more useful. """ # Short-circuit query when no pores or throats are given if (np.size(pores) == 0) and (np.size(throats) == 0): if element is None: element = ['pore', 'throat'] if isinstance(element, str): element = [element] labels = PrintableList() for k, v in self.items(): el, prop = k.split('.', 1) if (el in element) and (v.dtype == bool) and not prop.startswith('_'): labels.append(k) elif (np.size(pores) > 0) and (np.size(throats) > 0): raise Exception('Cannot perform label query on pores and ' + 'throats simultaneously') elif np.size(pores) > 0: labels = self._get_labels(element='pore', locations=pores, mode=mode) elif np.size(throats) > 0: labels = self._get_labels(element='throat', locations=throats, mode=mode) return sorted(labels)
[docs] def set_label(self, label, pores=None, throats=None, mode='add'): r""" Creates or updates a label array Parameters ---------- label : str The label to apply to the specified locations pores : array_like A list of pore indices or a boolean mask of where given label should be added or removed (see ``mode``) throats : array_like A list of throat indices or a boolean mask of where given label should be added or removed (see ``mode``) mode : str Controls how the labels are handled. Options are: =========== ====================================================== mode description =========== ====================================================== 'add' (default) Adds the given label to the specified locations while keeping existing labels 'overwrite' Removes existing label from all locations before adding the label in the specified locations 'remove' Removes the given label from the specified locations leaving the remainder intact 'purge' Removes the specified label from the object completely. This ignores the ``pores`` and ``throats`` arguments. 'clear' Sets all the labels to ``False`` but does not remove the label array =========== ====================================================== """ self._parse_mode(mode=mode, allowed=['add', 'overwrite', 'remove', 'purge', 'clear']) if label.split('.', 1)[0] in ['pore', 'throat']: label = label.split('.', 1)[1] if (pores is not None) and (throats is not None): self.set_label(label=label, pores=pores, mode=mode) self.set_label(label=label, throats=throats, mode=mode) return elif pores is not None: locs = self._parse_indices(pores) element = 'pore' elif throats is not None: locs = self._parse_indices(throats) element = 'throat' if mode == 'add': if element + '.' + label not in self.keys(): self[element + '.' + label] = False self[element + '.' + label][locs] = True if mode == 'overwrite': self[element + '.' + label] = False self[element + '.' + label][locs] = True if mode == 'remove': self[element + '.' + label][locs] = False if mode == 'clear': self['pore' + '.' + label] = False self['throat' + '.' + label] = False if mode == 'purge': _ = self.pop('pore.' + label, None) _ = self.pop('throat.' + label, None)
def _get_indices(self, element, labels, mode='or'): r""" This is the actual method for getting indices, but should not be called directly. Use ``pores`` or ``throats`` instead. """ # Parse and validate all input values. element = self._parse_element(element, single=True) labels = self._parse_labels(labels=labels, element=element) # Begin computing label array if mode in ['or', 'any', 'union']: union = np.zeros([self._count(element), ], dtype=bool) for item in labels: # Iterate over labels and collect all indices union = union + self[element+'.'+item.split('.', 1)[-1]] ind = union elif mode in ['and', 'all', 'intersection']: intersect = np.ones([self._count(element), ], dtype=bool) for item in labels: # Iterate over labels and collect all indices intersect = intersect*self[element+'.'+item.split('.', 1)[-1]] ind = intersect elif mode in ['xor', 'exclusive_or']: xor = np.zeros([self._count(element), ], dtype=int) for item in labels: # Iterate over labels and collect all indices info = self[element+'.'+item.split('.', 1)[-1]] xor = xor + np.int8(info) ind = (xor == 1) elif mode in ['nor', 'not', 'none']: nor = np.zeros([self._count(element), ], dtype=int) for item in labels: # Iterate over labels and collect all indices info = self[element+'.'+item.split('.', 1)[-1]] nor = nor + np.int8(info) ind = (nor == 0) elif mode in ['nand']: nand = np.zeros([self._count(element), ], dtype=int) for item in labels: # Iterate over labels and collect all indices info = self[element+'.'+item.split('.', 1)[-1]] nand = nand + np.int8(info) ind = (nand < len(labels)) * (nand > 0) elif mode in ['xnor', 'nxor']: xnor = np.zeros([self._count(element), ], dtype=int) for item in labels: # Iterate over labels and collect all indices info = self[element+'.'+item.split('.', 1)[-1]] xnor = xnor + np.int8(info) ind = (xnor > 1) else: raise Exception('Unsupported mode: '+mode) # Extract indices from boolean mask ind = np.where(ind)[0] ind = ind.astype(dtype=int) return ind
[docs] def pores(self, labels=None, mode='or', asmask=False): r""" Returns pore indicies where given labels exist, according to the logic specified by the ``mode`` argument. Parameters ---------- labels : str or list[str] The label(s) whose pores locations are requested. This argument also accepts '*' for wildcard searches. mode : str Specifies how the query should be performed. The options are: ============== =================================================== mode meaning ============== =================================================== 'or' Returns the labels that are assigned to *any* of the given locations. Also accepts 'union' and 'any' 'and' Labels that are present on all the given locations. also accepts 'intersection' and 'all' 'xor' Labels that are present on *only one* of the given locations.Also accepts 'exclusive_or' 'nor' Labels that are *not* present on any of the given locations. Also accepts 'not' and 'none' 'nand' Labels that are present on *all but one* of the given locations 'xnor' Labels that are present on *more than one* of the given locations. ============== =================================================== asmask : bool If ``True`` then a boolean array of length Np is returned with ``True`` values indicating the pores that satisfy the query. Returns ------- A Numpy array containing pore indices filtered by the logic specified in ``mode``. See Also -------- throats Notes ----- Technically, *nand* and *xnor* should also return pores with *none* of the labels but these are not included. This makes the returned list more useful. To perform more complex or compound queries, you can opt to receive the result a a boolean mask (``asmask=True``), then manipulate the arrays manually. """ if labels is None: labels = self.name ind = self._get_indices(element='pore', labels=labels, mode=mode) if asmask: ind = self.to_mask(pores=ind) return ind
[docs] def throats(self, labels=None, mode='or', asmask=False): r""" Returns throat locations where given labels exist, according to the logic specified by the ``mode`` argument. Parameters ---------- labels : str or list[str] The throat label(s) whose locations are requested. If omitted, 'all' throat inidices are returned. This argument also accepts '*' for wildcard searches. mode : str Specifies how the query should be performed. The options are: ============== =================================================== mode meaning ============== =================================================== 'or' Returns the labels that are assigned to *any* of the given locations. Also accepts 'union' and 'any' 'and' Labels that are present on all the given locations. also accepts 'intersection' and 'all' 'xor' Labels that are present on *only one* of the given locations.Also accepts 'exclusive_or' 'nor' Labels that are *not* present on any of the given locations. Also accepts 'not' and 'none' 'nand' Labels that are present on *all but one* of the given locations 'xnor' Labels that are present on *more than one* of the given locations. ============== =================================================== asmask : bool If ``True`` then a boolean array of length Nt is returned with ``True`` values indicating the throats that satisfy the query. Returns ------- A Numpy array containing throat indices filtered by the logic specified in ``mode``. See Also -------- pores """ if labels is None: labels = self.name ind = self._get_indices(element='throat', labels=labels, mode=mode) if asmask: ind = self.to_mask(throats=ind) return ind
[docs] def filter_by_label(self, pores=[], throats=[], labels=None, mode='or'): r""" Returns which of the supplied pores (or throats) has the specified label(s) Parameters ---------- pores, or throats : array_like List of pores or throats to be filtered labels : list of strings The labels to apply as a filter mode : str Controls how the filter is applied. The default value is 'or'. Options include: ============== =================================================== mode meaning ============== =================================================== 'or' Returns the labels that are assigned to *any* of the given locations. Also accepts 'union' and 'any' 'and' Labels that are present on all the given locations. also accepts 'intersection' and 'all' 'xor' Labels that are present on *only one* of the given locations.Also accepts 'exclusive_or' 'nor' Labels that are *not* present on any of the given locations. Also accepts 'not' and 'none' 'nand' Labels that are present on *all but one* of the given locations 'xnor' Labels that are present on *more than one* of the given locations. ============== =================================================== Returns ------- A list of pores (or throats) that have been filtered according the given criteria. The returned list is a subset of the received list of pores (or throats). See Also -------- pores throats """ # Convert inputs to locations and element if (np.size(throats) > 0) and (np.size(pores) > 0): raise Exception('Can only filter either pores OR labels') if np.size(pores) > 0: element = 'pore' locations = self._parse_indices(pores) elif np.size(throats) > 0: element = 'throat' locations = self._parse_indices(throats) else: return np.array([], dtype=int) labels = self._parse_labels(labels=labels, element=element) labels = [element+'.'+item.split('.', 1)[-1] for item in labels] all_locs = self._get_indices(element=element, labels=labels, mode=mode) mask = self._tomask(indices=all_locs, element=element) ind = mask[locations] return locations[ind]
[docs] def num_pores(self, labels='all', mode='or'): r""" Returns the number of pores of the specified labels Parameters ---------- labels : list of strings, optional The pore labels that should be included in the count. If not supplied, all pores are counted. labels : list of strings Label of pores to be returned mode : str, optional Specifies how the count should be performed. The options are: ============== =================================================== mode meaning ============== =================================================== 'or' Returns the labels that are assigned to *any* of the given locations. Also accepts 'union' and 'any' 'and' Labels that are present on all the given locations. also accepts 'intersection' and 'all' 'xor' Labels that are present on *only one* of the given locations.Also accepts 'exclusive_or' 'nor' Labels that are *not* present on any of the given locations. Also accepts 'not' and 'none' 'nand' Labels that are present on *all but one* of the given locations 'xnor' Labels that are present on *more than one* of the given locations. ============== =================================================== Returns ------- Np : int Number of pores with the specified labels See Also -------- num_throats count Notes ----- Technically, *'nand'* and *'xnor'* should also count pores with *none* of the labels, however, to make the count more useful these are not included. """ # Count number of pores of specified type Ps = self._get_indices(labels=labels, mode=mode, element='pore') Np = np.shape(Ps)[0] return Np
[docs] def num_throats(self, labels='all', mode='union'): r""" Return the number of throats of the specified labels Parameters ---------- labels : list of strings, optional The throat labels that should be included in the count. If not supplied, all throats are counted. mode : str, optional Specifies how the count should be performed. The options are: ============== =================================================== mode meaning ============== =================================================== 'or' Returns the labels that are assigned to *any* of the given locations. Also accepts 'union' and 'any' 'and' Labels that are present on all the given locations. also accepts 'intersection' and 'all' 'xor' Labels that are present on *only one* of the given locations.Also accepts 'exclusive_or' 'nor' Labels that are *not* present on any of the given locations. Also accepts 'not' and 'none' 'nand' Labels that are present on *all but one* of the given locations 'xnor' Labels that are present on *more than one* of the given locations. ============== =================================================== Returns ------- Nt : int Number of throats with the specified labels See Also -------- num_pores count Notes ----- Technically, *'nand'* and *'xnor'* should also count throats with *none* of the labels, however, to make the count more useful these are not included. """ # Count number of pores of specified type Ts = self._get_indices(labels=labels, mode=mode, element='throat') Nt = np.shape(Ts)[0] return Nt