"""xarray Dataset accessor for transfer-function representations."""
from __future__ import annotations
from typing import Any
import numpy as np
import scipy.interpolate
import xarray as xr
from loguru import logger
from . import IMPEDANCE_UNITS
from .pt import PhaseTensor
from .tf_helpers import (
compute_phase,
compute_phase_error,
compute_phase_tensor,
compute_phase_tensor_error,
compute_pt_alpha,
compute_pt_alpha_error,
compute_pt_azimuth,
compute_pt_azimuth_error,
compute_pt_beta,
compute_pt_beta_error,
compute_pt_det,
compute_pt_det_error,
compute_pt_eccentricity,
compute_pt_eccentricity_error,
compute_pt_ellipticity,
compute_pt_ellipticity_error,
compute_pt_phimax,
compute_pt_phimax_error,
compute_pt_phimin,
compute_pt_phimin_error,
compute_pt_skew,
compute_pt_skew_error,
compute_pt_trace,
compute_pt_trace_error,
compute_resistivity,
compute_resistivity_error,
compute_tipper_amp_phase_error,
compute_tipper_amplitude,
compute_tipper_angle_error,
compute_tipper_angle_imag,
compute_tipper_angle_real,
compute_tipper_magnitude_error,
compute_tipper_magnitude_imag,
compute_tipper_magnitude_real,
compute_tipper_phase,
)
from .tipper import Tipper
from .z import Z
[docs]
@xr.register_dataset_accessor("tf")
class TFDatasetAccessor:
"""Transfer-function accessor for xarray datasets.
The accessor exposes impedance, tipper, and phase-tensor views from an
``xr.Dataset`` with transfer-function variables and provides convenience
mutation methods for common MT workflows.
Notes
-----
The underlying dataset is expected to contain transfer-function variables
with dimensions ``('period', 'output', 'input')`` and corresponding period
coordinates.
Examples
--------
>>> ds.tf.validate()
>>> z_obj = ds.tf.to_z()
>>> rotated = ds.tf.rotate(15)
"""
_REQUIRED_VARIABLES = (
"transfer_function",
"transfer_function_error",
"transfer_function_model_error",
)
def __init__(self, xarray_obj: xr.Dataset) -> None:
self._obj = xarray_obj
self._cache: dict[str, Any] = {}
@staticmethod
def _resolve_frequency_period(
frequency: np.ndarray | None = None,
period: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
"""Resolve and validate frequency/period inputs."""
if frequency is not None and period is not None:
raise ValueError("Provide either frequency or period, not both.")
if period is not None:
period = np.asarray(period, dtype=float)
return 1.0 / period, period
if frequency is not None:
frequency = np.asarray(frequency, dtype=float)
return frequency, 1.0 / frequency
return None, None
@staticmethod
def _validate_input_mode(
obj: Any,
array_value: np.ndarray | None,
label: str,
) -> None:
"""Validate mutually exclusive object vs array input mode."""
if obj is not None and array_value is not None:
raise ValueError(
f"Provide either {label}_obj or {label} array inputs, not both."
)
def _validate_period_alignment(self, period: np.ndarray) -> None:
"""Require exact period alignment for in-place channel updates."""
if "period" not in self._obj.coords:
raise KeyError("Dataset missing required coord: period")
current = np.asarray(self._obj.coords["period"].values, dtype=float)
if current.shape != period.shape or not np.allclose(current, period):
raise ValueError(
"Input period does not match Dataset period. "
"Interpolate data first, then update values."
)
def _target_dataset(self, inplace: bool) -> xr.Dataset:
"""Return target dataset honoring inplace behavior."""
return self._obj if inplace else self._obj.copy(deep=True)
@staticmethod
def _get_labels_for_update(
ds: xr.Dataset,
output_candidates: list[str],
input_candidates: list[str],
n_output: int,
n_input: int,
) -> tuple[list[Any], list[Any]]:
"""Select output/input labels to update for TF subsets."""
outputs = list(ds.coords["output"].values)
inputs = list(ds.coords["input"].values)
out_labels = TFDatasetAccessor._pick_channel_labels(
outputs, output_candidates, n_output
)
in_labels = TFDatasetAccessor._pick_channel_labels(
inputs, input_candidates, n_input
)
if out_labels is None and len(outputs) == n_output:
out_labels = outputs[:n_output]
if in_labels is None and len(inputs) == n_input:
in_labels = inputs[:n_input]
if out_labels is None or in_labels is None:
raise ValueError("Could not determine channels to update in Dataset.")
return out_labels, in_labels
@staticmethod
def _update_tf_subset(
ds: xr.Dataset,
out_labels: list[Any],
in_labels: list[Any],
tf: np.ndarray,
tf_error: np.ndarray,
tf_model_error: np.ndarray,
) -> xr.Dataset:
"""Update transfer-function variables for a channel subset."""
indexer = {
"period": ds.coords["period"].values,
"output": out_labels,
"input": in_labels,
}
ds["transfer_function"].loc[indexer] = tf
ds["transfer_function_error"].loc[indexer] = tf_error
ds["transfer_function_model_error"].loc[indexer] = tf_model_error
return ds
[docs]
def validate(self) -> bool:
"""Validate dataset structure required by the accessor.
Returns
-------
bool
``True`` when required variables and coordinates are present.
Raises
------
KeyError
If required transfer-function variables, dimensions, or coordinates
are missing.
"""
for variable in self._REQUIRED_VARIABLES:
if variable not in self._obj.data_vars:
raise KeyError(f"Dataset missing required variable: {variable}")
tf = self._obj["transfer_function"]
for dim in ("period", "output", "input"):
if dim not in tf.dims:
raise KeyError(
"Dataset transfer_function must have dims "
"('period', 'output', 'input')"
)
if "period" not in self._obj.coords:
raise KeyError("Dataset missing required coord: period")
return True
@property
def frequency(self) -> np.ndarray:
"""Frequency array derived from period coordinates.
Returns
-------
np.ndarray
Frequency array derived from period coordinates.
"""
period = np.asarray(self._obj.coords["period"].values, dtype=float)
return 1.0 / period
@property
def impedance_units(self) -> str:
"""Impedance units for accessor outputs.
Returns
-------
str
Impedance units for accessor outputs.
"""
units = str(self._obj.attrs.get("impedance_units", "mt")).lower()
if units not in IMPEDANCE_UNITS:
return "mt"
return units
@property
def units(self) -> str:
"""Alias for impedance units to match Z API naming.
Returns
-------
str
Alias for impedance units to match Z API naming.
"""
return self.impedance_units
@staticmethod
def _pick_channel_labels(
available: list[Any], candidates: list[str], required: int
) -> list[Any] | None:
"""Pick ordered coordinate labels using preferred candidate names."""
channel_map = {str(label).lower(): label for label in available}
selected: list[Any] = []
for candidate in candidates:
key = candidate.lower()
if key in channel_map and channel_map[key] not in selected:
selected.append(channel_map[key])
if len(selected) == required:
return selected
return None
def _impedance_dataarray(self, variable: str) -> xr.DataArray:
"""Return impedance subset from a transfer-function variable."""
self.validate()
da = self._obj[variable]
outputs = list(self._obj.coords["output"].values)
inputs = list(self._obj.coords["input"].values)
out_labels = self._pick_channel_labels(outputs, ["ex", "ey", "x", "y"], 2)
in_labels = self._pick_channel_labels(inputs, ["hx", "hy", "x", "y"], 2)
if out_labels is None and da.sizes.get("output", 0) == 2:
out_labels = list(self._obj.coords["output"].values[:2])
if in_labels is None and da.sizes.get("input", 0) == 2:
in_labels = list(self._obj.coords["input"].values[:2])
if out_labels is None or in_labels is None:
raise ValueError(
"Could not determine impedance channels from output/input coords."
)
return da.sel(output=out_labels, input=in_labels)
def _tipper_dataarray(self, variable: str) -> xr.DataArray:
"""Return tipper subset from a transfer-function variable."""
self.validate()
da = self._obj[variable]
outputs = list(self._obj.coords["output"].values)
inputs = list(self._obj.coords["input"].values)
out_labels = self._pick_channel_labels(outputs, ["hz", "z"], 1)
in_labels = self._pick_channel_labels(inputs, ["hx", "hy", "x", "y"], 2)
if out_labels is None and da.sizes.get("output", 0) == 1:
out_labels = list(self._obj.coords["output"].values[:1])
if in_labels is None and da.sizes.get("input", 0) == 2:
in_labels = list(self._obj.coords["input"].values[:2])
if out_labels is None or in_labels is None:
raise ValueError(
"Could not determine tipper channels from output/input coords."
)
return da.sel(output=out_labels, input=in_labels)
@staticmethod
def _unit_factor(units: str) -> float:
"""Return conversion factor to convert internal mt units to requested units."""
key = units.lower()
if key not in IMPEDANCE_UNITS:
raise ValueError(
f"{units} is not an acceptable unit for impedance. "
f"Options are {list(IMPEDANCE_UNITS.keys())}."
)
return float(IMPEDANCE_UNITS[key])
[docs]
def z(self, units: str | None = None) -> np.ndarray:
"""Return impedance tensor values.
Parameters
----------
units : str or None, default=None
Output impedance units. If ``None``, uses dataset attribute
``impedance_units`` (falling back to ``"mt"``).
Returns
-------
numpy.ndarray
Complex impedance tensor array with shape
``(n_period, 2, 2)``.
"""
resolved_units = self.impedance_units if units is None else units.lower()
factor = self._unit_factor(resolved_units)
return self._impedance_dataarray("transfer_function").values / factor
[docs]
def z_error(self, units: str | None = None) -> np.ndarray:
"""Return impedance standard-deviation errors.
Parameters
----------
units : str or None, default=None
Output impedance units. If ``None``, uses dataset attribute
``impedance_units``.
Returns
-------
numpy.ndarray
Impedance error array with shape ``(n_period, 2, 2)``.
"""
resolved_units = self.impedance_units if units is None else units.lower()
factor = self._unit_factor(resolved_units)
return self._impedance_dataarray("transfer_function_error").values / factor
[docs]
def z_model_error(self, units: str | None = None) -> np.ndarray:
"""Return impedance model errors.
Parameters
----------
units : str or None, default=None
Output impedance units. If ``None``, uses dataset attribute
``impedance_units``.
Returns
-------
numpy.ndarray
Impedance model-error array with shape ``(n_period, 2, 2)``.
"""
resolved_units = self.impedance_units if units is None else units.lower()
factor = self._unit_factor(resolved_units)
return (
self._impedance_dataarray("transfer_function_model_error").values / factor
)
[docs]
def tipper(self) -> np.ndarray:
"""Return tipper tensor values.
Returns
-------
numpy.ndarray
Complex tipper array with shape ``(n_period, 1, 2)``.
"""
return self._tipper_dataarray("transfer_function").values
[docs]
def tipper_error(self) -> np.ndarray:
"""Return tipper standard-deviation errors.
Returns
-------
numpy.ndarray
Tipper error array with shape ``(n_period, 1, 2)``.
"""
return self._tipper_dataarray("transfer_function_error").values
[docs]
def tipper_model_error(self) -> np.ndarray:
"""Return tipper model errors.
Returns
-------
numpy.ndarray
Tipper model-error array with shape ``(n_period, 1, 2)``.
"""
return self._tipper_dataarray("transfer_function_model_error").values
@staticmethod
def _get_tipper_component(comp: str, array: np.ndarray | None) -> np.ndarray | None:
"""Return one tipper component from a (n_period, 1, 2) array stack."""
if array is None:
return None
index_dict = {"zx": 0, "zy": 1}
return array[:, 0, index_dict[comp.lower()]]
def _z_mt(self) -> np.ndarray:
"""Return impedance tensor in internal mt units."""
return self._impedance_dataarray("transfer_function").values
def _z_error_mt(self) -> np.ndarray:
"""Return impedance error tensor in internal mt units."""
return self._impedance_dataarray("transfer_function_error").values
def _z_model_error_mt(self) -> np.ndarray:
"""Return impedance model-error tensor in internal mt units."""
return self._impedance_dataarray("transfer_function_model_error").values
@staticmethod
def _get_component(comp: str, array: np.ndarray | None) -> np.ndarray | None:
"""Return one impedance-derived tensor component from a 2x2 array stack."""
if array is None:
return None
index_dict = {"x": 0, "y": 1}
ii = index_dict[comp[-2]]
jj = index_dict[comp[-1]]
return array[:, ii, jj]
[docs]
def to_z(self, units: str | None = None) -> Z:
"""Build a :class:`Z` object from dataset values.
Parameters
----------
units : str or None, default=None
Units for the returned ``Z`` object. If ``None``, uses
``self.impedance_units``.
Returns
-------
Z
``Z`` instance populated from the dataset.
Examples
--------
>>> z_obj = ds.tf.to_z()
>>> z_obj.rotate(10)
"""
resolved_units = self.impedance_units if units is None else units.lower()
z_object = Z(
z=self.z(units=resolved_units),
z_error=self.z_error(units=resolved_units),
z_model_error=self.z_model_error(units=resolved_units),
frequency=self.frequency,
units=resolved_units,
)
return z_object
[docs]
def to_tipper(self) -> Tipper:
"""Build a :class:`Tipper` object from dataset values.
Returns
-------
Tipper
``Tipper`` instance populated from the dataset.
Examples
--------
>>> tipper_obj = ds.tf.to_tipper()
"""
return Tipper(
tipper=self.tipper(),
tipper_error=self.tipper_error(),
tipper_model_error=self.tipper_model_error(),
frequency=self.frequency,
)
[docs]
def to_pt(self) -> PhaseTensor:
"""Build a :class:`PhaseTensor` object from dataset impedance.
Returns
-------
PhaseTensor
``PhaseTensor`` instance computed from impedance values.
Examples
--------
>>> pt_obj = ds.tf.to_pt()
"""
return PhaseTensor(
z=self.z(),
z_error=self.z_error(),
z_model_error=self.z_model_error(),
frequency=self.frequency,
)
[docs]
def with_z(
self,
z_obj: Z | None = None,
z: np.ndarray | None = None,
z_error: np.ndarray | None = None,
z_model_error: np.ndarray | None = None,
frequency: np.ndarray | None = None,
period: np.ndarray | None = None,
units: str = "mt",
inplace: bool = False,
) -> xr.Dataset:
"""Return a dataset with impedance channels updated.
Parameters
----------
z_obj : Z or None, default=None
Source ``Z`` object. If provided, array arguments are ignored.
z : numpy.ndarray or None, default=None
Impedance array used when ``z_obj`` is not provided.
z_error : numpy.ndarray or None, default=None
Impedance error array.
z_model_error : numpy.ndarray or None, default=None
Impedance model-error array.
frequency : numpy.ndarray or None, default=None
Frequency vector for array-mode input.
period : numpy.ndarray or None, default=None
Period vector alternative to ``frequency``.
units : str, default="mt"
Units for array-mode impedance input.
inplace : bool, default=False
If ``True``, mutate the bound dataset and return it.
Returns
-------
xarray.Dataset
Updated dataset.
Raises
------
ValueError
If input mode is ambiguous or insufficient.
Examples
--------
>>> updated = ds.tf.with_z(z=new_z, frequency=freq)
>>> ds.tf.with_z(z_obj=z_obj, inplace=True)
"""
self._validate_input_mode(z_obj, z, "z")
resolved_frequency, resolved_period = self._resolve_frequency_period(
frequency=frequency,
period=period,
)
if z_obj is None:
if z is None:
raise ValueError("z or z_obj must be provided.")
if resolved_frequency is None:
raise ValueError(
"frequency or period must be provided when z_obj is None."
)
z_obj = Z(
z=z,
z_error=z_error,
z_model_error=z_model_error,
frequency=resolved_frequency,
units=units,
)
ds_target = self._target_dataset(inplace=inplace)
self._validate_period_alignment(1.0 / z_obj.frequency)
out_labels, in_labels = self._get_labels_for_update(
ds_target,
output_candidates=["ex", "ey", "x", "y"],
input_candidates=["hx", "hy", "x", "y"],
n_output=2,
n_input=2,
)
self._update_tf_subset(
ds_target,
out_labels,
in_labels,
z_obj.z,
z_obj.z_error,
z_obj.z_model_error,
)
ds_target.attrs["impedance_units"] = z_obj.units
return ds_target
[docs]
def with_tipper(
self,
tipper_obj: Tipper | None = None,
tipper: np.ndarray | None = None,
tipper_error: np.ndarray | None = None,
tipper_model_error: np.ndarray | None = None,
frequency: np.ndarray | None = None,
period: np.ndarray | None = None,
inplace: bool = False,
) -> xr.Dataset:
"""Return a dataset with tipper channels updated.
Parameters
----------
tipper_obj : Tipper or None, default=None
Source ``Tipper`` object. If provided, array arguments are ignored.
tipper : numpy.ndarray or None, default=None
Tipper array used when ``tipper_obj`` is not provided.
tipper_error : numpy.ndarray or None, default=None
Tipper error array.
tipper_model_error : numpy.ndarray or None, default=None
Tipper model-error array.
frequency : numpy.ndarray or None, default=None
Frequency vector for array-mode input.
period : numpy.ndarray or None, default=None
Period vector alternative to ``frequency``.
inplace : bool, default=False
If ``True``, mutate the bound dataset and return it.
Returns
-------
xarray.Dataset
Updated dataset.
Examples
--------
>>> updated = ds.tf.with_tipper(tipper=tip, frequency=freq)
>>> ds.tf.with_tipper(tipper_obj=tipper_obj, inplace=True)
"""
self._validate_input_mode(tipper_obj, tipper, "tipper")
resolved_frequency, _ = self._resolve_frequency_period(
frequency=frequency,
period=period,
)
if tipper_obj is None:
if tipper is None:
raise ValueError("tipper or tipper_obj must be provided.")
if resolved_frequency is None:
raise ValueError(
"frequency or period must be provided when tipper_obj is None."
)
tipper_obj = Tipper(
tipper=tipper,
tipper_error=tipper_error,
tipper_model_error=tipper_model_error,
frequency=resolved_frequency,
)
ds_target = self._target_dataset(inplace=inplace)
self._validate_period_alignment(1.0 / tipper_obj.frequency)
out_labels, in_labels = self._get_labels_for_update(
ds_target,
output_candidates=["hz", "z"],
input_candidates=["hx", "hy", "x", "y"],
n_output=1,
n_input=2,
)
self._update_tf_subset(
ds_target,
out_labels,
in_labels,
tipper_obj.tipper,
tipper_obj.tipper_error,
tipper_obj.tipper_model_error,
)
return ds_target
[docs]
def with_res_phase(
self,
resistivity: np.ndarray,
phase: np.ndarray,
frequency: np.ndarray | None = None,
period: np.ndarray | None = None,
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,
units: str = "mt",
inplace: bool = False,
) -> xr.Dataset:
"""Return a dataset updated from resistivity and phase arrays.
Parameters
----------
resistivity : numpy.ndarray
Apparent resistivity array with impedance component shape.
phase : numpy.ndarray
Impedance phase array in degrees.
frequency : numpy.ndarray or None, default=None
Frequency vector. If ``None``, uses dataset frequency.
period : numpy.ndarray or None, default=None
Period vector alternative to ``frequency``.
res_error : numpy.ndarray or None, default=None
Resistivity error array.
phase_error : numpy.ndarray or None, default=None
Phase error array.
res_model_error : numpy.ndarray or None, default=None
Resistivity model-error array.
phase_model_error : numpy.ndarray or None, default=None
Phase model-error array.
units : str, default="mt"
Impedance units for the intermediate ``Z`` representation.
inplace : bool, default=False
If ``True``, mutate the bound dataset and return it.
Returns
-------
xarray.Dataset
Updated dataset.
Examples
--------
>>> ds2 = ds.tf.with_res_phase(resistivity=res, phase=phs)
"""
resolved_frequency, _ = self._resolve_frequency_period(
frequency=frequency,
period=period,
)
if resolved_frequency is None:
resolved_frequency = self.frequency
z_obj = Z(units=units)
z_obj.set_resistivity_phase(
resistivity=resistivity,
phase=phase,
frequency=resolved_frequency,
res_error=res_error,
phase_error=phase_error,
res_model_error=res_model_error,
phase_model_error=phase_model_error,
)
return self.with_z(z_obj=z_obj, inplace=inplace)
[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,
units: str = "mt",
inplace: bool = True,
) -> xr.Dataset | None:
"""Set impedance from resistivity and phase arrays.
This is a convenience wrapper around :meth:`with_res_phase` with
default ``inplace=True``.
Parameters
----------
resistivity : numpy.ndarray
Apparent resistivity array.
phase : numpy.ndarray
Impedance phase array in degrees.
frequency : numpy.ndarray
Frequency vector.
res_error : numpy.ndarray or None, default=None
Resistivity error array.
phase_error : numpy.ndarray or None, default=None
Phase error array.
res_model_error : numpy.ndarray or None, default=None
Resistivity model-error array.
phase_model_error : numpy.ndarray or None, default=None
Phase model-error array.
units : str, default="mt"
Impedance units.
inplace : bool, default=True
If ``True``, mutate in place and return ``None``.
Returns
-------
xarray.Dataset or None
Updated dataset when ``inplace=False``; otherwise ``None``.
"""
result = self.with_res_phase(
resistivity=resistivity,
phase=phase,
frequency=frequency,
res_error=res_error,
phase_error=phase_error,
res_model_error=res_model_error,
phase_model_error=phase_model_error,
units=units,
inplace=inplace,
)
if inplace:
return None
return result
[docs]
def set_amp_phase(
self,
r: np.ndarray,
phi: np.ndarray,
inplace: bool = True,
) -> xr.Dataset | None:
"""Set tipper values from amplitude/phase arrays.
Parameters
----------
r : numpy.ndarray
Tipper amplitude values.
phi : numpy.ndarray
Tipper phase values in degrees.
inplace : bool, default=True
If ``True``, mutate in place and return ``None``.
Returns
-------
xarray.Dataset or None
Updated dataset when ``inplace=False``; otherwise ``None``.
"""
tipper_obj = self.to_tipper()
tipper_obj.set_amp_phase(r, phi)
result = self.with_tipper(tipper_obj=tipper_obj, inplace=inplace)
if inplace:
return None
return result
[docs]
def set_mag_direction(
self,
mag_real: np.ndarray,
ang_real: np.ndarray,
mag_imag: np.ndarray,
ang_imag: np.ndarray,
inplace: bool = True,
) -> xr.Dataset | None:
"""Set tipper from magnitude and direction arrays.
Parameters
----------
mag_real : numpy.ndarray
Magnitude of real tipper vector.
ang_real : numpy.ndarray
Direction of real tipper vector in degrees.
mag_imag : numpy.ndarray
Magnitude of imaginary tipper vector.
ang_imag : numpy.ndarray
Direction of imaginary tipper vector in degrees.
inplace : bool, default=True
If ``True``, mutate in place and return ``None``.
Returns
-------
xarray.Dataset or None
Updated dataset when ``inplace=False``; otherwise ``None``.
"""
tipper_obj = self.to_tipper()
tipper_obj.set_mag_direction(
mag_real=mag_real,
ang_real=ang_real,
mag_imag=mag_imag,
ang_imag=ang_imag,
)
result = self.with_tipper(tipper_obj=tipper_obj, inplace=inplace)
if inplace:
return None
return result
[docs]
def rotate(
self,
alpha: float | int | str | list | tuple | np.ndarray,
inplace: bool = False,
coordinate_reference_frame: str = "ned",
) -> xr.Dataset | None:
"""Rotate available impedance and tipper channels.
Parameters
----------
alpha : float, int, str, list, tuple, or numpy.ndarray
Rotation angle(s) passed through to ``Z.rotate`` and
``Tipper.rotate``.
inplace : bool, default=False
If ``True``, mutate the bound dataset and return ``None``.
coordinate_reference_frame : str, default="ned"
Coordinate reference frame used for rotation semantics.
Returns
-------
xarray.Dataset or None
Rotated dataset when ``inplace=False``; otherwise ``None``.
Raises
------
ValueError
If no impedance or tipper channels can be identified for rotation.
Examples
--------
>>> ds_rot = ds.tf.rotate(30)
>>> ds.tf.rotate(30, inplace=True)
"""
ds_target = self._target_dataset(inplace=inplace)
target_accessor = TFDatasetAccessor(ds_target)
rotated_any = False
# Rotate impedance channels when available.
try:
z_rot = target_accessor.to_z().rotate(
alpha=alpha,
inplace=False,
coordinate_reference_frame=coordinate_reference_frame,
)
if z_rot is not None:
target_accessor.with_z(z_obj=z_rot, inplace=True)
rotated_any = True
except ValueError as error:
if "Could not determine impedance channels" not in str(error):
raise
# Rotate tipper channels when available.
try:
tipper_rot = target_accessor.to_tipper().rotate(
alpha=alpha,
inplace=False,
coordinate_reference_frame=coordinate_reference_frame,
)
if tipper_rot is not None:
target_accessor.with_tipper(tipper_obj=tipper_rot, inplace=True)
rotated_any = True
except ValueError as error:
if "Could not determine tipper channels" not in str(error):
raise
if not rotated_any:
raise ValueError("No impedance or tipper channels found to rotate.")
if inplace:
return None
return ds_target
[docs]
def interpolate(
self,
new_periods: np.ndarray,
inplace: bool = False,
method: str = "slinear",
extrapolate: bool = False,
**kwargs: Any,
) -> xr.Dataset | None:
"""Interpolate transfer-function variables onto new periods.
Interpolation is performed component-by-component for each
output/input channel pair.
Parameters
----------
new_periods : numpy.ndarray
Target periods in seconds.
inplace : bool, default=False
If ``True``, mutate the bound dataset and return ``None``.
method : str, default="slinear"
Interpolation method. Supported methods include ``linear``,
``cubic``, ``nearest``, ``slinear``, ``quadratic``, ``zero``,
``previous``, ``next``, ``pchip``, ``spline``, ``akima``,
``barycentric``, ``polynomial``, and ``krogh``.
extrapolate : bool, default=False
If ``True``, allow values outside source period range.
**kwargs
Additional keyword arguments forwarded to the selected scipy
interpolator.
Returns
-------
xarray.Dataset or None
Interpolated dataset when ``inplace=False``; otherwise ``None``.
Examples
--------
>>> periods = np.logspace(-2, 3, 60)
>>> ds_interp = ds.tf.interpolate(periods, method="pchip")
>>> ds.tf.interpolate(periods, inplace=True, extrapolate=True)
"""
new_periods = np.array(new_periods, dtype=float)
if not np.all(np.diff(new_periods) > 0):
sort_indices = np.argsort(new_periods)
new_periods = new_periods[sort_indices]
need_unsort = True
unsort_indices = np.argsort(sort_indices)
else:
need_unsort = False
source_periods = self._obj.period.values
if not extrapolate:
min_period = np.nanmin(source_periods)
max_period = np.nanmax(source_periods)
valid_indices = (new_periods >= min_period) & (new_periods <= max_period)
if not all(valid_indices):
logger.warning(
f"Some target periods outside source range ({min_period:.6g} - {max_period:.6g}s) "
"and extrapolate=False. These values will be set to NaN."
)
da_dict: dict[str, xr.DataArray] = {}
for var_name, da in self._obj.data_vars.items():
da_values = np.asarray(da.values)
output_labels = da.output.values
input_labels = da.input.values
output_shape = (len(new_periods),) + da.shape[1:]
if np.issubdtype(da.dtype, np.complexfloating):
output_array = np.zeros(output_shape, dtype=complex)
is_complex = True
else:
output_array = np.zeros(output_shape, dtype=da.dtype)
is_complex = False
for i_index, inp in enumerate(input_labels):
for j_index, outp in enumerate(output_labels):
comp_data = da_values[:, j_index, i_index]
finite_mask = np.isfinite(comp_data)
if not np.any(finite_mask):
if is_complex:
output_array[:, j_index, i_index] = np.nan + 1j * np.nan
else:
output_array[:, j_index, i_index] = np.nan
continue
valid_periods = source_periods[finite_mask]
valid_data = comp_data[finite_mask]
if len(valid_periods) < 2:
if is_complex:
output_array[:, j_index, i_index] = np.nan + 1j * np.nan
else:
output_array[:, j_index, i_index] = np.nan
continue
try:
if is_complex:
real_interp = self._get_interpolator(
valid_periods,
np.real(valid_data),
method=method,
extrapolate=extrapolate,
**kwargs,
)
imag_interp = self._get_interpolator(
valid_periods,
np.imag(valid_data),
method=method,
extrapolate=extrapolate,
**kwargs,
)
real_part = real_interp(new_periods)
imag_part = imag_interp(new_periods)
if not extrapolate:
mask = (new_periods < valid_periods.min()) | (
new_periods > valid_periods.max()
)
real_part[mask] = np.nan
imag_part[mask] = np.nan
output_array[:, j_index, i_index] = (
real_part + 1j * imag_part
)
else:
interp_func = self._get_interpolator(
valid_periods,
valid_data,
method=method,
extrapolate=extrapolate,
**kwargs,
)
result = interp_func(new_periods)
if not extrapolate:
mask = (new_periods < valid_periods.min()) | (
new_periods > valid_periods.max()
)
result[mask] = np.nan
output_array[:, j_index, i_index] = result
except Exception as error:
logger.warning(
f"Interpolation failed for {var_name}[{outp},{inp}]: {str(error)}"
)
if is_complex:
output_array[:, j_index, i_index] = np.nan + 1j * np.nan
else:
output_array[:, j_index, i_index] = np.nan
da_dict[var_name] = xr.DataArray(
data=output_array,
dims=["period", "output", "input"],
coords={
"period": new_periods,
"output": output_labels,
"input": input_labels,
},
name=var_name,
)
ds = xr.Dataset(da_dict, attrs=dict(self._obj.attrs))
if need_unsort:
original_order_periods = new_periods[unsort_indices]
ds = ds.reindex(period=original_order_periods)
if inplace:
self._obj._replace(
variables=ds._variables,
coord_names=ds._coord_names,
dims=ds._dims,
attrs=ds._attrs,
indexes=ds._indexes,
encoding=ds._encoding,
inplace=True,
)
return None
return ds
def _get_interpolator(
self,
x: np.ndarray,
y: np.ndarray,
method: str,
extrapolate: bool = False,
**kwargs: Any,
) -> Any:
"""Build a scipy interpolator for the requested method.
Parameters
----------
x : numpy.ndarray
Monotonic x values.
y : numpy.ndarray
y values corresponding to ``x``.
method : str
Interpolation method name.
extrapolate : bool, default=False
Whether to permit extrapolation beyond ``x`` bounds.
**kwargs
Extra method-specific keyword arguments.
Returns
-------
Any
Callable interpolator object.
Raises
------
ValueError
If ``method`` is not supported.
"""
x = np.asarray(x)
y = np.asarray(y)
if not np.all(np.diff(x) > 0):
indices = np.argsort(x)
x = x[indices]
y = y[indices]
if method == "spline":
k = kwargs.pop("k", 3)
if len(x) <= k:
k = min(len(x) - 1, 1)
return scipy.interpolate.InterpolatedUnivariateSpline(
x,
y,
k=k,
ext=int(extrapolate),
)
if method == "pchip":
return scipy.interpolate.PchipInterpolator(
x,
y,
extrapolate=extrapolate,
**kwargs,
)
if method == "akima":
interp = scipy.interpolate.Akima1DInterpolator(x, y, **kwargs)
if extrapolate:
return lambda xx: interp(xx, extrapolate=True)
return interp
if method == "polynomial":
return scipy.interpolate.CubicSpline(
x,
y,
extrapolate=extrapolate,
**kwargs,
)
if method in [
"linear",
"cubic",
"nearest",
"slinear",
"quadratic",
"zero",
"previous",
"next",
"barycentric",
"krogh",
]:
fill_value = "extrapolate" if extrapolate else np.nan
return scipy.interpolate.interp1d(
x,
y,
kind=method,
bounds_error=False,
fill_value=fill_value,
**kwargs,
)
raise ValueError(
f"Interpolation method {method} is not supported. "
"Supported methods are linear, cubic, nearest, slinear, quadratic, "
"zero, previous, next, pchip, spline, akima, barycentric, polynomial, krogh."
)
@property
def resistivity(self) -> np.ndarray | None:
"""Apparent resistivity tensor computed directly from Dataset values.
Returns
-------
np.ndarray | None
Apparent resistivity tensor computed directly from Dataset values.
"""
return compute_resistivity(self._z_mt(), self.frequency)
@property
def phase(self) -> np.ndarray | None:
"""Impedance phase tensor (degrees) computed directly from Dataset values.
Returns
-------
np.ndarray | None
Impedance phase tensor (degrees) computed directly from Dataset values.
"""
return compute_phase(self._z_mt())
@property
def resistivity_error(self) -> np.ndarray | None:
"""Apparent resistivity error computed directly from Dataset values.
Returns
-------
np.ndarray | None
Apparent resistivity error computed directly from Dataset values.
"""
return compute_resistivity_error(
self._z_mt(),
self._z_error_mt(),
self.frequency,
)
@property
def phase_error(self) -> np.ndarray | None:
"""Impedance phase error (degrees) computed directly from Dataset values.
Returns
-------
np.ndarray | None
Impedance phase error (degrees) computed directly from Dataset values.
"""
return compute_phase_error(self._z_mt(), self._z_error_mt())
@property
def resistivity_model_error(self) -> np.ndarray | None:
"""Apparent resistivity model error computed directly from Dataset values.
Returns
-------
np.ndarray | None
Apparent resistivity model error computed directly from Dataset values.
"""
return compute_resistivity_error(
self._z_mt(),
self._z_model_error_mt(),
self.frequency,
)
@property
def phase_model_error(self) -> np.ndarray | None:
"""Impedance phase model error (degrees) computed directly from Dataset values.
Returns
-------
np.ndarray | None
Impedance phase model error (degrees) computed directly from Dataset values.
"""
return compute_phase_error(self._z_mt(), self._z_model_error_mt())
@property
def det(self) -> np.ndarray | None:
"""Determinant of impedance tensor in mt units.
Returns
-------
np.ndarray | None
Determinant of impedance tensor in mt units.
"""
return np.array([np.linalg.det(ii) ** 0.5 for ii in self._z_mt()])
@property
def det_error(self) -> np.ndarray | None:
"""Determinant error from transfer_function_error.
Returns
-------
np.ndarray | None
Determinant error from transfer_function_error.
"""
z_error = self._z_error_mt()
det_error = np.zeros_like(self.det, dtype=float)
with np.errstate(invalid="ignore"):
det_error[:] = (
np.abs(
np.linalg.det(self._z_mt() + z_error)
- np.linalg.det(self._z_mt() - z_error)
)
/ 2.0
) ** 0.5
return det_error
@property
def det_model_error(self) -> np.ndarray | None:
"""Determinant model error from transfer_function_model_error.
Returns
-------
np.ndarray | None
Determinant model error from transfer_function_model_error.
"""
z_model_error = self._z_model_error_mt()
det_error = np.zeros_like(self.det, dtype=float)
with np.errstate(invalid="ignore"):
det_error[:] = (
np.abs(
np.linalg.det(self._z_mt() + z_model_error)
- np.linalg.det(self._z_mt() - z_model_error)
)
/ 2.0
) ** 0.5
return det_error
@property
def phase_det(self) -> np.ndarray | None:
"""Phase of determinant in degrees.
Returns
-------
np.ndarray | None
Phase of determinant in degrees.
"""
return np.rad2deg(np.arctan2(self.det.imag, self.det.real))
@property
def phase_error_det(self) -> np.ndarray | None:
"""Phase error of determinant in degrees.
Returns
-------
np.ndarray | None
Phase error of determinant in degrees.
"""
return np.rad2deg(np.arcsin(self.det_error / abs(self.det)))
@property
def phase_model_error_det(self) -> np.ndarray | None:
"""Phase model error of determinant in degrees.
Returns
-------
np.ndarray | None
Phase model error of determinant in degrees.
"""
return np.rad2deg(np.arcsin(self.det_model_error / abs(self.det)))
@property
def res_det(self) -> np.ndarray | None:
"""Apparent resistivity from determinant.
Returns
-------
np.ndarray | None
Apparent resistivity from determinant.
"""
return 0.2 * (1.0 / self.frequency) * abs(self.det) ** 2
@property
def res_error_det(self) -> np.ndarray | None:
"""Apparent resistivity error from determinant.
Returns
-------
np.ndarray | None
Apparent resistivity error from determinant.
"""
return (
0.2 * (1.0 / self.frequency) * np.abs(self.det + self.det_error) ** 2
- self.res_det
)
@property
def res_model_error_det(self) -> np.ndarray | None:
"""Apparent resistivity model error from determinant.
Returns
-------
np.ndarray | None
Apparent resistivity model error from determinant.
"""
return (
0.2 * (1.0 / self.frequency) * np.abs(self.det + self.det_model_error) ** 2
- self.res_det
)
@property
def res_error_xx(self) -> np.ndarray | None:
"""Apparent resistivity error of the xx impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity error of the xx impedance component.
"""
return self._get_component("xx", self.resistivity_error)
@property
def res_error_xy(self) -> np.ndarray | None:
"""Apparent resistivity error of the xy impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity error of the xy impedance component.
"""
return self._get_component("xy", self.resistivity_error)
@property
def res_error_yx(self) -> np.ndarray | None:
"""Apparent resistivity error of the yx impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity error of the yx impedance component.
"""
return self._get_component("yx", self.resistivity_error)
@property
def res_error_yy(self) -> np.ndarray | None:
"""Apparent resistivity error of the yy impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity error of the yy impedance component.
"""
return self._get_component("yy", self.resistivity_error)
@property
def res_model_error_xx(self) -> np.ndarray | None:
"""Apparent resistivity model error of the xx impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity model error of the xx impedance component.
"""
return self._get_component("xx", self.resistivity_model_error)
@property
def res_model_error_xy(self) -> np.ndarray | None:
"""Apparent resistivity model error of the xy impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity model error of the xy impedance component.
"""
return self._get_component("xy", self.resistivity_model_error)
@property
def res_model_error_yx(self) -> np.ndarray | None:
"""Apparent resistivity model error of the yx impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity model error of the yx impedance component.
"""
return self._get_component("yx", self.resistivity_model_error)
@property
def res_model_error_yy(self) -> np.ndarray | None:
"""Apparent resistivity model error of the yy impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity model error of the yy impedance component.
"""
return self._get_component("yy", self.resistivity_model_error)
@property
def res_xx(self) -> np.ndarray | None:
"""Apparent resistivity of the xx impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity of the xx impedance component.
"""
return self._get_component("xx", self.resistivity)
@property
def res_xy(self) -> np.ndarray | None:
"""Apparent resistivity of the xy impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity of the xy impedance component.
"""
return self._get_component("xy", self.resistivity)
@property
def res_yx(self) -> np.ndarray | None:
"""Apparent resistivity of the yx impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity of the yx impedance component.
"""
return self._get_component("yx", self.resistivity)
@property
def res_yy(self) -> np.ndarray | None:
"""Apparent resistivity of the yy impedance component.
Returns
-------
np.ndarray | None
Apparent resistivity of the yy impedance component.
"""
return self._get_component("yy", self.resistivity)
@property
def phase_xx(self) -> np.ndarray | None:
"""Phase of the xx impedance component in degrees.
Returns
-------
np.ndarray | None
Phase of the xx impedance component in degrees.
"""
return self._get_component("xx", self.phase)
@property
def phase_xy(self) -> np.ndarray | None:
"""Phase of the xy impedance component in degrees.
Returns
-------
np.ndarray | None
Phase of the xy impedance component in degrees.
"""
return self._get_component("xy", self.phase)
@property
def phase_yx(self) -> np.ndarray | None:
"""Phase of the yx impedance component in degrees.
Returns
-------
np.ndarray | None
Phase of the yx impedance component in degrees.
"""
return self._get_component("yx", self.phase)
@property
def phase_yy(self) -> np.ndarray | None:
"""Phase of the yy impedance component in degrees.
Returns
-------
np.ndarray | None
Phase of the yy impedance component in degrees.
"""
return self._get_component("yy", self.phase)
@property
def phase_error_xx(self) -> np.ndarray | None:
"""Phase error of the xx impedance component in degrees.
Returns
-------
np.ndarray | None
Phase error of the xx impedance component in degrees.
"""
return self._get_component("xx", self.phase_error)
@property
def phase_error_xy(self) -> np.ndarray | None:
"""Phase error of the xy impedance component in degrees.
Returns
-------
np.ndarray | None
Phase error of the xy impedance component in degrees.
"""
return self._get_component("xy", self.phase_error)
@property
def phase_error_yx(self) -> np.ndarray | None:
"""Phase error of the yx impedance component in degrees.
Returns
-------
np.ndarray | None
Phase error of the yx impedance component in degrees.
"""
return self._get_component("yx", self.phase_error)
@property
def phase_error_yy(self) -> np.ndarray | None:
"""Phase error of the yy impedance component in degrees.
Returns
-------
np.ndarray | None
Phase error of the yy impedance component in degrees.
"""
return self._get_component("yy", self.phase_error)
@property
def phase_model_error_xx(self) -> np.ndarray | None:
"""Phase model error of the xx impedance component in degrees.
Returns
-------
np.ndarray | None
Phase model error of the xx impedance component in degrees.
"""
return self._get_component("xx", self.phase_model_error)
@property
def phase_model_error_xy(self) -> np.ndarray | None:
"""Phase model error of the xy impedance component in degrees.
Returns
-------
np.ndarray | None
Phase model error of the xy impedance component in degrees.
"""
return self._get_component("xy", self.phase_model_error)
@property
def phase_model_error_yx(self) -> np.ndarray | None:
"""Phase model error of the yx impedance component in degrees.
Returns
-------
np.ndarray | None
Phase model error of the yx impedance component in degrees.
"""
return self._get_component("yx", self.phase_model_error)
@property
def phase_model_error_yy(self) -> np.ndarray | None:
"""Phase model error of the yy impedance component in degrees.
Returns
-------
np.ndarray | None
Phase model error of the yy impedance component in degrees.
"""
return self._get_component("yy", self.phase_model_error)
@property
def tipper_amplitude(self) -> np.ndarray | None:
"""Tipper amplitude derived from the accessor tipper view.
Returns
-------
np.ndarray | None
Tipper amplitude derived from the accessor tipper view.
"""
return compute_tipper_amplitude(self.tipper())
@property
def t_zx(self) -> np.ndarray | None:
"""zx component of the tipper.
Returns
-------
np.ndarray | None
zx component of the tipper.
"""
return self._get_tipper_component("zx", self.tipper())
@property
def t_zy(self) -> np.ndarray | None:
"""zy component of the tipper.
Returns
-------
np.ndarray | None
zy component of the tipper.
"""
return self._get_tipper_component("zy", self.tipper())
@property
def t_zx_error(self) -> np.ndarray | None:
"""zx component error of the tipper.
Returns
-------
np.ndarray | None
zx component error of the tipper.
"""
return self._get_tipper_component("zx", self.tipper_error())
@property
def t_zy_error(self) -> np.ndarray | None:
"""zy component error of the tipper.
Returns
-------
np.ndarray | None
zy component error of the tipper.
"""
return self._get_tipper_component("zy", self.tipper_error())
@property
def t_zx_model_error(self) -> np.ndarray | None:
"""zx component model error of the tipper.
Returns
-------
np.ndarray | None
zx component model error of the tipper.
"""
return self._get_tipper_component("zx", self.tipper_model_error())
@property
def t_zy_model_error(self) -> np.ndarray | None:
"""zy component model error of the tipper.
Returns
-------
np.ndarray | None
zy component model error of the tipper.
"""
return self._get_tipper_component("zy", self.tipper_model_error())
@property
def tipper_phase(self) -> np.ndarray | None:
"""Tipper phase in degrees derived from the accessor tipper view.
Returns
-------
np.ndarray | None
Tipper phase in degrees derived from the accessor tipper view.
"""
return compute_tipper_phase(self.tipper())
@property
def tipper_amplitude_error(self) -> np.ndarray | None:
"""Tipper amplitude error derived from the accessor tipper view.
Returns
-------
np.ndarray | None
Tipper amplitude error derived from the accessor tipper view.
"""
return compute_tipper_amp_phase_error(self.tipper(), self.tipper_error())[0]
@property
def tipper_phase_error(self) -> np.ndarray | None:
"""Tipper phase error in degrees derived from the accessor tipper view.
Returns
-------
np.ndarray | None
Tipper phase error in degrees derived from the accessor tipper view.
"""
return compute_tipper_amp_phase_error(self.tipper(), self.tipper_error())[1]
@property
def tipper_amplitude_model_error(self) -> np.ndarray | None:
"""Tipper amplitude model error derived from Dataset values.
Returns
-------
np.ndarray | None
Tipper amplitude model error derived from Dataset values.
"""
return compute_tipper_amp_phase_error(self.tipper(), self.tipper_model_error())[
0
]
@property
def tipper_phase_model_error(self) -> np.ndarray | None:
"""Tipper phase model error in degrees derived from Dataset values.
Returns
-------
np.ndarray | None
Tipper phase model error in degrees derived from Dataset values.
"""
return compute_tipper_amp_phase_error(self.tipper(), self.tipper_model_error())[
1
]
@property
def tipper_mag_real(self) -> np.ndarray | None:
"""Tipper real-component magnitude.
Returns
-------
np.ndarray | None
Tipper real-component magnitude.
"""
return compute_tipper_magnitude_real(self.tipper())
@property
def tipper_mag_imag(self) -> np.ndarray | None:
"""Tipper imaginary-component magnitude.
Returns
-------
np.ndarray | None
Tipper imaginary-component magnitude.
"""
return compute_tipper_magnitude_imag(self.tipper())
@property
def tipper_angle_real(self) -> np.ndarray | None:
"""Tipper real-component angle in degrees.
Returns
-------
np.ndarray | None
Tipper real-component angle in degrees.
"""
return compute_tipper_angle_real(self.tipper())
@property
def tipper_angle_imag(self) -> np.ndarray | None:
"""Tipper imaginary-component angle in degrees.
Returns
-------
np.ndarray | None
Tipper imaginary-component angle in degrees.
"""
return compute_tipper_angle_imag(self.tipper())
@property
def tipper_mag_error(self) -> np.ndarray | None:
"""Tipper magnitude error derived from Dataset values.
Returns
-------
np.ndarray | None
Tipper magnitude error derived from Dataset values.
"""
return compute_tipper_magnitude_error(self.tipper_error())
@property
def tipper_angle_error(self) -> np.ndarray | None:
"""Tipper angle error in degrees derived from Dataset values.
Returns
-------
np.ndarray | None
Tipper angle error in degrees derived from Dataset values.
"""
return compute_tipper_angle_error(self.tipper_error())
@property
def tipper_mag_model_error(self) -> np.ndarray | None:
"""Tipper magnitude model error derived from Dataset values.
Returns
-------
np.ndarray | None
Tipper magnitude model error derived from Dataset values.
"""
return compute_tipper_magnitude_error(self.tipper_model_error())
@property
def tipper_angle_model_error(self) -> np.ndarray | None:
"""Tipper angle model error in degrees derived from Dataset values.
Returns
-------
np.ndarray | None
Tipper angle model error in degrees derived from Dataset values.
"""
return compute_tipper_angle_error(self.tipper_model_error())
@property
def pt(self) -> np.ndarray | None:
"""Phase tensor array derived from impedance channels.
Returns
-------
np.ndarray | None
Phase tensor array derived from impedance channels.
"""
return compute_phase_tensor(self._z_mt())
@property
def pt_error(self) -> np.ndarray | None:
"""Phase tensor error array derived from impedance channels.
Returns
-------
np.ndarray | None
Phase tensor error array derived from impedance channels.
"""
return compute_phase_tensor_error(self._z_mt(), self._z_error_mt())
@property
def pt_model_error(self) -> np.ndarray | None:
"""Phase tensor model error array derived from impedance channels.
Returns
-------
np.ndarray | None
Phase tensor model error array derived from impedance channels.
"""
return compute_phase_tensor_error(self._z_mt(), self._z_model_error_mt())
@property
def pt_xx(self) -> np.ndarray | None:
"""xx component of the phase tensor.
Returns
-------
np.ndarray | None
xx component of the phase tensor.
"""
return self._get_component("xx", self.pt)
@property
def pt_xy(self) -> np.ndarray | None:
"""xy component of the phase tensor.
Returns
-------
np.ndarray | None
xy component of the phase tensor.
"""
return self._get_component("xy", self.pt)
@property
def pt_yx(self) -> np.ndarray | None:
"""yx component of the phase tensor.
Returns
-------
np.ndarray | None
yx component of the phase tensor.
"""
return self._get_component("yx", self.pt)
@property
def pt_yy(self) -> np.ndarray | None:
"""yy component of the phase tensor.
Returns
-------
np.ndarray | None
yy component of the phase tensor.
"""
return self._get_component("yy", self.pt)
@property
def pt_error_xx(self) -> np.ndarray | None:
"""xx component error of the phase tensor.
Returns
-------
np.ndarray | None
xx component error of the phase tensor.
"""
return self._get_component("xx", self.pt_error)
@property
def pt_error_xy(self) -> np.ndarray | None:
"""xy component error of the phase tensor.
Returns
-------
np.ndarray | None
xy component error of the phase tensor.
"""
return self._get_component("xy", self.pt_error)
@property
def pt_error_yx(self) -> np.ndarray | None:
"""yx component error of the phase tensor.
Returns
-------
np.ndarray | None
yx component error of the phase tensor.
"""
return self._get_component("yx", self.pt_error)
@property
def pt_error_yy(self) -> np.ndarray | None:
"""yy component error of the phase tensor.
Returns
-------
np.ndarray | None
yy component error of the phase tensor.
"""
return self._get_component("yy", self.pt_error)
@property
def pt_model_error_xx(self) -> np.ndarray | None:
"""xx component model error of the phase tensor.
Returns
-------
np.ndarray | None
xx component model error of the phase tensor.
"""
return self._get_component("xx", self.pt_model_error)
@property
def pt_model_error_xy(self) -> np.ndarray | None:
"""xy component model error of the phase tensor.
Returns
-------
np.ndarray | None
xy component model error of the phase tensor.
"""
return self._get_component("xy", self.pt_model_error)
@property
def pt_model_error_yx(self) -> np.ndarray | None:
"""yx component model error of the phase tensor.
Returns
-------
np.ndarray | None
yx component model error of the phase tensor.
"""
return self._get_component("yx", self.pt_model_error)
@property
def pt_model_error_yy(self) -> np.ndarray | None:
"""yy component model error of the phase tensor.
Returns
-------
np.ndarray | None
yy component model error of the phase tensor.
"""
return self._get_component("yy", self.pt_model_error)
@property
def pt_phimin(self) -> np.ndarray | None:
"""Minimum phase angle of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Minimum phase angle of the phase tensor in degrees.
"""
return compute_pt_phimin(self.pt)
@property
def pt_phimin_error(self) -> np.ndarray | None:
"""Minimum phase angle error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Minimum phase angle error of the phase tensor in degrees.
"""
return compute_pt_phimin_error(self.pt, self.pt_error)
@property
def pt_phimin_model_error(self) -> np.ndarray | None:
"""Minimum phase angle model error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Minimum phase angle model error of the phase tensor in degrees.
"""
return compute_pt_phimin_error(self.pt, self.pt_model_error)
@property
def pt_phimax(self) -> np.ndarray | None:
"""Maximum phase angle of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Maximum phase angle of the phase tensor in degrees.
"""
return compute_pt_phimax(self.pt)
@property
def pt_phimax_error(self) -> np.ndarray | None:
"""Maximum phase angle error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Maximum phase angle error of the phase tensor in degrees.
"""
return compute_pt_phimax_error(self.pt, self.pt_error)
@property
def pt_phimax_model_error(self) -> np.ndarray | None:
"""Maximum phase angle model error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Maximum phase angle model error of the phase tensor in degrees.
"""
return compute_pt_phimax_error(self.pt, self.pt_model_error)
@property
def pt_trace(self) -> np.ndarray | None:
"""Trace of the phase tensor.
Returns
-------
np.ndarray | None
Trace of the phase tensor.
"""
return compute_pt_trace(self.pt)
@property
def pt_trace_error(self) -> np.ndarray | None:
"""Trace error of the phase tensor.
Returns
-------
np.ndarray | None
Trace error of the phase tensor.
"""
return compute_pt_trace_error(self.pt_error)
@property
def pt_trace_model_error(self) -> np.ndarray | None:
"""Trace model error of the phase tensor.
Returns
-------
np.ndarray | None
Trace model error of the phase tensor.
"""
return compute_pt_trace_error(self.pt_model_error)
@property
def pt_alpha(self) -> np.ndarray | None:
"""Principal axis angle of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Principal axis angle of the phase tensor in degrees.
"""
return compute_pt_alpha(self.pt)
@property
def pt_alpha_error(self) -> np.ndarray | None:
"""Principal axis angle error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Principal axis angle error of the phase tensor in degrees.
"""
return compute_pt_alpha_error(self.pt, self.pt_error)
@property
def pt_alpha_model_error(self) -> np.ndarray | None:
"""Principal axis angle model error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
Principal axis angle model error of the phase tensor in degrees.
"""
return compute_pt_alpha_error(self.pt, self.pt_model_error)
@property
def pt_beta(self) -> np.ndarray | None:
"""3D-dimensionality angle of the phase tensor in degrees.
Returns
-------
np.ndarray | None
3D-dimensionality angle of the phase tensor in degrees.
"""
return compute_pt_beta(self.pt)
@property
def pt_beta_error(self) -> np.ndarray | None:
"""3D-dimensionality angle error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
3D-dimensionality angle error of the phase tensor in degrees.
"""
return compute_pt_beta_error(self.pt, self.pt_error)
@property
def pt_beta_model_error(self) -> np.ndarray | None:
"""3D-dimensionality angle model error of the phase tensor in degrees.
Returns
-------
np.ndarray | None
3D-dimensionality angle model error of the phase tensor in degrees.
"""
return compute_pt_beta_error(self.pt, self.pt_model_error)
@property
def pt_azimuth(self) -> np.ndarray | None:
"""Phase tensor azimuth in degrees.
Returns
-------
np.ndarray | None
Phase tensor azimuth in degrees.
"""
return compute_pt_azimuth(self.pt)
@property
def pt_azimuth_error(self) -> np.ndarray | None:
"""Phase tensor azimuth error in degrees.
Returns
-------
np.ndarray | None
Phase tensor azimuth error in degrees.
"""
return compute_pt_azimuth_error(self.pt, self.pt_error)
@property
def pt_azimuth_model_error(self) -> np.ndarray | None:
"""Phase tensor azimuth model error in degrees.
Returns
-------
np.ndarray | None
Phase tensor azimuth model error in degrees.
"""
return compute_pt_azimuth_error(self.pt, self.pt_model_error)
@property
def pt_skew(self) -> np.ndarray | None:
"""Phase tensor skew in degrees.
Returns
-------
np.ndarray | None
Phase tensor skew in degrees.
"""
return compute_pt_skew(self.pt)
@property
def pt_skew_error(self) -> np.ndarray | None:
"""Phase tensor skew error in degrees.
Returns
-------
np.ndarray | None
Phase tensor skew error in degrees.
"""
return compute_pt_skew_error(self.pt, self.pt_error)
@property
def pt_skew_model_error(self) -> np.ndarray | None:
"""Phase tensor skew model error in degrees.
Returns
-------
np.ndarray | None
Phase tensor skew model error in degrees.
"""
return compute_pt_skew_error(self.pt, self.pt_model_error)
@property
def pt_det(self) -> np.ndarray | None:
"""Determinant of the phase tensor.
Returns
-------
np.ndarray | None
Determinant of the phase tensor.
"""
return compute_pt_det(self.pt)
@property
def pt_det_error(self) -> np.ndarray | None:
"""Determinant error of the phase tensor.
Returns
-------
np.ndarray | None
Determinant error of the phase tensor.
"""
return compute_pt_det_error(self.pt, self.pt_error)
@property
def pt_det_model_error(self) -> np.ndarray | None:
"""Determinant model error of the phase tensor.
Returns
-------
np.ndarray | None
Determinant model error of the phase tensor.
"""
return compute_pt_det_error(self.pt, self.pt_model_error)
@property
def pt_ellipticity(self) -> np.ndarray | None:
"""Phase tensor ellipticity.
Returns
-------
np.ndarray | None
Phase tensor ellipticity.
"""
return compute_pt_ellipticity(self.pt)
@property
def pt_ellipticity_error(self) -> np.ndarray | None:
"""Phase tensor ellipticity error.
Returns
-------
np.ndarray | None
Phase tensor ellipticity error.
"""
return compute_pt_ellipticity_error(self.pt, self.pt_error)
@property
def pt_ellipticity_model_error(self) -> np.ndarray | None:
"""Phase tensor ellipticity model error.
Returns
-------
np.ndarray | None
Phase tensor ellipticity model error.
"""
return compute_pt_ellipticity_error(self.pt, self.pt_model_error)
@property
def pt_eccentricity(self) -> np.ndarray | None:
"""Phase tensor eccentricity.
Returns
-------
np.ndarray | None
Phase tensor eccentricity.
"""
return compute_pt_eccentricity(self.pt)
@property
def pt_eccentricity_error(self) -> np.ndarray | None:
"""Phase tensor eccentricity error.
Returns
-------
np.ndarray | None
Phase tensor eccentricity error.
"""
return compute_pt_eccentricity_error(self.pt, self.pt_error)
@property
def pt_eccentricity_model_error(self) -> np.ndarray | None:
"""Phase tensor eccentricity model error.
Returns
-------
np.ndarray | None
Phase tensor eccentricity model error.
"""
return compute_pt_eccentricity_error(self.pt, self.pt_model_error)
@property
def pt_only1d(self) -> np.ndarray | None:
"""Phase tensor expressed in the 1D convenience form.
Returns
-------
np.ndarray | None
Phase tensor expressed in the 1D convenience form.
"""
if self.pt is None:
return None
pt_1d = self.pt.copy()
pt_1d[:, 0, 1] = 0
pt_1d[:, 1, 0] = 0
mean_1d = 0.5 * (pt_1d[:, 0, 0] + pt_1d[:, 1, 1])
pt_1d[:, 0, 0] = mean_1d
pt_1d[:, 1, 1] = mean_1d
return pt_1d
@property
def pt_only2d(self) -> np.ndarray | None:
"""Phase tensor expressed in the 2D convenience form.
Returns
-------
np.ndarray | None
Phase tensor expressed in the 2D convenience form.
"""
if self.pt is None:
return None
pt_2d = self.pt.copy()
pt_2d[:, 0, 1] = 0
pt_2d[:, 1, 0] = 0
pt_2d[:, 0, 0] = self.pt_phimax[:]
pt_2d[:, 1, 1] = self.pt_phimin[:]
return pt_2d
@property
def phase_tensor(self) -> Any:
"""Phase tensor object derived from the accessor impedance view.
Returns
-------
Any
Phase tensor object derived from the accessor impedance view.
"""
return self.to_z().phase_tensor
@property
def invariants(self) -> Any:
"""Impedance invariants object derived from the accessor impedance view.
Returns
-------
Any
Impedance invariants object derived from the accessor impedance view.
"""
return self.to_z().invariants
[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,
units: str | None = None,
as_dataset: bool = False,
) -> xr.Dataset | Z:
"""Apply static-shift correction using :class:`Z.remove_ss`.
Parameters
----------
reduce_res_factor_x : float or array-like, optional
Static-shift correction factor for x-polarized rows.
reduce_res_factor_y : float or array-like, optional
Static-shift correction factor for y-polarized rows.
units : str, optional
Impedance unit system passed to :meth:`to_z`.
as_dataset : bool, optional
If ``True``, return corrected transfer-function dataset.
If ``False``,
return a corrected :class:`Z` object.
Returns
-------
xarray.Dataset or Z
Corrected result as either dataset or ``Z`` object.
Examples
--------
>>> z_corr = ds.tf.remove_ss(1.1, 0.9)
>>> ds_corr = ds.tf.remove_ss(1.1, 0.9, as_dataset=True)
"""
z_out = self.to_z(units=units).remove_ss(
reduce_res_factor_x=reduce_res_factor_x,
reduce_res_factor_y=reduce_res_factor_y,
inplace=False,
)
if as_dataset:
ds_out = self._obj.copy(deep=True)
TFDatasetAccessor(ds_out).with_z(z_obj=z_out, inplace=True)
ds_out.attrs.update(dict(self._obj.attrs))
ds_out.attrs["impedance_units"] = z_out.units
return ds_out
return z_out
[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,
units: str | None = None,
as_dataset: bool = False,
) -> xr.Dataset | Z:
"""Apply galvanic distortion correction using :class:`Z.remove_distortion`.
Parameters
----------
distortion_tensor : numpy.ndarray or None, optional
Distortion tensor estimate.
distortion_error_tensor : numpy.ndarray or None, optional
Distortion tensor error estimate.
n_frequencies : int or None, optional
Number of frequencies used for estimation when an explicit tensor is
not provided.
comp : str, default="det"
Impedance component selection strategy.
only_2d : bool, default=False
If ``True``, constrain correction to 2D assumptions.
units : str or None, optional
Impedance unit system passed to :meth:`to_z`.
as_dataset : bool, default=False
If ``True``, return corrected transfer-function dataset.
If ``False``, return corrected :class:`Z` object.
Returns
-------
xarray.Dataset or Z
Corrected result as either dataset or ``Z`` object.
"""
z_out = self.to_z(units=units).remove_distortion(
distortion_tensor=distortion_tensor,
distortion_error_tensor=distortion_error_tensor,
n_frequencies=n_frequencies,
comp=comp,
only_2d=only_2d,
inplace=False,
)
if as_dataset:
ds_out = self._obj.copy(deep=True)
TFDatasetAccessor(ds_out).with_z(z_obj=z_out, inplace=True)
ds_out.attrs.update(dict(self._obj.attrs))
ds_out.attrs["impedance_units"] = z_out.units
return ds_out
return z_out
[docs]
def estimate_dimensionality(
self, skew_threshold: float = 5, eccentricity_threshold: float = 0.1
) -> np.ndarray:
"""Estimate 1D/2D/3D dimensionality from phase-tensor metrics.
Parameters
----------
skew_threshold : float, default=5
Skew threshold (degrees) used to discriminate dimensionality.
eccentricity_threshold : float, default=0.1
Eccentricity threshold used to discriminate dimensionality.
Returns
-------
numpy.ndarray
Integer dimensionality classification per period.
"""
return self.to_z().estimate_dimensionality(
skew_threshold=skew_threshold,
eccentricity_threshold=eccentricity_threshold,
)
[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 galvanic distortion tensor.
Parameters
----------
n_frequencies : int or None, optional
Number of frequencies to include in the estimate.
comp : str, default="det"
Impedance component selection strategy.
only_2d : bool, default=False
If ``True``, constrain the estimate to 2D assumptions.
clockwise : bool, default=True
Rotation-direction convention used internally.
Returns
-------
tuple of numpy.ndarray
Distortion tensor and associated error tensor.
"""
return self.to_z().estimate_distortion(
n_frequencies=n_frequencies,
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 result from the underlying ``Z`` object.
"""
return self.to_z().estimate_depth_of_investigation()