importnumpyasnpimportloggingimportuuidfromcopyimportdeepcopyfromopenpnm.coreimport(LabelMixin,ParserMixin,ModelsMixin2)fromopenpnm.utilsimport(Workspace,SettingsAttr,PrintableList,PrintableDict,Docorator,get_printable_props,get_printable_labels,)docstr=Docorator()logger=logging.getLogger(__name__)ws=Workspace()__all__=['Base2','Domain',]@docstr.get_sections(base='BaseSettings',sections=docstr.all_sections)@docstr.dedentclassBaseSettings:r""" The default settings to use on instance of Base Parameters ---------- uuid : str A universally unique identifier for the object to keep things straight """default_domain='domain_1'
[docs]@docstr.get_sections(base='Base',sections=['Parameters'])@docstr.dedentclassBase2(dict):r""" A subclassed dictionary used for storing data Parameters ---------- network : dict An OpenPNM Network object, which is a subclass of a dict """def__new__(cls,*args,**kwargs):instance=super(Base2,cls).__new__(cls,*args,**kwargs)# It is necessary to set the SettingsAttr here since some classes# use it before calling super.__init__()instance.settings=SettingsAttr()instance.settings['uuid']=str(uuid.uuid4())returninstancedef__init__(self,network=None,project=None,name='obj_?'):super().__init__()# Add default settingsself.settings._update(BaseSettings())# Add parameters attrself._params=PrintableDict(key="Parameters",value="Value")# Associate with projectif(networkisNone)and(projectisNone):project=ws.new_project()elifprojectisNone:project=network.projectproject.append(self)self.name=namedef__eq__(self,other):returnhex(id(self))==hex(id(other))def__repr__(self):# pragma: no covermodule=self.__module__module=".".join([xforxinmodule.split(".")ifnotx.startswith("_")])cname=self.__class__.__name__returnf'{self.name} : <{module}.{cname} at {hex(id(self))}>'def__setitem__(self,key,value):ifvalueisNone:return# Intercept parametersifkey.startswith('param'):_,key=key.split('.',1)self._params[key]=valuereturnifnot(key.startswith('pore.')orkey.startswith('throat.')):raiseException("All dict names must start with pore, throat, or param")# Intercept @ symbolif'@'inkey:element,prop=key.split('@')[0].split('.',1)domain=key.split('@')[1]locs=super().__getitem__(f'{element}.{domain}')try:vals=self[f'{element}.{prop}']vals[locs]=valueself[f'{element}.{prop}']=valsexceptKeyError:value=np.array(value)temp=self._initialize_empty_array_like(value,element)self.__setitem__(f'{element}.{prop}',temp)self[f'{element}.{prop}'][locs]=valuereturnelement,prop=key.split('.',1)# Catch dictionaries and break them upifisinstance(value,dict):fork,vinvalue.items():self[f'{key}.{k}']=vreturn# Enforce correct dict namingifelementnotin['pore','throat']:raiseException('All keys must start with either pore or throat')# Convert value to ndarrayifnotisinstance(value,np.ndarray):value=np.array(value,ndmin=1)# Skip checks for coords and connsifkeyin['pore.coords','throat.conns']:self.update({key:value})return# Finally write dataifself._count(element)isNone:self.update({key:value})# If length not defined, do itelifvalue.shape[0]==1:# If value is scalarvalue=np.ones((self._count(element),),dtype=value.dtype)*valueself.update({key:value})elifnp.shape(value)[0]==self._count(element):self.update({key:value})else:raiseException('Provided array is wrong length for '+key)def__getitem__(self,key):# If key is a just a numerical value, then kick it directly back.# This allows one to do either value='pore.blah' or value=1.0 in# pore-scale modelsifnotisinstance(key,str):returnkeyifkey.startswith('param'):_,key=key.split('.',1)try:returnself._params[key]exceptKeyError:returnself.network._params[key]# If key contains an @ symbol then return a subset of values at the# requested locations, by recursively calling __getitem__if'@'inkey:element,prop=key.split('@')[0].split('.',1)domain=key.split('@')[1]iff'{element}.{domain}'notinself.keys():raiseKeyError(key)locs=self[f'{element}.{domain}']vals=self[f'{element}.{prop}']returnvals[locs]try:returnsuper().__getitem__(key)exceptKeyError:# If key is object's name or all, return onesifkey.split('.',1)[-1]in[self.name,'all']:element,prop=key.split('.',1)vals=np.ones(self._count(element),dtype=bool)returnvalselse:vals={}# Gather any arrays into a dictforkinself.keys():ifk.startswith(f'{key}.'):vals.update({k.replace(f'{key}.',''):self[k]})iflen(vals)>0:returnvalselse:raiseKeyError(key)def__delitem__(self,key):try:super().__delitem__(key)exceptKeyError:d=self[key]# If key is a nested dict, get all valuesforitemind.keys():super().__delitem__(f'{key}.{item}')
[docs]defclear(self,mode=None):r""" Clears or deletes certain things from object. If no arguments are provided it defaults to the normal `dict` behavior. Parameters ---------- mode : str Controls which things are to be deleted. Options are: =========== ============================================================ `mode` Description =========== ============================================================ 'props' Deletes all pore and throat properties (i.e numerical data) in the object's dictionary (except 'pore.coords' and 'throat.conns' if it is a network object). 'labels' Deletes all labels (i.e. boolean data) in the object's dictionary. 'models' Delete are pore and throat properties that were produced by a pore-scale model. =========== ============================================================ """ifmodeisNone:super().clear()else:ifisinstance(mode,str):mode=[mode]if'props'inmode:foriteminself.props():ifitemnotin['pore.coords','throat.conns']:delself[item]if'labels'inmode:foriteminself.labels():ifitemnotin['pore.'+self.name,'throat.'+self.name]:delself[item]if'models'inmode:foriteminself.models.keys():_=self.pop(item.split('@')[0],None)
[docs]defkeys(self,mode=None):r""" An overloaded version of ``keys`` that optionally accepts a ``mode`` Parameters ---------- mode : str If given, optionally, it controls which type of keys are returned. Options are: ========== ======================================================= mode description ========== ======================================================= props Returns only keys that contain numerical arrays labels Returns only keys that contain boolean arrays models Returns only keys that were generated by a pore-scale model constants Returns only keys are were *not* generated by a pore- scale model ========== ======================================================= """ifmodeisNone:returnsuper().keys()else:ifisinstance(mode,str):mode=[mode]vals=set()if'props'inmode:foriteminself.props():vals.add(item)if'labels'inmode:foriteminself.labels():vals.add(item)if'models'inmode:foriteminself.models.keys():propname=item.split('@')[0]ifpropnameinself.keys():vals.add(propname)if'constants'inmode:vals=vals.union(set(self.props()))foriteminself.models.keys():propname=item.split('@')[0]ifpropnameinvals:vals.remove(propname)returnPrintableList(vals)
def_set_name(self,name,validate=True):ifnothasattr(self,'_name'):self._name=''old_name=self._nameifname==old_name:returnname=self.project._generate_name(name)self._name=namedef_get_name(self):try:returnself._nameexceptAttributeError:returnNonename=property(_get_name,_set_name)def_get_project(self):forprojinlist(ws.values()):ifselfinproj:returnprojproject=property(fget=_get_project)def_set_settings(self,settings):self._settings=deepcopy(settings)def_get_settings(self):ifself._settingsisNone:self._settings=SettingsAttr()returnself._settingsdef_del_settings(self):self._settings=Nonesettings=property(fget=_get_settings,fset=_set_settings,fdel=_del_settings)@propertydefnetwork(self):r""" Shortcut to retrieve a handle to the network object associated with the calling object """returnself.project.network@propertydefparams(self):r""" This attribute stores 'scalar' data that can be used by pore-scale models. For instance, if a model calls for `temperature` you can specify `pore.temperature` if every pore might have a different value, or `param.temperature` if a single value prevails everywhere. """returnself._paramsdef_count(self,element):ifelement=='pore':try:returnself['pore.coords'].shape[0]exceptKeyError:fork,vinself.items():ifk.startswith('pore.'):returnv.shape[0]elifelement=='throat':try:returnself['throat.conns'].shape[0]exceptKeyError:fork,vinself.items():ifk.startswith('throat.'):returnv.shape[0]@propertydefNt(self):r""" Shortcut to retrieve the number of throats in the domain """returnself._count('throat')@propertydefNp(self):r""" Shortcut to retrieve the number of pores in the domain """returnself._count('pore')@propertydefTs(self):r""" Shortcut to retrieve the indices of *all* throats """returnnp.arange(self._count('throat'))@propertydefPs(self):r""" Shortcut to retrieve the indices of *all* pores """returnnp.arange(self._count('pore'))def_tomask(self,element,indices):returnself.to_mask(**{element+'s':indices})
[docs]defto_mask(self,pores=None,throats=None):r""" Generates a boolean mask with `True` values in the given locations Parameters ---------- pores : array_like The pore indices where `True` values will be placed. If `pores` is given the `throats` is ignored. throats : array_like The throat indices where `True` values will be placed. If `pores` is given the `throats` is ignored. Returns ------- mask : ndarray, boolean A boolean array of length Np is `pores` was given or Nt if `throats` was given. """ifporesisnotNone:indices=np.array(pores,ndmin=1)N=self.NpelifthroatsisnotNone:indices=np.array(throats,ndmin=1)N=self.Ntelse:raiseException('Must specify either pores or throats')mask=np.zeros((N,),dtype=bool)mask[indices]=Truereturnmask
[docs]defto_indices(self,mask):r""" Converts a boolean mask to pore or throat indices Parameters ---------- mask : ndarray A boolean mask with `True` values indicating either pore or throat indices. This array must either be Nt or Np long, otherwise an Exception is raised. Returns ------- indices : ndarray An array containing numerical indices of where `mask` was `True`. Notes ----- This function is equivalent to just calling `np.where(mask)[0]` but does check to ensure that `mask` is a valid length. """mask=np.array(mask,dtype=bool)ifmask.shape[0]notin[self.Np,self.Nt]:raiseException('Mask must be either Nt or Np long')returnnp.where(mask)[0]
[docs]defprops(self,element=['pore','throat']):r""" Retrieves a list of keys that contain numerical data (i.e. "properties") Parameters ---------- element : str, list of strings Indicates whether `'pore'` or `'throat'` properties should be returned. The default is `['pore', 'throat']`, so both are returned. Returns ------- props : list of strings The names of all dictionary keys on the object that contain numerical data. """ifelementisNone:element=['pore','throat']ifisinstance(element,str):element=[element]props=[]fork,vinself.items():el,prop=k.split('.',1)if(elinelement)and(v.dtype!=bool)andnotprop.startswith('_'):props.append(k)props=sorted(props)props=PrintableList(props)returnprops
[docs]definterpolate_data(self,propname,mode='mean'):r""" Generates an array of the requested pore/throat data by interpolating the neighboring throat/pore data. Parameters ---------- propname : str The data to be generated. mode : str Dictate how the interpolation is done. Options are 'mean', 'min', and 'max'. Returns ------- data : ndarray An ndarray containing the interpolated data. E.g. Requesting 'throat.temperature' will read the values of 'pore.temperature' in each of the neighboring pores and compute the average (if `mode='mean'`). """fromopenpnm.models.miscimportfrom_neighbor_throats,from_neighbor_poreselement,prop=propname.split('.',1)ifelement=='throat':ifself['pore.'+prop].dtype==bool:raiseException('The requested datatype is boolean, cannot interpolate')values=from_neighbor_pores(self,prop='pore.'+prop,mode=mode)elifelement=='pore':ifself['throat.'+prop].dtype==bool:raiseException('The requested datatype is boolean, cannot interpolate')values=from_neighbor_throats(self,prop='throat.'+prop,mode=mode)returnvalues
[docs]defget_conduit_data(self,propname):r""" Fetches an Nt-by-3 array of the requested property Parameters ---------- propname : str The dictionary key of the property to fetch. Returns ------- data : ndarray An Nt-by-3 array with each column containing the requrested data for pore1, throat, and pore2 respectively. """poreprop='pore.'+propname.split('.',1)[-1]throatprop='throat.'+propname.split('.',1)[-1]conns=self.network.connstry:T=self[throatprop]ifT.ndim>1:raiseException(f'{throatprop} must be a single column wide')exceptKeyError:T=np.ones([self.Nt,],dtype=float)*np.nantry:P1,P2=self[poreprop][conns.T]exceptKeyError:P1=np.ones([self.Nt,],dtype=float)*np.nanP2=np.ones([self.Nt,],dtype=float)*np.nanvals=np.vstack((P1,T,P2)).Tifnp.isnan(vals).sum()==vals.size:raiseKeyError(f'{propname} not found')returnvals
def__str__(self):# pragma: no coverhr='―'*78lines=''lines+='\n'+"═"*78+'\n'+self.__repr__()+'\n'+hr+'\n'lines+=get_printable_props(self)lines+='\n'+hr+'\n'lines+=get_printable_labels(self)lines+='\n'+hrreturnlinesdef_initialize_empty_array_like(self,value,element):element=element.split('.',1)[0]value=np.array(value)ifvalue.dtype==bool:temp=np.zeros([self._count(element),*value.shape[1:]],dtype=bool)else:temp=np.zeros([self._count(element),*value.shape[1:]],dtype=float)*np.nanreturntemp
[docs]classDomain(ParserMixin,LabelMixin,ModelsMixin2,Base2):r""" The main class used for Network, Phase and Algorithm objects. This class inherits from ``Base2``, but also has several mixins for added functionality. """...