#!/usr/bin/env python
"""
Z
===
Container for the Impedance Tensor
Originally written by Jared Peacock Lars Krieger
Updated 2022 by J. Peacock to work with new framework
"""
# =============================================================================
# Imports
# =============================================================================
from __future__ import annotations
import copy
from typing import Any
import numpy as np
from . import IMPEDANCE_UNITS, MT_TO_OHM_FACTOR
from .base import TFBase
from .pt import PhaseTensor
from .tf_helpers import (
compute_impedance_error,
compute_phase,
compute_phase_error,
compute_resistivity,
compute_resistivity_error,
)
from .z_analysis import (
calculate_depth_of_investigation,
find_distortion,
remove_distortion_from_z_object,
ZInvariants,
)
# ==============================================================================
# Impedance Tensor Class
# ==============================================================================
[docs]
class Z(TFBase):
"""
Impedance tensor (Z) class.
Z is a complex array of the form (n_frequency, 2, 2) with indices:
- Zxx: (0,0)
- Zxy: (0,1)
- Zyx: (1,0)
- Zyy: (1,1)
All errors are given as standard deviations (sqrt(VAR)).
Parameters
----------
z : np.ndarray, optional
Array containing complex impedance values (n_frequency, 2, 2)
z_error : np.ndarray, optional
Array containing error values (standard deviation) of impedance
tensor elements (n_frequency, 2, 2)
frequency : np.ndarray, optional
Array of frequency values corresponding to impedance tensor
elements (n_frequency)
z_model_error : np.ndarray, optional
Array containing model error values (n_frequency, 2, 2)
units : str, optional
Units for impedance: 'mt' [mV/km/nT] or 'ohm' [Ohms], by default 'mt'
"""
def __init__(
self,
z: np.ndarray | None = None,
z_error: np.ndarray | None = None,
frequency: np.ndarray | None = None,
z_model_error: np.ndarray | None = None,
units: str | None = None,
input_units: str | None = None,
output_units: str | None = None,
) -> None:
"""
Initialize an instance of the Z class.
Parameters
----------
z : np.ndarray, optional
Array containing complex impedance values (n_frequency, 2, 2),
by default None
z_error : np.ndarray, optional
Array containing error values (standard deviation) of impedance
tensor elements (n_frequency, 2, 2), by default None
frequency : np.ndarray, optional
Array of frequency values corresponding to impedance tensor
elements (n_frequency), by default None
z_model_error : np.ndarray, optional
Array containing model error values (n_frequency, 2, 2),
by default None
units : str or None, optional
Backward-compatible alias for both input and output units,
by default None.
input_units : str or None, optional
Units of incoming impedance arrays (z, z_error, z_model_error),
by default None.
output_units : str or None, optional
Units for returned impedance arrays, by default None.
"""
self._ohm_factor = MT_TO_OHM_FACTOR
self._unit_factors = IMPEDANCE_UNITS
# Backward compatibility: if only `units` is provided, treat it as
# both input and output units.
if units is not None:
if input_units is None:
input_units = units
if output_units is None:
output_units = units
if input_units is None:
input_units = "mt"
if output_units is None:
output_units = "mt"
self.input_units = input_units
self.output_units = output_units
# Convert incoming values to internal mt units so storage is consistent
# in [mV/km/nT].
if z is not None:
z = z * self._input_scale_factor
if z_error is not None:
z_error = z_error * self._input_scale_factor
if z_model_error is not None:
z_model_error = z_model_error * self._input_scale_factor
super().__init__(
tf=z,
tf_error=z_error,
tf_model_error=z_model_error,
frequency=frequency,
_name="impedance",
)
@property
def input_units(self) -> str:
"""Units for incoming impedance arrays."""
return self._input_units
@input_units.setter
def input_units(self, value: str) -> None:
self._input_units = self._validate_units(value)
@property
def output_units(self) -> str:
"""Units for returned impedance arrays."""
return self._output_units
@output_units.setter
def output_units(self, value: str) -> None:
self._output_units = self._validate_units(value)
@property
def units(self) -> str:
"""Backward-compatible alias of output_units."""
return self.output_units
@units.setter
def units(self, value: str) -> None:
"""Set output units (backward-compatible alias)."""
self.output_units = value
def _validate_units(self, value: str) -> str:
"""Validate impedance units (options: 'mt' or 'ohm')."""
if not isinstance(value, str):
raise TypeError("Units input must be a string.")
if value.lower() not in self._unit_factors.keys():
raise ValueError(f"{value} is not an acceptable unit for impedance.")
return value.lower()
@property
def _input_scale_factor(self) -> float:
"""Unit scale factor for incoming arrays."""
return self._unit_factors[self.input_units]
@property
def _output_scale_factor(self) -> float:
"""Unit scale factor for returned arrays."""
return self._unit_factors[self.output_units]
@property
def _scale_factor(self) -> float:
"""Backward-compatible alias of output scale factor."""
return self._output_scale_factor
@property
def z(self) -> np.ndarray | None:
"""Impedance tensor array (nfrequency, 2, 2)."""
if self._has_tf():
return self._dataset.transfer_function.values / self._output_scale_factor
@z.setter
def z(self, z: np.ndarray | None) -> None:
"""
Set impedance tensor.
Parameters
----------
z : np.ndarray or None
Complex impedance tensor array (nfrequency, 2, 2) in mt units [mV/km/nT]
"""
old_shape = None
if self._has_tf():
old_shape = self._dataset.transfer_function.shape
elif self._has_frequency():
old_shape = (
self.frequency.size,
self._expected_shape[0],
self._expected_shape[1],
)
z = self._validate_array_input(z, "complex", old_shape)
if z is None:
return
z = z * self._input_scale_factor
if self._is_empty():
self._dataset = self._initialize(tf=z)
else:
self._dataset["transfer_function"].loc[self.comps] = z
# ----impedance error-----------------------------------------------------
@property
def z_error(self) -> np.ndarray | None:
"""Error of impedance tensor array as standard deviation."""
if self._has_tf_error():
return (
self._dataset.transfer_function_error.values / self._output_scale_factor
)
@z_error.setter
def z_error(self, z_error: np.ndarray | None) -> None:
"""
Set impedance tensor error.
Parameters
----------
z_error : np.ndarray or None
Error of impedance tensor array as standard deviation (nfrequency, 2, 2)
"""
old_shape = None
if not self._has_tf_error():
old_shape = self._dataset.transfer_function_error.shape
elif self._has_frequency():
old_shape = (
self.frequency.size,
self._expected_shape[0],
self._expected_shape[1],
)
z_error = self._validate_array_input(z_error, "float", old_shape)
if z_error is None:
return
z_error = z_error * self._input_scale_factor
if self._is_empty():
self._dataset = self._initialize(tf_error=z_error)
else:
self._dataset["transfer_function_error"].loc[self.comps] = z_error
# ----impedance model error-----------------------------------------------------
@property
def z_model_error(self) -> np.ndarray | None:
"""Model error of impedance tensor array as standard deviation."""
if self._has_tf_model_error():
return (
self._dataset.transfer_function_model_error.values
/ self._output_scale_factor
)
@z_model_error.setter
def z_model_error(self, z_model_error: np.ndarray | None) -> None:
"""
Set impedance tensor model error.
Parameters
----------
z_model_error : np.ndarray or None
Model error of impedance tensor array as standard deviation
(nfrequency, 2, 2)
"""
old_shape = None
if not self._has_tf_model_error():
old_shape = self._dataset.transfer_function_error.shape
elif self._has_frequency():
old_shape = (
self.frequency.size,
self._expected_shape[0],
self._expected_shape[1],
)
z_model_error = self._validate_array_input(z_model_error, "float", old_shape)
if z_model_error is None:
return
z_model_error = z_model_error * self._input_scale_factor
if self._is_empty():
self._dataset = self._initialize(tf_error=z_model_error)
else:
self._dataset["transfer_function_model_error"].loc[
self.comps
] = z_model_error
[docs]
def remove_ss(
self,
reduce_res_factor_x: float | list | np.ndarray = 1.0,
reduce_res_factor_y: float | list | np.ndarray = 1.0,
inplace: bool = False,
) -> "Z" | None:
"""
Remove static shift by providing correction factors.
Assume the original observed tensor Z is built by a static shift S
and an unperturbed "correct" Z0:
Z = S * Z0
Therefore the correct Z will be:
Z0 = S^(-1) * Z
Parameters
----------
reduce_res_factor_x : float or array-like, optional
Static shift factor to be applied to x components (z[:, 0, :]).
Assumed to be in resistivity scale, by default 1.0
reduce_res_factor_y : float or array-like, optional
Static shift factor to be applied to y components (z[:, 1, :]).
Assumed to be in resistivity scale, by default 1.0
inplace : bool, optional
Update the current object or return a new impedance, by default False
Returns
-------
Z or None
Corrected Z if inplace is False, None otherwise
"""
def _validate_factor_single(factor):
"""Validate factor single."""
try:
x_factor = float(factor)
except ValueError:
msg = f"factor must be a valid number not {factor}"
self.logger.error(msg)
raise ValueError(msg)
return np.repeat(x_factor, len(self.z))
def _validate_ss_input(factor):
"""Validate ss input."""
if not np.iterable(factor):
x_factor = _validate_factor_single(factor)
elif len(reduce_res_factor_x) == 1:
x_factor = _validate_factor_single(factor)
else:
x_factor = np.array(factor, dtype=float)
if len(x_factor) != len(self.z):
msg = (
f"Length of reduce_res_factor_x needs to be {len(self.z)} "
f" not {len(x_factor)}"
)
self.logger.error(msg)
raise ValueError(msg)
return x_factor
x_factors = np.sqrt(_validate_ss_input(reduce_res_factor_x))
y_factors = np.sqrt(_validate_ss_input(reduce_res_factor_y))
z_corrected = copy.copy(self.z)
z_corrected[:, 0, 0] = (self.z[:, 0, 0] * self._scale_factor) / x_factors
z_corrected[:, 0, 1] = (self.z[:, 0, 1] * self._scale_factor) / x_factors
z_corrected[:, 1, 0] = (self.z[:, 1, 0] * self._scale_factor) / y_factors
z_corrected[:, 1, 1] = (self.z[:, 1, 1] * self._scale_factor) / y_factors
z_corrected = z_corrected / self._scale_factor
if inplace:
self.z = z_corrected
else:
return Z(
z=z_corrected,
z_error=self.z_error,
frequency=self.frequency,
z_model_error=self.z_model_error,
units=self.output_units,
)
[docs]
def remove_distortion(
self,
distortion_tensor: np.ndarray | None = None,
distortion_error_tensor: np.ndarray | None = None,
n_frequencies: int | None = None,
comp: str = "det",
only_2d: bool = False,
inplace: bool = False,
) -> "Z" | None:
"""
Remove distortion D from observed impedance tensor Z.
Obtain the unperturbed "correct" Z0 from:
Z = D * Z0
Propagation of errors/uncertainties included.
Parameters
----------
distortion_tensor : np.ndarray, optional
Real distortion tensor (2, 2), by default None
distortion_error_tensor : np.ndarray, optional
Real distortion error tensor (2, 2), by default None
n_frequencies : int, optional
Number of frequencies to use for estimation, by default None
comp : str, optional
Component to use for estimation, by default 'det'
only_2d : bool, optional
Only use 2D data, by default False
inplace : bool, optional
Update the current object or return a new impedance, by default False
Returns
-------
Z or None
Impedance tensor with distortion removed if inplace is False,
None otherwise
"""
if distortion_tensor is None:
(
distortion_tensor,
distortion_error_tensor,
) = self.estimate_distortion(
n_frequencies=n_frequencies, comp=comp, only_2d=only_2d
)
z_corrected, z_corrected_error = remove_distortion_from_z_object(
self, distortion_tensor, distortion_error_tensor, self.logger
)
# into mt units
z_corrected = z_corrected * self._scale_factor
z_corrected_error = z_corrected_error * self._scale_factor
if inplace:
self.z = z_corrected
self.z_error = z_corrected_error
else:
z_object = Z(
z=z_corrected,
z_error=z_corrected_error,
frequency=self.frequency,
z_model_error=self.z_model_error,
input_units="mt",
output_units=self.output_units,
)
return z_object
@property
def resistivity(self) -> np.ndarray | None:
"""Resistivity of impedance."""
if self._has_tf():
return compute_resistivity(
self._dataset.transfer_function.values, self.frequency
)
@property
def phase(self) -> np.ndarray | None:
"""Phase of impedance."""
if self._has_tf():
return compute_phase(self._dataset.transfer_function.values)
@property
def resistivity_error(self) -> np.ndarray | None:
"""
Resistivity error of impedance.
By standard error propagation, relative error in resistivity is
2 * relative error in z amplitude.
"""
if self._has_tf() and self._has_tf_error():
return compute_resistivity_error(
self._dataset.transfer_function.values,
self._dataset.transfer_function_error.values,
self.frequency,
)
@property
def phase_error(self) -> np.ndarray | None:
"""
Phase error of impedance.
Uncertainty in phase (in degrees) is computed by defining a circle around
the z vector in the complex plane. The uncertainty is the absolute angle
between the vector to (x,y) and the vector between the origin and the
tangent to the circle.
"""
if self._has_tf() and self._has_tf_error():
return compute_phase_error(
self._dataset.transfer_function.values,
self._dataset.transfer_function_error.values,
)
@property
def resistivity_model_error(self) -> np.ndarray | None:
"""Resistivity model error of impedance."""
if self._has_tf() and self._has_tf_model_error():
return compute_resistivity_error(
self._dataset.transfer_function.values,
self._dataset.transfer_function_model_error.values,
self.frequency,
)
@property
def phase_model_error(self) -> np.ndarray | None:
"""Phase model error of impedance."""
if self._has_tf() and self._has_tf_model_error():
return compute_phase_error(
self._dataset.transfer_function.values,
self._dataset.transfer_function_model_error.values,
)
def _compute_z_error(
self, res_error: np.ndarray | None, phase_error: np.ndarray | None
) -> np.ndarray | None:
"""
Compute impedance error from apparent resistivity and phase errors.
Parameters
----------
res_error : np.ndarray or None
Resistivity error array
phase_error : np.ndarray or None
Phase error array in degrees
Returns
-------
np.ndarray or None
Impedance error array
"""
return compute_impedance_error(res_error, phase_error, self.frequency)
[docs]
def set_resistivity_phase(
self,
resistivity: np.ndarray,
phase: np.ndarray,
frequency: np.ndarray,
res_error: np.ndarray | None = None,
phase_error: np.ndarray | None = None,
res_model_error: np.ndarray | None = None,
phase_model_error: np.ndarray | None = None,
) -> None:
"""
Set values for resistivity and phase with error propagation.
Parameters
----------
resistivity : np.ndarray
Resistivity array in Ohm-m (num_frequency, 2, 2)
phase : np.ndarray
Phase array in degrees (num_frequency, 2, 2)
frequency : np.ndarray
Frequency array in Hz (num_frequency)
res_error : np.ndarray, optional
Resistivity error array in Ohm-m (num_frequency, 2, 2), by default None
phase_error : np.ndarray, optional
Phase error array in degrees (num_frequency, 2, 2), by default None
res_model_error : np.ndarray, optional
Resistivity model error array in Ohm-m (num_frequency, 2, 2),
by default None
phase_model_error : np.ndarray, optional
Phase model error array in degrees (num_frequency, 2, 2),
by default None
"""
if resistivity is None or phase is None or frequency is None:
self.logger.debug(
"Cannot estimate resitivity and phase if resistivity, "
"phase, or frequency is None."
)
return
self.frequency = self._validate_frequency(frequency)
resistivity = self._validate_array_input(resistivity, float)
phase = self._validate_array_input(phase, float)
res_error = self._validate_array_input(res_error, float)
phase_error = self._validate_array_input(phase_error, float)
res_model_error = self._validate_array_input(res_model_error, float)
phase_model_error = self._validate_array_input(phase_model_error, float)
abs_z = np.sqrt(5.0 * self.frequency * (resistivity.T)).T
self.z = abs_z * np.exp(1j * np.radians(phase))
self.z_error = self._compute_z_error(res_error, phase_error)
self.z_model_error = self._compute_z_error(res_model_error, phase_model_error)
@property
def det(self) -> np.ndarray | None:
"""Determinant of impedance."""
if self.z is not None:
det_z = np.array(
[np.linalg.det(ii * self._scale_factor) ** 0.5 for ii in self.z]
)
return det_z
@property
def det_error(self) -> np.ndarray | None:
"""Determinant of impedance error."""
det_z_error = None
if self.z_error is not None:
det_z_error = np.zeros_like(self.det, dtype=float)
with np.errstate(invalid="ignore"):
# components of the impedance tensor are not independent variables
# so can't use standard error propagation
# calculate manually:
# difference of determinant of z + z_error and z - z_error then divide by 2
det_z_error[:] = (
self._scale_factor
* (
np.abs(
np.linalg.det(self.z + self.z_error)
- np.linalg.det(self.z - self.z_error)
)
/ 2.0
)
** 0.5
)
return det_z_error
@property
def det_model_error(self) -> np.ndarray | None:
"""Determinant of impedance model error."""
det_z_error = None
if self.z_model_error is not None:
det_z_error = np.zeros_like(self.det, dtype=float)
with np.errstate(invalid="ignore"):
# components of the impedance tensor are not independent variables
# so can't use standard error propagation
# calculate manually:
# difference of determinant of z + z_error and z - z_error then divide by 2
det_z_error[:] = (
np.abs(
np.linalg.det(self.z + self.z_model_error)
- np.linalg.det(self.z - self.z_model_error)
)
/ 2.0
) ** 0.5
return det_z_error
@property
def phase_det(self) -> np.ndarray | None:
"""Phase determinant."""
if self.det is not None:
return np.rad2deg(np.arctan2(self.det.imag, self.det.real))
@property
def phase_error_det(self) -> np.ndarray | None:
"""Phase error determinant."""
if self.det is not None:
return np.rad2deg(np.arcsin(self.det_error / abs(self.det)))
@property
def phase_model_error_det(self) -> np.ndarray | None:
"""Phase model error determinant."""
if self.det is not None:
return np.rad2deg(np.arcsin(self.det_model_error / abs(self.det)))
@property
def res_det(self) -> np.ndarray | None:
"""Resistivity determinant."""
if self.det is not None:
return (
0.2 * (1.0 / self.frequency) * abs(self.det / self._scale_factor) ** 2
)
@property
def res_error_det(self) -> np.ndarray | None:
"""Resistivity error determinant."""
if self.det_error is not None:
return (
0.2
* (1.0 / self.frequency)
* np.abs((self.det + self.det_error) / self._scale_factor) ** 2
- self.res_det
)
@property
def res_model_error_det(self) -> np.ndarray | None:
"""Resistivity model error determinant."""
if self.det_model_error is not None:
return (
0.2
* (1.0 / self.frequency)
* np.abs((self.det + self.det_model_error) / self._scale_factor) ** 2
- self.res_det
)
def _get_component(self, comp: str, array: np.ndarray | None) -> np.ndarray | None:
"""
Get the correct component from an array.
Parameters
----------
comp : str
Component name: 'xx', 'xy', 'yx', or 'yy'
array : np.ndarray or None
Impedance array
Returns
-------
np.ndarray or None
Array component
"""
if array is not None:
index_dict = {"x": 0, "y": 1}
ii = index_dict[comp[-2]]
jj = index_dict[comp[-1]]
return array[:, ii, jj]
@property
def res_xx(self) -> np.ndarray | None:
"""Resistivity of xx component."""
return self._get_component("xx", self.resistivity)
@property
def res_xy(self) -> np.ndarray | None:
"""Resistivity of xy component."""
return self._get_component("xy", self.resistivity)
@property
def res_yx(self) -> np.ndarray | None:
"""Resistivity of yx component."""
return self._get_component("yx", self.resistivity)
@property
def res_yy(self) -> np.ndarray | None:
"""Resistivity of yy component."""
return self._get_component("yy", self.resistivity)
@property
def res_error_xx(self) -> np.ndarray | None:
"""Resistivity error of xx component."""
return self._get_component("xx", self.resistivity_error)
@property
def res_error_xy(self) -> np.ndarray | None:
"""Resistivity error of xy component."""
return self._get_component("xy", self.resistivity_error)
@property
def res_error_yx(self) -> np.ndarray | None:
"""Resistivity error of yx component."""
return self._get_component("yx", self.resistivity_error)
@property
def res_error_yy(self) -> np.ndarray | None:
"""Resistivity error of yy component."""
return self._get_component("yy", self.resistivity_error)
@property
def res_model_error_xx(self) -> np.ndarray | None:
"""Resistivity model error of xx component."""
return self._get_component("xx", self.resistivity_model_error)
@property
def res_model_error_xy(self) -> np.ndarray | None:
"""Resistivity model error of xy component."""
return self._get_component("xy", self.resistivity_model_error)
@property
def res_model_error_yx(self) -> np.ndarray | None:
"""Resistivity model error of yx component."""
return self._get_component("yx", self.resistivity_model_error)
@property
def res_model_error_yy(self) -> np.ndarray | None:
"""Resistivity model error of yy component."""
return self._get_component("yy", self.resistivity_model_error)
@property
def phase_xx(self) -> np.ndarray | None:
"""Phase of xx component."""
return self._get_component("xx", self.phase)
@property
def phase_xy(self) -> np.ndarray | None:
"""Phase of xy component."""
return self._get_component("xy", self.phase)
@property
def phase_yx(self) -> np.ndarray | None:
"""Phase of yx component."""
return self._get_component("yx", self.phase)
@property
def phase_yy(self) -> np.ndarray | None:
"""Phase of yy component."""
return self._get_component("yy", self.phase)
@property
def phase_error_xx(self) -> np.ndarray | None:
"""Phase error of xx component."""
return self._get_component("xx", self.phase_error)
@property
def phase_error_xy(self) -> np.ndarray | None:
"""Phase error of xy component."""
return self._get_component("xy", self.phase_error)
@property
def phase_error_yx(self) -> np.ndarray | None:
"""Phase error of yx component."""
return self._get_component("yx", self.phase_error)
@property
def phase_error_yy(self) -> np.ndarray | None:
"""Phase error of yy component."""
return self._get_component("yy", self.phase_error)
@property
def phase_model_error_xx(self) -> np.ndarray | None:
"""Phase model error of xx component."""
return self._get_component("xx", self.phase_model_error)
@property
def phase_model_error_xy(self) -> np.ndarray | None:
"""Phase model error of xy component."""
return self._get_component("xy", self.phase_model_error)
@property
def phase_model_error_yx(self) -> np.ndarray | None:
"""Phase model error of yx component."""
return self._get_component("yx", self.phase_model_error)
@property
def phase_model_error_yy(self) -> np.ndarray | None:
"""Phase model error of yy component."""
return self._get_component("yy", self.phase_model_error)
@property
def phase_tensor(self) -> PhaseTensor:
"""Phase tensor object based on impedance."""
return PhaseTensor(
z=self.z,
z_error=self.z_error,
z_model_error=self.z_model_error,
frequency=self.frequency,
)
@property
def invariants(self) -> ZInvariants:
"""Weaver invariants."""
return ZInvariants(z=self.z)
[docs]
def estimate_dimensionality(
self, skew_threshold: float = 5, eccentricity_threshold: float = 0.1
) -> np.ndarray:
"""
Estimate dimensionality of the impedance tensor.
Based on parameters such as strike and phase tensor eccentricity.
Parameters
----------
skew_threshold : float, optional
Skew threshold for 3D determination, by default 5
eccentricity_threshold : float, optional
Eccentricity threshold for 2D determination, by default 0.1
Returns
-------
np.ndarray
Dimensionality array (1D, 2D, or 3D) for each period
"""
dimensionality = np.ones(self.period.size, dtype=int)
# need to get 2D first then 3D
dimensionality[
np.where(self.phase_tensor.eccentricity > eccentricity_threshold)
] = 2
dimensionality[np.where(np.abs(self.phase_tensor.skew) > skew_threshold)] = 3
return dimensionality
[docs]
def estimate_distortion(
self,
n_frequencies: int | None = None,
comp: str = "det",
only_2d: bool = False,
clockwise: bool = True,
) -> tuple[np.ndarray, np.ndarray]:
"""
Estimate distortion tensor.
Parameters
----------
n_frequencies : int, optional
Number of frequencies to use, by default None (uses all)
comp : str, optional
Component to use for estimation, by default 'det'
only_2d : bool, optional
Only use 2D data, by default False
clockwise : bool, optional
Clockwise rotation, by default True
Returns
-------
tuple of np.ndarray
Distortion tensor and distortion error tensor
"""
if n_frequencies is None:
nf = self.frequency.size
else:
nf = n_frequencies
if self._has_tf():
new_z_object = Z(
z=self._dataset.transfer_function.values[0:nf, :, :],
frequency=self.frequency[0:nf],
)
if self._has_tf_error():
new_z_object.z_error = self._dataset.transfer_function_error.values[
0:nf
]
return find_distortion(
new_z_object, comp=comp, only_2d=only_2d, clockwise=clockwise
)
[docs]
def estimate_depth_of_investigation(self) -> Any:
"""
Estimate depth of investigation.
Returns
-------
Any
Depth of investigation results
"""
return calculate_depth_of_investigation(self)