#pylint: disable=too-many-lines
"""faults and FaultSegment components."""
from copy import deepcopy
from itertools import product
import warnings
import numpy as np
import pandas as pd
import h5py
from anytree import (RenderTree, AsciiStyle, Resolver, PreOrderIter,
find_by_attr)
from .fault_segment import FaultSegment
from .base_component import BaseComponent
from .faults_load_utils import load_faults, load_multflt
from .faults_dump_utils import write_faults, write_multflt
from .decorators import apply_to_each_segment
FACES = {'X': [1, 3, 5, 7], 'Y': [2, 3, 6, 7], 'Z': [4, 5, 6, 7]}
class IterableFaults:
"""Faults iterator."""
def __init__(self, root):
self.iter = PreOrderIter(root)
def __next__(self):
x = next(self.iter)
if x.ntype != 'fault':
return next(self)
return x
[docs]
class Faults(BaseComponent):
"""Faults component.
Contains faults in a single tree structure, faults attributes
and preprocessing actions.
Parameters
----------
node : FaultSegment, optional
Root node for fault's tree.
"""
def __init__(self, node=None, **kwargs):
super().__init__(**kwargs)
self._root = FaultSegment(name='FIELD', ntype="group") if node is None else node
self._resolver = Resolver()
self.init_state(has_blocks=False)
[docs]
def copy(self):
"""Returns a deepcopy. Cached properties are not copied."""
copy = self.__class__(self.root.copy())
copy._state = deepcopy(self.state) #pylint: disable=protected-access
for node in PreOrderIter(self.root):
if node.is_root:
continue
node_copy = node.copy()
node_copy.parent = copy[node.parent.name]
return copy
@property
def root(self):
"""Tree root."""
return self._root
@property
def names(self):
"""List of fault names."""
return [node.name for node in self]
def __getitem__(self, key):
node = find_by_attr(self.root, key)
if node is None:
raise KeyError(key)
return node
def __setitem__(self, key, value):
raise NotImplementedError()
def __delitem__(self, key):
self.drop(key)
def __iter__(self):
return IterableFaults(self.root)
def __contains__(self, key):
return find_by_attr(self.root, key) is not None
def _get_fmt_loader(self, fmt):
"""Get loader for given file format."""
if fmt == 'RSM':
return self._load_rsm
return super()._get_fmt_loader(fmt)
[docs]
def update(self, faultsdata, mode='w', **kwargs):
"""Update tree nodes with new faultsdata. If fault does not exists,
it will be attached to root.
Parameters
----------
faultsdata : dict
Keys are fault names, values are dicts with fault attributes.
mode : str, optional
If 'w', write new data. If 'a', try to append new data. Default to 'w'.
kwargs : misc
Any additional named arguments to append.
Returns
-------
out : Faults
Faults with updated attributes.
"""
def _get_parent(name):
if ':' in name:
return self[':'.join(name.split(':')[:-1])]
return self.root
for name in sorted(faultsdata):
data = faultsdata[name]
name = name.strip(' \t\'"')
try:
segment = self[name]
except KeyError:
parent = _get_parent(name)
segment = FaultSegment(parent=parent, name=name, ntype='fault')
for k, v in data.items():
if mode == 'w':
setattr(segment, k, v)
elif mode == 'a':
if k in segment.attributes:
att = getattr(segment, k)
setattr(segment, k, pd.concat([att, v], **kwargs))
else:
setattr(segment, k, v)
else:
raise ValueError("Unknown mode {}. Expected 'w' (write) or 'a' (append)".format(mode))
return self
[docs]
def drop(self, names):#pylint:disable=arguments-renamed
"""Detach faults by names.
Parameters
----------
names : str, array-like
Faults to be detached.
Returns
-------
out : Faults
Faults without detached segments.
"""
for name in np.atleast_1d(names):
try:
self[name].parent = None
except KeyError:
continue
return self
@property
def resolver(self):
"""Tree resolver."""
return self._resolver
[docs]
def glob(self, name):
"""Return instances at ``name`` supporting wildcards."""
return self.resolver.glob(self.root, name)
[docs]
def render_tree(self):
"""Print tree structure."""
print(RenderTree(self.root, style=AsciiStyle()).by_attr())
return self
[docs]
@apply_to_each_segment
def get_blocks(self, segment, **kwargs):
"""Calculate grid blocks for the tree of faults.
Parameters
----------
segment : class instance
FaultSegment class.
kwargs : misc
Any additional named arguments to append.
Returns
-------
comp : faults
faults component with calculated grid blocks and fault in block projections.
"""
_ = kwargs
blocks_fault = []
xyz_fault = []
for idx in segment.faults.index:
cells = segment.faults.loc[idx, ['IX1', 'IX2', 'IY1', 'IY2', 'IZ1', 'IZ2', 'FACE']]
x_range = range(cells['IX1']-1, cells['IX2'])
y_range = range(cells['IY1']-1, cells['IY2'])
z_range = range(cells['IZ1']-1, cells['IZ2'])
blocks_segment = np.array(list(product(x_range, y_range, z_range)))
xyz_segment = self.field.grid.xyz[blocks_segment[:, 0],
blocks_segment[:, 1],
blocks_segment[:, 2]][:, FACES[cells['FACE']]]
blocks_fault.extend(blocks_segment)
xyz_fault.extend(xyz_segment)
segment.blocks = np.array(blocks_fault)
segment.faces_verts = np.array(xyz_fault)
self.set_state(has_blocks=True)
return self
def _read_buffer(self, buffer, attr, **kwargs):
"""Load fault data from an ASCII file.
Parameters
----------
buffer : StringIteratorIO
Buffer to get string from.
attr : str
Target keyword.
Returns
-------
comp : faults
faults component with loaded fault data.
"""
if attr == 'FAULTS':
return load_faults(self, buffer, **kwargs)
if attr == 'MULTFLT':
return load_multflt(self, buffer, **kwargs)
raise ValueError("Keyword {} is not supported in faults.".format(attr))
def _dump_ascii(self, path, attr, mode='w', **kwargs):
"""Save data into text file.
Parameters
----------
path : str
Path to output file.
attr : str
Attribute to dump into file.
mode : str
Mode to open file.
'w': write, a new file is created (an existing file with
the same name would be deleted).
'a': append, an existing file is opened for reading and writing,
and if the file does not exist it is created.
Default to 'w'.
Returns
-------
comp : Faults
Faults unchanged.
"""
with open(path, mode) as f:
if attr.upper() == 'FAULTS':
write_faults(f, self)
elif attr.upper() == 'MULTFLT':
write_multflt(f, self)
else:
raise NotImplementedError("Dump for {} is not implemented.".format(attr.upper()))
def _dump_hdf5(self, path, mode='a', state=True, **kwargs): #pylint: disable=too-many-branches
"""Save data into HDF5 file.
Parameters
----------
path : str
Path to output file.
mode : str
Mode to open file.
'w': write, a new file is created (an existing file with
the same name would be deleted).
'a': append, an existing file is opened for reading and writing,
and if the file does not exist it is created.
Default to 'a'.
state : bool
Dump compoments's state.
Returns
-------
comp : Faults
Faults unchanged.
"""
_ = kwargs
with h5py.File(path, mode) as f:
faults = f[self.class_name] if self.class_name in f else f.create_group(self.class_name)
if state:
for k, v in self.state.as_dict().items():
faults.attrs[k] = v
for fault in PreOrderIter(self.root):
if fault.is_root:
continue
fault_path = fault.fullname
if fault.name == 'data':
raise ValueError("Name 'data' is not allowed for nodes.")
grp = faults[fault_path] if fault_path in faults else faults.create_group(fault_path)
grp.attrs['ntype'] = fault.ntype
if 'data' not in grp:
grp_faults_data = faults.create_group(fault_path + '/data')
else:
grp_faults_data = grp['data']
for att, data in fault.items():
if isinstance(data, pd.DataFrame):
continue
if att in grp_faults_data:
del grp_faults_data[att]
grp_faults_data.create_dataset(att, data=data)
with warnings.catch_warnings():
warnings.simplefilter('ignore')
for fault in PreOrderIter(self.root):
if fault.is_root:
continue
for att, data in fault.items():
if isinstance(data, pd.DataFrame):
data.to_hdf(path, key='/'.join([self.class_name, fault.fullname,
'data', att]), mode='a')
def _load_hdf5(self, path, attrs=None, **kwargs):
"""Load data from a HDF5 file.
Parameters
----------
path : str
Path to file to load data from.
attrs : str or array of str, optional
Array of dataset's names to get from file. If not given, loads all.
Returns
-------
comp : BaseComponent
BaseComponent with loaded attributes.
"""
_ = kwargs
if isinstance(attrs, str):
attrs = [attrs]
def update_faults(grp, parent=None):
"""Build tree recursively following HDF5 node hierarchy."""
if parent is None:
fault = self.root
else:
ntype = grp.attrs.get('ntype', None)
if ntype is None: #backward compatibility, will be removed in a future
ntype = 'group' if grp.attrs.get('is_group', False) else 'fault'
fault = FaultSegment(parent=parent, name=grp.name.split('/')[-1], ntype=ntype)
for k, v in grp.items():
if k == 'data':
for att in v.keys() if attrs is None else attrs:
try:
data = v[att]
except KeyError:
continue
if isinstance(data, h5py.Group):
data = pd.read_hdf(path, key='/'.join([grp.name, 'data', att]), mode='r')
setattr(fault, att, data)
else:
setattr(fault, att, data[()])
else:
update_faults(v, fault)
with h5py.File(path, 'r') as f:
self.set_state(**dict(f[self.class_name].attrs.items()))
update_faults(f[self.class_name])
return self