"""
Geosoft vector.
:Classes:
=============== =========================
:class:`GXvv` data vector
=============== =========================
VA and VV classes are related based on a key called a *fiducial*,
which has a start value and increment between values. The :meth:`refid` method can be used to resample vector
data to the same fiducial so that vector-to-vector operations can be performed.
.. seealso:: :mod:`geosoft.gxpy.va`, :mod:`geosoft.gxapi.GXVV`, :mod:`geosoft.gxapi.GXVA`
.. note::
Regression tests provide usage examples:
`vv tests <https://github.com/GeosoftInc/gxpy/blob/master/geosoft/gxpy/tests/test_vv.py>`_
"""
from collections.abc import Sequence
import geosoft
import numpy as np
import geosoft.gxapi as gxapi
from . import utility as gxu
__version__ = geosoft.__version__
MAX_VV_BYTES = 4096 #: maximum bytes per element in VV
def _t(s):
return geosoft.gxpy.system.translate(s)
[docs]class VVException(geosoft.GXRuntimeError):
"""
Exceptions from :mod:`geosoft.gxpy.vv`.
.. versionadded:: 9.1
"""
pass
[docs]def np_from_vvset(vvset, axis=1):
"""
Return a 2d numpy array from a set of `GXvv` instances.
:param vvset: (vv1, vv2, ...) set ot `geosoft.gxpy.vv.GXvv` instances
:param axis: axis for the vv, default is 1, such that each vv is a column, `axis=0` for vv to be rows
:return: numpy array shaped (max_vv_length, number_of_vv) for `axis=1`, or (number_of_vv, max_vv_length)
for `axis=0`.
.. versionadded:: 9.3.1
"""
nvv = len(vvset)
length = 0
for vv in vvset:
if len(vv) > length:
length = len(vv)
npd = np.empty((length, nvv), dtype=vvset[0].dtype)
for i in range(nvv):
npd[:, i] = vvset[i].np
if axis == 1:
return npd
else:
return npd.T
[docs]def vvset_from_np(npd, axis=1):
"""
Return a set of `GXvv` instances from a 2d numpy array.
:param npd: numpy data array of dimension 2. If the array has higher dimensions it will first
be reshaped to a 2-dimension array based on the last axis.
:param axis: axis for the vv, default is 1, such that each columns become vv, `axis=0` for rows to be vv
:return: [vv0, vv1, vv2, ...] `geosoft.gxpy.vv.GXvv` instances for each column or row (`axis=0`)
For example:
npd = np.array([[1, 2, 3], [4, 5, 6]])
returns (vv([1, 4]), vv([2, 5]), vv([3, 6]))
.. versionadded:: 9.3.1
"""
if npd.ndim == 1:
vv = [GXvv(npd)]
else:
if npd.ndim > 2:
npd = npd.reshape((-1, npd.shape[-1]))
if axis == 0:
npd = npd.T
vv = []
for i in range(npd.shape[1]):
vv.append(GXvv(npd[:, i]))
return tuple(vv)
[docs]class GXvv(Sequence):
"""
VV class wrapper.
:param array: array-like, None to create an empty VV. Can have 2 dimensions for float32 or
float64 data, in which case the second dimension can be 2 or 3 to use Geosoft
2D and 3D dimensioned types. This can also be another `GXvv` instance, in which
case a copy of the data is made and the dtype, dim, fid an unit_of_measurement
will default to the source instance.
:param dtype: numpy data type. For unicode strings 'U#', where # is a string length. If not specified
the type is taken from first element in array, of if no array the default is 'float'.
:param dim: dimension can be 1 (default), 2 (2D) or 3 (3D). Ignored if array is defined as the array
dimensions will be used.
:param fid: (start, increment) fiducial
:param unit_of_measure: unit of measure for the contained data.
:param len: length of VV
:Properties:
``vv`` :class:`geosoft.gxapi.GXvv` instance
``fid`` (start, increment) fiducial
``length`` number of elements in the VV
``gxtype`` GX data type
``dtype`` numpy data type
``dim`` dimension
.. versionadded:: 9.1
.. versionchanged:: 9.2 support construction directly from arrays
.. versionchanged:: 9.3 added unit_of_measure
.. versionchanged:: 9.3.1 added string support in __getitem__, and creates from a source `GVvv` instance.
.. versionchanged:: 9.6 Added length parameter.
"""
def __enter__(self):
return self
def __exit__(self, _type, _value, _traceback):
self.__del__()
def __del__(self):
if hasattr(self, '_gxvv'):
self._gxvv = None
def __eq__(self, other):
return np.array_equal(self.np, other.np) \
and self.fid == other.fid \
and self.dim == other.dim \
and self.unit_of_measure == other.unit_of_measure
[docs] def __init__(self, array=None, dtype=None, fid=None, unit_of_measure=None, dim=None, len=0):
if array is not None:
if isinstance(array, GXvv):
if fid is None:
fid = array.fid
if unit_of_measure is None:
unit_of_measure = array.unit_of_measure
if dtype is None:
dtype = array.dtype
if dim is None:
dim = array.dim
array = array.np
else:
if not isinstance(array, np.ndarray):
array = np.array(array)
if array.ndim == 2:
dim = array.shape[1]
else:
dim = 1
if dtype is None:
dtype = array.dtype
if fid is None:
fid = (0.0, 1.0)
if unit_of_measure is None:
unit_of_measure = ''
if dim is None:
dim = 1
elif dim not in (1, 2, 3):
raise VVException(_t('dimension (array, or dim=) must be 1, 2 or 3'))
self._dim = dim
if dtype is None:
dtype = np.float64
self._gxtype = max(-MAX_VV_BYTES, gxu.gx_dtype(dtype))
# Since we are using UTF-8 internally characters can take anywhere between 1 and 4 bytes.
# The gx_dtype method and the gxapi wrappers accounts for that by multiplying the dtype number accordingly.
# Specifying a numpy dtype to instantiate VV will ensure the internal space is enough to allocate up to
# that 4 times the Unicode characters, however any Numpy arrays should still used the passed dtype as specified
if dtype is not None and isinstance(dtype, np.dtype) and dtype.type is np.str_:
self._dtype = dtype
elif type(dtype) is str:
self._dtype = np.dtype(dtype)
else:
self._dtype = gxu.dtype_gx(self._gxtype)
self._is_float = self._is_int = self._is_string = False
if gxu.is_float(self._gxtype):
self._is_float = True
elif gxu.is_int(self._gxtype):
self._is_int = True
else:
self._is_string = True
if not self._is_float and self._dim != 1:
raise VVException(_t('2 or 3 dimensioned data must be float32 or float64'))
if self._dim != 1:
self._gxvv = gxapi.GXVV.create_ext(gxu.gx_dtype_dimension(self._dtype, self._dim), len)
else:
self._gxvv = gxapi.GXVV.create_ext(self._gxtype, len)
self.fid = fid
self._next = 0
self._unit_of_measure = unit_of_measure
if array is not None:
self.set_data(array, fid)
def __len__(self):
return self._gxvv.length()
def __iter__(self):
return self
def __next__(self):
if self._next >= self.length:
self._next = 0
raise StopIteration
else:
i = self._next
self._next += 1
return self.np[i], self.fid[0] + self.fid[1] * i
def __getitem__(self, item):
start, incr = self.fid
if self._is_float:
v = float(self.np[item])
elif self._is_int:
v = int(self.np[item])
else:
v = str(self.np[item])
return v, start + incr * item
def _set_data_np(self, npd, start=0):
"""set to data in a numpy array"""
if not npd.flags['C_CONTIGUOUS']:
npd = np.ascontiguousarray(npd)
self.gxvv.set_data(start, npd.shape[0], npd.data.tobytes(), gxu.gx_dtype_dimension(npd.dtype, self._dim))
def _get_data_np(self, start=0, n=None, dtype=None):
"""return data in a numpy array"""
if n is None:
n = self.length - start
if self._dim == 1:
sh = (n,)
else:
sh = (n, self._dim)
bytearr = np.empty(sh, dtype=dtype).tobytes()
self.gxvv.get_data(start, n, bytearr, gxu.gx_dtype_dimension(dtype, self._dim))
npd = np.frombuffer(bytearr, dtype=dtype).reshape(sh)
return np.array(npd)
@property
def unit_of_measure(self):
""" data unit of measurement"""
return self._unit_of_measure
@unit_of_measure.setter
def unit_of_measure(self, uom):
self._unit_of_measure = str(uom)
@property
def gxvv(self):
""":class:`geosoft.gxapi.GXVV` instance"""
return self._gxvv
@property
def fid(self):
"""
fid tuple (start,increment), can be set
.. versionadded:: 9.1
"""
return self._gxvv.get_fid_start(), self._gxvv.get_fid_incr()
@fid.setter
def fid(self, fid):
self._gxvv.set_fid_start(fid[0])
self._gxvv.set_fid_incr(fid[1])
@property
def length(self):
"""
number of elements in the VV, can be set
.. versionadded:: 9.1
.. versionchanged:: 9.3 can be set
"""
return self.__len__()
@length.setter
def length(self, length):
self.refid(self.fid, length)
@property
def gxtype(self):
"""
GX data type
.. versionadded:: 9.1
"""
return self._gxtype
@property
def dtype(self):
"""
numpy data type
.. versionadded:: 9.1
"""
return self._dtype
@property
def is_float(self):
""" True if a base float type, 32 or 64-bit"""
return self._is_float
@property
def is_float64(self):
"""
True if a base 64-bit float
.. versionadded:: 9.3.1
"""
if self.dtype == np.float64:
return True
return False
@property
def is_int(self):
""" True if a base integer type"""
return self._is_int
@property
def is_string(self):
""" True if a base string type"""
return self._is_string
@property
def dim(self):
"""Dimension of elements in the array, 1, 2 or 3."""
return self._dim
@property
def np(self):
"""
Numpy array of VV data, in the data type of the VV. Use :meth:`get_data` to get a numpy array
in another dtype.
Note that changing the data in the numpy array does NOT change the data in the VV. Use
`set_data` to change data in the VV.
.. versionadded:: 9.2
"""
return self.get_data()[0]
[docs] def get_data(self, dtype=None, start=0, n=None, float_dummies_to_nan=True):
"""
Return vv data in a numpy array
:param start: index of first value, must be >=0
:param n: number of values wanted
:param dtype: numpy data type wanted
:returns: (data, (fid_start, fid_incr))
.. versionadded:: 9.1
"""
if dtype is None:
dtype = self._dtype
else:
dtype = np.dtype(dtype)
if n is None:
n = self.length - start
else:
n = min((self.length - start), n)
if (n < 0) or (start < 0) or ((start >= self.length) and self.length > 0):
raise VVException(_t('Cannot get (start,n) ({},{}) from vv of length {}').format(start, n, self.length))
if (n == 0) or (self.length == 0):
npd = np.array([], dtype=dtype)
else:
# strings wanted
if dtype.type is np.str_:
sr = gxapi.str_ref()
npd = np.empty((n,), dtype=dtype)
for i in range(start, start + n):
self._gxvv.get_string(i, sr)
npd[i - start] = sr.value
# numeric wanted
else:
# strings to numeric
if self._gxtype < 0:
if np.issubclass_(dtype.type, np.integer):
vvd = gxapi.GXVV.create_ext(gxapi.GS_LONG, n)
else:
vvd = gxapi.GXVV.create_ext(gxapi.GS_DOUBLE, n)
vvd.copy(self._gxvv) # this will do the conversion
npd = vvd.get_data_np(start, n, dtype)
# numeric to numeric
else:
npd = self._get_data_np(start, n, dtype)
if float_dummies_to_nan:
if npd.dtype == np.float32 or npd.dtype == np.float64:
npd[npd == gxu.gx_dummy(npd.dtype)] = np.nan
fid = self.fid
start = fid[0] + start * fid[1]
return npd, (start, fid[1])
[docs] def set_data(self, data, fid=None):
"""
Set vv data from an iterable, which can be another `GXvv` instance. If the data is float type numpy.nan
\ are used to indicate dummy values.
:param data: data array of `GXvv` instance, will be reshapped to VV dimension
:param fid: fid tuple (start,increment), default does not change current fid
.. versionadded:: 9.1
.. versionchanged:: 9.3
default fid leaves fid unchanged
.. versionchanged:: 9.3.1
now accepts `GXvv` instance as the source data.
"""
if isinstance(data, GXvv):
data = data.np
elif not isinstance(data, np.ndarray):
data = np.array(data)
if data.size == 0:
self.length = 0
if fid:
self.fid = fid
return
if self.dim == 1:
data = data.flatten()
else:
data = data.reshape((-1, self.dim))
if data.size > gxapi.iMAX:
raise VVException(_t('data length {}, max allowed is {})').format(np.size(data), gxapi.iMAX))
# numerical data
if self._gxtype >= 0:
# strings
if gxu.gx_dtype(data.dtype) < 0:
i = 0
for s in data:
self._gxvv.set_double(i, gxu.rdecode(s))
i += 1
else:
if data.dtype == np.float32 or data.dtype == np.float64:
if np.isnan(data).any():
data = data.copy()
data[np.isnan(data)] = gxu.gx_dummy(data.dtype)
self._set_data_np(data)
# strings
else:
i = 0
for d in data:
self._gxvv.set_string(i, str(d))
i += 1
self._gxvv.set_len(data.shape[0])
if fid:
self.fid = fid
[docs] def refid(self, fid, length=None):
"""
Resample VV to a new fiducial and length
:param fid: (start, incr)
:param length: length, if not specified the length is calculated to the end of the data.
.. versionadded:: 9.1
.. versionchanged:: 9.3.1 added default length calculation
"""
if fid[1] <= 0.:
raise VVException(_t('fid increment must be greater than 0.'))
if length is None:
end_fid = self.fid[0] + self.fid[1] * (self.length - 1)
length = (((end_fid - fid[0]) + fid[1] * 0.5) // fid[1]) + 1
if length < 0:
length = 0
self._gxvv.re_fid(fid[0], fid[1], int(length))
self.fid = fid
[docs] def list(self):
"""
Return the content of the VV as a list.
.. versionadded:: 9.2
"""
return [v[0] for v in self]
[docs] def fill(self, value):
"""
Fill a vv with a constant value.
:param value: value to fill
.. versionadded:: 9.3.1
"""
if self.is_float:
self.gxvv.fill_double(float(value))
if self.is_int:
self.gxvv.fill_int(int(value))
else:
self.gxvv.fill_string(str(value))
[docs] def min_max(self):
"""
Return the minimum and maximum values as doubles. Strings are converted if possible.
:return: (minimum, maximum), or if all dummy, (None, None)
.. versionadded:: 9.3.1
"""
rmin = gxapi.float_ref()
rmax = gxapi.float_ref()
self._gxvv.range_double(rmin, rmax)
if rmin.value == gxapi.rDUMMY:
return (None, None)
return rmin.value, rmax.value