# -*- coding: utf-8 -*-
"""
Created on Tue Oct 11 16:01:37 2022
@author: jpeacock
"""
# =============================================================================
# Imports
# =============================================================================
import numpy as np
# =============================================================================
[docs]
class ModelErrors:
def __init__(self, data=None, measurement_error=None, **kwargs):
"""Init function."""
self._functions = {
"egbert": self.compute_geometric_mean_error,
"geometric_mean": self.compute_geometric_mean_error,
"arithmetic_mean": self.compute_arithmetic_mean_error,
"row": self.compute_row_error,
"mean_od": self.compute_arithmetic_mean_error,
"median": self.compute_median_error,
"eigen": self.compute_eigen_value_error,
"percent": self.compute_percent_error,
"absolute": self.compute_absolute_error,
"abs": self.compute_absolute_error,
"data": self.use_measurement_error,
}
self._array_shapes = {
"impedance": (2, 2),
"z": (2, 2),
"transfer_function": (3, 2),
"tipper": (1, 2),
"t": (1, 2),
}
self.error_value = 5
self.error_type = "percent"
self.floor = True
self.mode = "impedance"
for key, value in kwargs.items():
setattr(self, key, value)
self.data = data
self.measurement_error = measurement_error
def __str__(self):
"""Str function."""
lines = ["Model Errors:", "-" * 20]
lines += [f"\terror_type: {self.error_type}"]
lines += [f"\terror_value: {self.error_value}"]
lines += [f"\tfloor: {self.floor}"]
lines += [f"\tmode: {self.mode}"]
return "\n".join(lines)
def __repr__(self):
"""Repr function."""
return self.__str__()
def __eq__(self, other):
"""Eq function."""
if not isinstance(other, ModelErrors):
raise TypeError(f"Cannot compare ModelErrors to type {type(other)}")
for key in ["error_value", "error_type", "floor", "mode"]:
value_og = getattr(self, key)
value_other = getattr(other, key)
if value_og != value_other:
return False
return True
[docs]
def validate_percent(self, value):
"""Make sure the percent is a decimal.
:param value: DESCRIPTION.
:type value: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
if value >= 1:
value /= 100.0
return value
@property
def error_parameters(self):
"""Error parameters."""
return {
"error_value": self.error_value,
"error_type": self.error_type,
"floor": self.floor,
}
@property
def error_type(self):
"""Error type."""
return self._error_type
@error_type.setter
def error_type(self, value):
"""Error type."""
if value not in self._functions.keys():
raise NotImplementedError(f"Error Type {value} not supported.")
self._error_type = value
@property
def floor(self):
"""Floor function."""
return self._floor
@floor.setter
def floor(self, value):
"""Floor function."""
if value not in [False, True]:
raise ValueError("Floor must be True or False")
self._floor = value
@property
def error_value(self):
"""Error value."""
return self._error_value
@error_value.setter
def error_value(self, value):
"""Error value."""
self._error_value = self.validate_percent(value)
@property
def mode(self):
"""Mode function."""
return self._mode
@mode.setter
def mode(self, value):
"""Mode function."""
if value not in self._array_shapes.keys():
raise NotImplementedError(f"Mode {value} not supported.")
self._mode = value
def _get_shape(self):
"""Get shape."""
try:
return self._array_shapes[self.mode]
except KeyError:
raise NotImplementedError(f"Mode {self.mode} not supported.")
[docs]
def validate_array_shape(self, data):
"""Validate array shape.
:param data: DESCRIPTION.
:type data: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
if not isinstance(data, np.ndarray):
data = np.array(data)
expected_shape = self._get_shape()
if data.shape == expected_shape:
data = data.reshape((1, expected_shape[0], expected_shape[1]))
if data.shape[1] != expected_shape[0] or data.shape[2] != expected_shape[1]:
raise ValueError(
f"Shape {data.shape} is not expected shape of (n, "
f"{expected_shape[0]}, {expected_shape[1]})"
)
return data
@property
def data(self):
"""Data function."""
return self._data
@data.setter
def data(self, value):
"""Data function."""
if value is not None:
self._data = self.validate_array_shape(value)
else:
self._data = None
@property
def measurement_error(self):
"""Measurement error."""
return self._measurement_error
@measurement_error.setter
def measurement_error(self, value):
"""Measurement error."""
if value is not None:
self._measurement_error = self.validate_array_shape(value)
else:
self._measurement_error = None
[docs]
def mask_zeros(self, data):
"""Mask zeros.
:param data: DESCRIPTION.
:type data: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
dshape = data.shape
data = np.nan_to_num(data).reshape(dshape)
return np.ma.masked_equal(data, 0)
[docs]
def resize_output(self, error_array):
"""Resize the error estimtion to the same size as the input data.
:param error_array: DESCRIPTION.
:type error_array: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
if error_array.shape != self.data.shape:
if error_array.shape[0] == self.data.shape[0]:
err = np.zeros_like(self.data, dtype=float)
for index in range(self.data.shape[0]):
err[index] = error_array[index]
return err
return error_array
[docs]
def set_floor(self, error_array):
"""Set error floor.
:param error_array:
:param array: DESCRIPTION.
:type array: TYPE
:param floor: DESCRIPTION.
:type floor: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
if self.measurement_error is not None:
index = np.where(error_array < self.measurement_error)
error_array[index] = self.measurement_error[index]
return error_array
[docs]
def use_measurement_error(self):
"""Use measurement error."""
return self.measurement_error
[docs]
def compute_percent_error(self):
"""Percent error.
:param data: DESCRIPTION.
:type data: TYPE
:param percent: DESCRIPTION.
:type percent: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
err = self.error_value * np.abs(self.data)
if self.floor:
err = self.set_floor(err)
return err
[docs]
def compute_arithmetic_mean_error(self):
"""Error_value * (Zxy + Zyx) / 2.
:param data: DESCRIPTION.
:type data: TYPE
:param error_value: DESCRIPTION.
:type error_value: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
if self.data.shape[1] < 2:
od = self.mask_zeros(np.array([self.data[:, 0, 0], self.data[:, 0, 1]]))
else:
od = self.mask_zeros(np.array([self.data[:, 0, 1], self.data[:, 1, 0]]))
err = self.resize_output(self.error_value * np.ma.mean(np.ma.abs(od), axis=0))
if self.floor:
err = self.set_floor(err)
if isinstance(err, np.ma.core.MaskedArray):
return err.data
return err
[docs]
def compute_eigen_value_error(self):
"""Error_value * eigen(data).mean().
:param data: DESCRIPTION.
:type data: TYPE
:param error_value: DESCRIPTION.
:type error_value: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
if self.data.shape[1] < 2:
raise IndexError(
"Cannot compute eigen value error with an array of shape "
f"{self.data.shape}"
)
data = self.mask_zeros(self.data)
try:
err = self.error_value * np.abs(np.linalg.eigvals(data)).mean(axis=1)
except Exception:
data_shape = data.shape
err = (
self.error_value
* np.abs(
np.linalg.eigvals(np.nan_to_num(data).reshape(data_shape))
).mean()
)
if np.atleast_1d(err).sum(axis=0) == 0:
err = self.error_value * data[np.nonzero(data)].mean()
err = self.resize_output(err)
if self.floor:
err = self.set_floor(err)
return err
[docs]
def compute_geometric_mean_error(self):
"""Error_value * sqrt(Zxy * Zyx).
:param data: DESCRIPTION.
:type data: TYPE
:param error_value: DESCRIPTION.
:type error_value: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
data = self.data.copy()
if self.data.shape[1] < 2:
zero_xy = np.where(data[:, 0, 0] == 0)
data[zero_xy, 0, 0] = data[zero_xy, 0, 0]
zero_yx = np.where(data[:, 0, 1] == 0)
data[zero_yx, 0, 1] = data[zero_yx, 0, 1]
data = self.mask_zeros(data)
err = self.resize_output(
self.error_value * np.ma.sqrt(np.ma.abs(data[:, 0, 0] * data[:, 0, 1]))
)
else:
zero_xy = np.where(data[:, 0, 1] == 0)
data[zero_xy, 0, 1] = data[zero_xy, 1, 0]
zero_yx = np.where(data[:, 1, 0] == 0)
data[zero_yx, 1, 0] = data[zero_yx, 0, 1]
data = self.mask_zeros(data)
err = self.resize_output(
self.error_value * np.ma.sqrt(np.ma.abs(data[:, 0, 1] * data[:, 1, 0]))
)
if self.floor:
err = self.set_floor(err)
if isinstance(err, np.ma.core.MaskedArray):
return err.data
return err
[docs]
def compute_row_error(self):
"""Set zxx and zxy the same error and zyy and zyx the same error.
:param data: DESCRIPTION.
:type data: TYPE
:param error_value: DESCRIPTION.
:type error_value: TYPE
:param floor: DESCRIPTION, defaults to True.
:type floor: TYPE, optional
:return: DESCRIPTION.
:rtype: TYPE
"""
if self.data.shape[1] < 2:
err_xy = np.abs(self.data[:, 0, 0]) * self.error_value
err_yx = np.abs(self.data[:, 0, 1]) * self.error_value
err = np.zeros_like(self.data, dtype=float)
err[:, 0, 0] = err_xy
err[:, 0, 1] = err_yx
else:
err_xy = np.abs(self.data[:, 0, 1]) * self.error_value
err_yx = np.abs(self.data[:, 1, 0]) * self.error_value
err = np.zeros_like(self.data, dtype=float)
err[:, 0, 0] = err_xy
err[:, 0, 1] = err_xy
err[:, 1, 0] = err_yx
err[:, 1, 1] = err_yx
err = self.resize_output(err)
if self.floor:
err = self.set_floor(err)
return err
[docs]
def compute_absolute_error(self):
"""Compute absolute error.
:param data: DESCRIPTION.
:type data: TYPE
:param error_value: DESCRIPTION.
:type error_value: TYPE
:return: DESCRIPTION.
:rtype: TYPE
"""
err = np.ones_like(self.data, dtype=float) * self.error_value
if self.floor:
err = self.set_floor(err)
return err
[docs]
def compute_error(self, data=None, error_type=None, error_value=None, floor=None):
"""Compute error.
:param data: DESCRIPTION, defaults to None.
:type data: TYPE, optional
:param error_type: DESCRIPTION, defaults to None.
:type error_type: TYPE, optional
:param error_value: DESCRIPTION, defaults to None.
:type error_value: TYPE, optional
:param floor: DESCRIPTION, defaults to None.
:type floor: TYPE, optional
:return: DESCRIPTION.
:rtype: TYPE
"""
if data is not None:
self.data = data
if error_type is not None:
self.error_type = error_type
if error_value is not None:
self.error_value = error_value
if floor is not None:
self.floor = floor
return self._functions[self.error_type]()