"""
Notes:
- Just need top depths/elevations.
"""
import copy
import numpy as np
from math import floor, ceil
import matplotlib.pyplot as plt
from striplog import Striplog
from litholog import Bed
from litholog import utils
from litholog.sequence import SequenceIOMixin, SequenceVizMixin, SequenceStatsMixin
[docs]class BedSequence(SequenceIOMixin, SequenceVizMixin, SequenceStatsMixin, Striplog):
"""
Ordered collection of ``Bed`` instances.
"""
def __init__(self, list_of_Beds, metadata={}):
"""
Parameters
----------
list_of_Beds : list(``litholog.Bed``)
A list containing the Bed(s) comprising the sequence.
metadata : dict, optional
Any additional metadata about the sequence as a whole.
"""
self.metadata = metadata
Striplog.__init__(self, list_of_Beds)
"""
Possible `stumpy` stuff:
def motif(self, location, size):
assert size % 2 == 1, 'Should only do odd sizes.'
first = location - (size // 2)
last = location + (size // 2)
assert first > 0 and last < len(self), f'Motif at {location, size} incompatible with {len(self)}'
beds = self[first:last+1]
"""
@property
def values(self):
"""
Get the instance as a 2D array w/ shape (``nsamples``, ``nfeatures``).
"""
return self.get_values()
[docs] def get_values(self, exclude_keys=[]):
"""
Getter for ``values`` that allows dropping ``exclude_keys`` (e.g., sample depths) from array
"""
pairs = zip(self[:-1], self[1:])
assert all(t.compatible_with(b) for t, b in pairs), 'Beds must have compatible data'
vals = [bed.get_values(exclude_keys=exclude_keys) for bed in self]
return np.vstack(vals)
@property
def nsamples(self):
"""
The number of sample rows in ``values``.
NOTE: ``len(striplog.Striplog)`` will already give number of beds.
"""
return self.values.shape[0]
@property
def nfeatures(self):
"""
The number of columns in ``values``.
"""
return self.values.shape[1]
[docs] def max_field(self, field):
"""
Override method from ``striplog.Striplog`` to account for iterable ``Bed`` data.
"""
return max(filter(None, [iv.max_field(field) for iv in self]))
[docs] def min_field(self, field):
"""
Override method from ``striplog.Striplog`` to account for iterable ``Bed`` data.
"""
return min(filter(None, [iv.min_field(field) for iv in self]))
[docs] def get_field(self, field, lithology=None, default_value=0.0):
"""
Get 'vertical' array of ``field`` values.
If `lithology` provided, will only use the beds matching that lithology (in primary component).
"""
if lithology is not None:
ivs = [iv for iv in self if iv.primary.lithology == lithology]
else:
ivs = self
vals = [iv[field] for iv in ivs]
vals = [default_value] if len(vals) == 0 else vals
try:
return np.concatenate(vals)
except ValueError:
return np.array(vals)
[docs] def reduce_field(self, field, fn):
"""
Apply ``fn`` to the output of ``get_field(field)``
"""
result = fn(self.get_field(field))
if not hasattr(result, '__iter__'):
result = [result]
return np.array(result)
[docs] def reduce_fields(self, field_fn_dict):
"""
Return array, result of applying `fn` values to `field` keys.
The funcs can return scalars or arrays, but all of the return values
should be numpy-concatable. The concatenation
"""
try:
vals = [self.reduce_field(field, fn) for field, fn in field_fn_dict.items()]
except Exception as e:
print(f'Error reducing fields for: {self.metadata}')
raise(e)
try:
return np.concatenate(vals)
except ValueError as ve:
print(f'Incompatible shapes: {[v.shape for v in vals]}')
raise(ve)
[docs] def resample_data(self, depth_key, step, kind='linear'):
"""
Resample the data at approximate depth intervals of size `step`.
`depth_key` can be a `str` (for dict-like bed data) or column index (for array bed data).
I think we probably want to maintain top/base samples, and sample to the nearest `step` b/t.
Maybe this could be the default of multiple options? Implement it as the default first though.
NOTE: We could return a new instance rather than modify inplace, since it's hard to undo.
"""
for iv in self:
iv.resample_data(depth_key, step, kind=kind)
return self.values
[docs] def flip_convention(self, depth_key=None):
"""
Changes the depth convention (elevation <-> depth), setting to base or top to 0.0, respectively.
If `depth_key` is given, that data column in each bed should be shifted the same amount.
"""
return self.shift(delta=-self.stop.z, depth_key=depth_key)
[docs] def shift(self, delta=None, start=None, depth_key=None):
"""
Shift all the intervals by `delta` (negative numbers are 'up'), or by setting a new start depth.
Returns a new copy of the BedSequence.
"""
if delta is None:
if start is None:
raise ValueError("You must provide a delta or a new start.")
delta = start - self.start.z
new_beds = []
for bed in self:
try:
components = bed.components
except AttributeError:
components = []
data = copy.deepcopy(bed.data)
if depth_key is not None:
data[depth_key] = np.abs(data[depth_key] + delta)
new_bed = Bed(
np.abs(bed.top.z + delta), np.abs(bed.base.z + delta),
data=data, components=components
)
new_beds.append(new_bed)
# Should we reverse beds order here?
return BedSequence(new_beds, metadata=self.metadata)