# -*- coding: utf-8 -*-
"""
Created on Sun Oct 2 13:20:28 2022
@author: jpeacock
"""
# =============================================================================
# Imports
# =============================================================================
from __future__ import annotations
from typing import Any
import numpy as np
import pandas as pd
from scipy.spatial.distance import pdist
from . import Tipper, Z
# =============================================================================
[docs]
class MTDataFrame:
"""
DataFrame for a single MT station.
Parameters
----------
data : dict, np.ndarray, pd.DataFrame, MTDataFrame, or None, optional
Initial data to populate the dataframe, by default None
n_entries : int, optional
Number of empty entries to create if data is None, by default 0
**kwargs : dict
Additional keyword arguments for setting attributes
Attributes
----------
dataframe : pd.DataFrame
Pandas DataFrame containing MT data with standardized columns
working_survey : str or None
Current working survey name
working_station : str or None
Current working station name
Notes
-----
Tried subclassing pandas.DataFrame, but that turned out to not be
straightforward, so went with composition instead.
"""
def __init__(
self,
data: dict | np.ndarray | pd.DataFrame | "MTDataFrame" | None = None,
n_entries: int = 0,
**kwargs: Any,
) -> None:
self._dtype_list = [
("survey", "U25"),
("station", "U25"),
("latitude", float),
("longitude", float),
("elevation", float),
("datum_epsg", "U6"),
("east", float),
("north", float),
("utm_epsg", "U6"),
("model_east", float),
("model_north", float),
("model_elevation", float),
("profile_offset", float),
("period", float),
("z_xx", complex),
("z_xx_error", float),
("z_xx_model_error", float),
("z_xy", complex),
("z_xy_error", float),
("z_xy_model_error", float),
("z_yx", complex),
("z_yx_error", float),
("z_yx_model_error", float),
("z_yy", complex),
("z_yy_error", float),
("z_yy_model_error", float),
("t_zx", complex),
("t_zx_error", float),
("t_zx_model_error", float),
("t_zy", complex),
("t_zy_error", float),
("t_zy_model_error", float),
("t_mag_real", float),
("t_mag_real_error", float),
("t_mag_real_model_error", float),
("t_mag_imag", float),
("t_mag_imag_error", float),
("t_mag_imag_model_error", float),
("t_angle_real", float),
("t_angle_real_error", float),
("t_angle_real_model_error", float),
("t_angle_imag", float),
("t_angle_imag_error", float),
("t_angle_imag_model_error", float),
("res_xx", float),
("res_xx_error", float),
("res_xx_model_error", float),
("res_xy", float),
("res_xy_error", float),
("res_xy_model_error", float),
("res_yx", float),
("res_yx_error", float),
("res_yx_model_error", float),
("res_yy", float),
("res_yy_error", float),
("res_yy_model_error", float),
("phase_xx", float),
("phase_xx_error", float),
("phase_xx_model_error", float),
("phase_xy", float),
("phase_xy_error", float),
("phase_xy_model_error", float),
("phase_yx", float),
("phase_yx_error", float),
("phase_yx_model_error", float),
("phase_yy", float),
("phase_yy_error", float),
("phase_yy_model_error", float),
("pt_xx", float),
("pt_xx_error", float),
("pt_xx_model_error", float),
("pt_xy", float),
("pt_xy_error", float),
("pt_xy_model_error", float),
("pt_yx", float),
("pt_yx_error", float),
("pt_yx_model_error", float),
("pt_yy", float),
("pt_yy_error", float),
("pt_yy_model_error", float),
("pt_phimin", float),
("pt_phimin_error", float),
("pt_phimin_model_error", float),
("pt_phimax", float),
("pt_phimax_error", float),
("pt_phimax_model_error", float),
("pt_azimuth", float),
("pt_azimuth_error", float),
("pt_azimuth_model_error", float),
("pt_skew", float),
("pt_skew_error", float),
("pt_skew_model_error", float),
("pt_ellipticity", float),
("pt_ellipticity_error", float),
("pt_ellipticity_model_error", float),
("pt_det", float),
("pt_det_error", float),
("pt_det_model_error", float),
("rms_zxx", float),
("rms_zxy", float),
("rms_zyx", float),
("rms_zyy", float),
("rms_tzx", float),
("rms_tzy", float),
]
self._index_dict = {
"xx": {"ii": 0, "jj": 0},
"xy": {"ii": 0, "jj": 1},
"yx": {"ii": 1, "jj": 0},
"yy": {"ii": 1, "jj": 1},
"zx": {"ii": 0, "jj": 0},
"zy": {"ii": 0, "jj": 1},
}
self._key_dict = {
"z": "z",
"res": "resistivity",
"phase": "phase",
"pt": "phase_tensor",
"t": "tipper",
}
self._station_location_attrs = [
"survey",
"station",
"latitude",
"longitude",
"elevation",
"datum_epsg",
"east",
"north",
"utm_epsg",
"model_east",
"model_north",
"model_elevation",
"profile_offset",
]
if data is not None:
self.dataframe = self._validate_data(data)
else:
self.dataframe = self._get_initial_df(n_entries)
self.working_survey = None
self.working_station = None
for key, value in kwargs.items():
setattr(self, key, value)
def __str__(self) -> str:
"""
String representation of MTDataFrame.
Returns
-------
str
String representation of the dataframe or empty message
"""
if self._has_data():
return self.dataframe.__str__()
else:
return "Empty MTStationDataFrame"
def __repr__(self) -> str:
"""
Representation of MTDataFrame.
Returns
-------
str
String representation of the dataframe or empty constructor
"""
if self._has_data():
return self.dataframe.__repr__()
else:
return "MTStationDataFrame()"
@property
def _column_names(self) -> list[str]:
"""
List of all column names in the dataframe.
Returns
-------
list of str
Column names extracted from dtype list
"""
return [col[0] for col in self._dtype_list]
@property
def _pt_attrs(self) -> list[str]:
"""
List of phase tensor attribute column names.
Returns
-------
list of str
Column names starting with 'pt'
"""
return [col for col in self._column_names if col.startswith("pt")]
@property
def _tipper_attrs(self) -> list[str]:
"""
List of tipper attribute column names.
Returns
-------
list of str
Column names starting with 't_'
"""
return [col for col in self._column_names if col.startswith("t_")]
def __eq__(self, other: Any) -> bool:
"""
Compare two MTDataFrame objects for equality.
Parameters
----------
other : MTDataFrame or compatible data type
Another dataframe to compare with
Returns
-------
bool
True if dataframes are equal, False otherwise
"""
other = self._validate_data(other)
return (self.dataframe == other).all().all()
@property
def nonzero_items(self) -> int:
"""
Count number of non-zero entries in data columns.
Returns
-------
int
Number of non-zero entries excluding error columns
"""
if self._has_data():
cols = [
dtype[0] for dtype in self._dtype_list[14:] if "error" not in dtype[0]
]
return np.count_nonzero(self.dataframe[cols])
else:
return 0
def _validate_data(
self, data: dict | np.ndarray | pd.DataFrame | "MTDataFrame" | None
) -> pd.DataFrame | None:
"""
Validate and convert input data to standardized DataFrame format.
Parameters
----------
data : dict, np.ndarray, pd.DataFrame, MTDataFrame, or None
Input data to validate
Returns
-------
pd.DataFrame or None
Validated and standardized DataFrame
Raises
------
TypeError
If data type is not supported
"""
if data is None:
return
if isinstance(data, (dict, np.ndarray, pd.DataFrame)):
df = pd.DataFrame(data)
elif isinstance(data, (MTDataFrame)):
df = data.dataframe
else:
raise TypeError(f"Input data must be a pandas.DataFrame not {type(data)}")
missing_cols = [col for col in self._dtype_list if col[0] not in df.columns]
if missing_cols:
missing_df = pd.DataFrame(
{
col_name: np.zeros(df.shape[0], dtype=col_dtype)
for col_name, col_dtype in missing_cols
},
index=df.index,
)
df = pd.concat([df, missing_df], axis=1)
# resort to the desired column order
if df.columns.to_list() != self._column_names:
df = df[self._column_names]
return df
def _get_initial_df(self, n_entries: int = 0) -> pd.DataFrame:
"""
Create an empty DataFrame with the standard MT data structure.
Parameters
----------
n_entries : int, optional
Number of empty rows to create, by default 0
Returns
-------
pd.DataFrame
Empty DataFrame with standard columns
"""
return pd.DataFrame(np.empty(n_entries, dtype=np.dtype(self._dtype_list)))
def _has_data(self) -> bool:
"""
Check if dataframe contains any data.
Returns
-------
bool
True if dataframe has rows, False otherwise
"""
if self.dataframe is None:
return False
elif self.dataframe.shape[0] > 0:
return True
return False
[docs]
def get_station_df(self, station: str | None = None) -> pd.DataFrame:
"""
Get DataFrame for a single station.
Parameters
----------
station : str, optional
Station name to retrieve, by default None (uses working_station)
Returns
-------
pd.DataFrame
DataFrame filtered for the specified station
Raises
------
ValueError
If station is not found in dataframe
"""
if station is not None:
self.working_station = station
if self._has_data():
if self.working_station is None:
self.working_station = self.dataframe.station.unique()[0]
if self.working_station not in self.dataframe.station.values:
raise ValueError(
f"Could not find station {self.working_station} in dataframe."
)
return self.dataframe[self.dataframe.station == self.working_station]
@property
def size(self) -> int | None:
"""
Number of periods in the dataframe.
Returns
-------
int or None
Number of unique periods, or None if no data
"""
if self._has_data():
return self.period.size
def _get_index(self, comp: str) -> dict[str, int] | None:
"""
Get component index values for tensor elements.
Parameters
----------
comp : str
Component identifier: 'xx', 'xy', 'yx', 'yy', 'zx', or 'zy'
Returns
-------
dict or None
Dictionary with 'ii' and 'jj' keys for array indices, or None if invalid
"""
if comp in self._index_dict.keys():
return self._index_dict[comp]
def _get_key_index(self, key: str) -> dict[str, int] | None:
"""
Get index from a column key name.
Parameters
----------
key : str
Column name (e.g., 'z_xy', 't_zx')
Returns
-------
dict or None
Dictionary with 'ii' and 'jj' keys for array indices
"""
if key.count("_") > 0:
comp = key.split("_")[1]
return self._get_index(comp)
@property
def period(self) -> np.ndarray | None:
"""
Array of unique periods in sorted order.
Returns
-------
np.ndarray or None
Sorted array of period values, or None if no data
"""
if self._has_data():
return np.sort(self.dataframe.period.unique())
@property
def frequency(self) -> np.ndarray | None:
"""
Array of unique frequencies in Hz.
Returns
-------
np.ndarray or None
Array of frequency values (1/period), or None if no data
"""
if self._has_data():
return 1.0 / self.period
[docs]
def get_period(self, period: float, tol: float | None = None) -> "MTDataFrame":
"""
Get data for a specific period or period range.
Parameters
----------
period : float
Target period value in seconds
tol : float, optional
Tolerance as a fraction (e.g., 0.05 for 5%), by default None
Returns
-------
MTDataFrame
New MTDataFrame containing only the requested period(s)
"""
if tol is not None:
return MTDataFrame(
self.dataframe[
(self.dataframe.period > period * (1 - tol)) & self.dataframe.period
< period * (1 + tol)
]
)
else:
return MTDataFrame(self.dataframe[self.dataframe.period == period])
@property
def survey(self) -> str | None:
"""
Survey name from the dataframe.
Returns
-------
str or None
Survey name, or None if no data
"""
if self._has_data():
if self.working_survey is None:
self.working_survey = self.dataframe.survey.unique()[0]
return self.working_survey
@survey.setter
def survey(self, value: str) -> None:
"""
Set survey name in the dataframe.
Parameters
----------
value : str
Survey name to set
"""
if self._has_data():
if self.working_survey in [None, ""]:
self.dataframe.loc[self.dataframe.survey == "", "survey"] = value
self.working_survey = value
@property
def station(self) -> str | None:
"""
Station name from the dataframe.
Returns
-------
str or None
Station name, or None if no data
"""
if self._has_data():
if self.working_station is None:
self.working_station = self.dataframe.station.unique()[0]
return self.working_station
@station.setter
def station(self, value: str) -> None:
"""
Set station name in the dataframe.
Parameters
----------
value : str
Station name to set
"""
if self._has_data():
if self.working_station in [None, ""]:
self.dataframe.loc[self.dataframe.station == "", "station"] = value
self.working_station = value
@property
def latitude(self) -> float | None:
"""
Station latitude in decimal degrees.
Returns
-------
float or None
Latitude value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "latitude"
].unique()[0]
@latitude.setter
def latitude(self, value: float) -> None:
"""
Set station latitude.
Parameters
----------
value : float
Latitude in decimal degrees
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "latitude"] = (
value
)
@property
def longitude(self) -> float | None:
"""
Station longitude in decimal degrees.
Returns
-------
float or None
Longitude value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "longitude"
].unique()[0]
@longitude.setter
def longitude(self, value: float) -> None:
"""
Set station longitude.
Parameters
----------
value : float
Longitude in decimal degrees
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "longitude"] = (
value
)
@property
def elevation(self) -> float | None:
"""
Station elevation in meters.
Returns
-------
float or None
Elevation value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "elevation"
].unique()[0]
@elevation.setter
def elevation(self, value: float) -> None:
"""
Set station elevation.
Parameters
----------
value : float
Elevation in meters
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "elevation"] = (
value
)
@property
def datum_epsg(self) -> str | None:
"""
Datum EPSG code.
Returns
-------
str or None
EPSG code string, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "datum_epsg"
].unique()[0]
@datum_epsg.setter
def datum_epsg(self, value: str) -> None:
"""
Set datum EPSG code.
Parameters
----------
value : str
EPSG code string
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "datum_epsg"] = (
str(value) if value is not None else ""
)
@property
def east(self) -> float | None:
"""
Station easting coordinate in meters.
Returns
-------
float or None
Easting value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "east"
].unique()[0]
@east.setter
def east(self, value: float) -> None:
"""
Set station easting coordinate.
Parameters
----------
value : float
Easting in meters
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "east"] = value
@property
def north(self) -> float | None:
"""
Station northing coordinate in meters.
Returns
-------
float or None
Northing value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "north"
].unique()[0]
@north.setter
def north(self, value: float) -> None:
"""
Set station northing coordinate.
Parameters
----------
value : float
Northing in meters
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "north"] = value
@property
def utm_epsg(self) -> str | None:
"""
UTM EPSG code.
Returns
-------
str or None
EPSG code string, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "utm_epsg"
].unique()[0]
@utm_epsg.setter
def utm_epsg(self, value: str) -> None:
"""
Set UTM EPSG code.
Parameters
----------
value : str
EPSG code string
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "utm_epsg"] = (
str(value) if value is not None else ""
)
@property
def model_east(self) -> float | None:
"""
Model easting coordinate in meters.
Returns
-------
float or None
Model easting value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "model_east"
].unique()[0]
@model_east.setter
def model_east(self, value: float) -> None:
"""
Set model easting coordinate.
Parameters
----------
value : float
Model easting in meters
"""
if self._has_data():
self.dataframe.loc[self.dataframe.station == self.station, "model_east"] = (
value
)
@property
def model_north(self) -> float | None:
"""
Model northing coordinate in meters.
Returns
-------
float or None
Model northing value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "model_north"
].unique()[0]
@model_north.setter
def model_north(self, value: float) -> None:
"""
Set model northing coordinate.
Parameters
----------
value : float
Model northing in meters
"""
if self._has_data():
self.dataframe.loc[
self.dataframe.station == self.station, "model_north"
] = value
@property
def model_elevation(self) -> float | None:
"""
Model elevation in meters.
Returns
-------
float or None
Model elevation value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "model_elevation"
].unique()[0]
@model_elevation.setter
def model_elevation(self, value: float) -> None:
"""
Set model elevation.
Parameters
----------
value : float
Model elevation in meters
"""
if self._has_data():
self.dataframe.loc[
self.dataframe.station == self.station,
"model_elevation",
] = value
@property
def profile_offset(self) -> float | None:
"""
Distance along profile in meters.
Returns
-------
float or None
Profile offset value, or None if no data
"""
if self._has_data():
return self.dataframe.loc[
self.dataframe.station == self.station, "profile_offset"
].unique()[0]
@profile_offset.setter
def profile_offset(self, value: float) -> None:
"""
Set distance along profile.
Parameters
----------
value : float
Profile offset in meters
"""
if self._has_data():
self.dataframe.loc[
self.dataframe.station == self.station,
"profile_offset",
] = value
def _get_empty_impedance_array(self, dtype: type = complex) -> np.ndarray:
"""
Create an empty impedance tensor array.
Parameters
----------
dtype : type, optional
Data type for the array, by default complex
Returns
-------
np.ndarray
Array of shape (n_periods, 2, 2) filled with zeros
"""
return np.zeros((self.period.size, 2, 2), dtype=complex)
@property
def impedance(self) -> np.ndarray:
"""
Impedance tensor from dataframe.
Returns
-------
np.ndarray
Impedance tensor of shape (n_periods, 2, 2)
"""
z = self._get_empty_impedance_array()
for key in ["zxx", "zxy", "zyx", "zyy"]:
index = self._get_index(key)
z[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, key
]
return z
def _get_data_array(self, obj: Any, attr: str) -> np.ndarray | None:
"""
Get data array from an object's attribute.
Parameters
----------
obj : object
Object to retrieve attribute from
attr : str
Attribute name
Returns
-------
np.ndarray or None
Data array if attribute exists, None otherwise
"""
try:
return getattr(obj, attr)
except TypeError:
return None
def _fill_data(
self,
data_array: np.ndarray | None,
column: str,
index: dict[str, int] | None,
) -> None:
"""
Fill dataframe column with data from an array.
Parameters
----------
data_array : np.ndarray or None
Data to fill into the dataframe
column : str
Column name to fill
index : dict or None
Dictionary with 'ii' and 'jj' keys for array slicing, or None for 1D data
"""
if data_array is not None:
if index is None:
self.dataframe.loc[
self.dataframe.station == self.station,
column,
] = data_array[:]
else:
self.dataframe.loc[
self.dataframe.station == self.station,
column,
] = data_array[:, index["ii"], index["jj"]]
[docs]
def from_z_object(self, z_object: Z) -> None:
"""
Fill dataframe from a Z impedance object.
Parameters
----------
z_object : Z
Z object containing impedance tensor data
Notes
-----
Populates impedance, resistivity, phase, and phase tensor columns
"""
self.dataframe.loc[self.dataframe.station == self.station, "period"] = (
z_object.period
)
# should make a copy of the phase tensor otherwise it gets calculated
# multiple times and becomes a time sink.
pt_object = z_object.phase_tensor.copy()
for error in ["", "_error", "_model_error"]:
if getattr(z_object, f"_has_tf{error}")():
for key in ["z", "res", "phase"]:
obj_key = self._key_dict[key]
data_array = getattr(z_object, f"{obj_key}{error}").copy()
for comp in ["xx", "xy", "yx", "yy"]:
index = self._get_index(comp)
# if key in ["pt"]:
# data_array = self._get_data_array(
# obj, f"{key}{error}"
# )
# else:
# data_array = self._get_data_array(
# z_object, f"{obj_key}{error}"
# )
self._fill_data(data_array, f"{key}_{comp}{error}", index)
## phase tensor
data_array = getattr(pt_object, f"pt{error}")
for comp in ["xx", "xy", "yx", "yy"]:
index = self._get_index(comp)
self._fill_data(data_array, f"pt_{comp}{error}", index)
# PT attributes
for pt_attr in [
"phimin",
"phimax",
"azimuth",
"skew",
"ellipticity",
"det",
]:
data_array = self._get_data_array(pt_object, f"{pt_attr}{error}")
self._fill_data(data_array, f"pt_{pt_attr}{error}", None)
[docs]
def from_t_object(self, t_object: Tipper) -> None:
"""
Fill dataframe from a Tipper object.
Parameters
----------
t_object : Tipper
Tipper object containing tipper data
Notes
-----
Populates tipper magnitude, angle, and component columns
"""
self.dataframe.loc[self.dataframe.station == self.station, "period"] = (
t_object.period
)
for error in ["", "_error", "_model_error"]:
if getattr(t_object, f"_has_tf{error}")():
obj_key = self._key_dict["t"]
for comp in ["zx", "zy"]:
index = self._get_index(comp)
data_array = self._get_data_array(t_object, f"{obj_key}{error}")
self._fill_data(data_array, f"t_{comp}{error}", index)
if error in [""]:
for t_attr in [
"mag_real",
"mag_imag",
"angle_real",
"angle_imag",
]:
data_array = self._get_data_array(t_object, t_attr)
self._fill_data(data_array, f"t_{t_attr}", None)
[docs]
def to_z_object(self, units: str = "mt") -> Z:
"""
Create a Z impedance object from dataframe data.
Parameters
----------
units : str, optional
Impedance units ('mt' or 'ohm'), by default 'mt'
Returns
-------
Z
Z object containing impedance tensor
Notes
-----
If impedance values are zero, attempts to reconstruct from
resistivity and phase data
"""
nf = self.period.size
z = np.zeros((nf, 2, 2), dtype=complex)
z_err = np.zeros((nf, 2, 2), dtype=float)
z_model_err = np.zeros((nf, 2, 2), dtype=float)
res = np.zeros((nf, 2, 2), dtype=float)
res_err = np.zeros((nf, 2, 2), dtype=float)
res_model_err = np.zeros((nf, 2, 2), dtype=float)
phase = np.zeros((nf, 2, 2), dtype=float)
phase_err = np.zeros((nf, 2, 2), dtype=float)
phase_model_err = np.zeros((nf, 2, 2), dtype=float)
for comp in ["xx", "xy", "yx", "yy"]:
index = self._get_index(comp)
z[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"z_{comp}"
]
z_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"z_{comp}_error"
]
z_model_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"z_{comp}_model_error"
]
z_object = Z(z, z_err, self.frequency, z_model_err, units=units)
if (z == 0).all():
for comp in ["xx", "xy", "yx", "yy"]:
index = self._get_index(comp)
### resistivity
res[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"res_{comp}"
]
res_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"res_{comp}_error"
]
res_model_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station,
f"res_{comp}_model_error",
]
### Phase
phase[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"phase_{comp}"
]
phase_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station,
f"phase_{comp}_error",
]
phase_model_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station,
f"phase_{comp}_model_error",
]
if not (res == 0).all():
if not (phase == 0).all():
z_object.set_resistivity_phase(
res,
phase,
self.frequency,
res_error=res_err,
phase_error=phase_err,
res_model_error=res_model_err,
phase_model_error=phase_model_err,
)
else:
raise ValueError("cannot estimate Z without phase information")
return z_object
[docs]
def to_t_object(self) -> Tipper:
"""
Create a Tipper object from dataframe data.
Returns
-------
Tipper
Tipper object containing tipper data
"""
nf = self.period.size
t = np.zeros((nf, 1, 2), dtype=complex)
t_err = np.zeros((nf, 1, 2), dtype=float)
t_model_err = np.zeros((nf, 1, 2), dtype=float)
for comp in ["zx", "zy"]:
index = self._get_index(comp)
t[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"t_{comp}"
]
t_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"t_{comp}_error"
]
t_model_err[:, index["ii"], index["jj"]] = self.dataframe.loc[
self.dataframe.station == self.station, f"t_{comp}_model_error"
]
return Tipper(t, t_err, self.frequency, t_model_err)
@property
def station_locations(self) -> pd.DataFrame:
"""
DataFrame of station location information.
Returns
-------
pd.DataFrame
DataFrame with one row per station containing location attributes
"""
return (
self.dataframe.groupby("station")
.nth(0)[self._station_location_attrs]
.reset_index()
)
@property
def phase_tensor(self) -> pd.DataFrame:
"""
DataFrame with phase tensor information.
Returns
-------
pd.DataFrame
DataFrame containing location and phase tensor attributes
"""
return self.dataframe[
self._station_location_attrs + self._pt_attrs
].reset_index()
@property
def tipper(self) -> pd.DataFrame:
"""
DataFrame with tipper information.
Returns
-------
pd.DataFrame
DataFrame containing location and tipper attributes
"""
return self.dataframe[
self._station_location_attrs + self._tipper_attrs
].reset_index()
@property
def for_shapefiles(self) -> pd.DataFrame:
"""
DataFrame formatted for shapefile export.
Returns
-------
pd.DataFrame
DataFrame with location, phase tensor, and tipper attributes
"""
return self.dataframe[
self._station_location_attrs + self._pt_attrs + self._tipper_attrs
].reset_index()
[docs]
def get_station_distances(self, utm: bool = False) -> pd.Series:
"""
Calculate pairwise distances between stations.
Parameters
----------
utm : bool, optional
If True, use UTM coordinates (east, north), otherwise use
geographic coordinates (longitude, latitude), by default False
Returns
-------
pd.Series
Series of non-zero pairwise distances between stations
"""
if utm:
x_key = "east"
y_key = "north"
else:
x_key = "longitude"
y_key = "latitude"
sdf = self.station_locations
distances = pdist(sdf[[x_key, y_key]].values, metric="euclidean")
distances = distances[np.nonzero(distances)]
return pd.Series(distances)