Skip to content

Commit 2e6a744

Browse files
authored
Merge pull request #100 from MTgeophysics/updates
Updates to Simpeg inversions
2 parents 386a776 + 03ce0e2 commit 2e6a744

22 files changed

Lines changed: 1191 additions & 279 deletions

File tree

.github/workflows/testing_in_conda.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ jobs:
9696
python -c "import pandas as pd; print(f'pandas version: {pd.__version__}')"
9797
conda list
9898
99+
- name: Run Simpeg1D tests (non-Windows)
100+
if: runner.os != 'Windows'
101+
shell: bash
102+
run: |
103+
source ~/.profile
104+
conda init --all
105+
conda activate mtpy-v2-test
106+
pytest -rA -q tests/modeling/simpeg/test_simpeg_1d_inversion_recipe.py
107+
99108
- name: Run Tests
100109
shell: bash
101110
run: |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Documentation Status](https://readthedocs.org/projects/mtpy-v2/badge/?version=latest)](https://mtpy-v2.readthedocs.io/en/latest/?badge=latest)
88
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/MTgeophysics/mtpy-v2/main)
99

10-
## Version 2.1.1
10+
## Version 2.1.2
1111

1212
# Description
1313

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@
6767
#
6868

6969
# The short X.Y version.
70-
version = "2.1.1"
70+
version = "2.1.2"
7171
# The full version, including alpha/beta/rc tags.
72-
release = "2.1.1"
72+
release = "2.1.2"
7373

7474
# The language for content autogenerated by Sphinx. Refer to documentation
7575
# for a list of supported languages.

mtpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from mtpy.imaging.mtcolors import MT_CMAP_DICT, register_cmaps
1919

2020

21-
__version__ = "2.1.1"
21+
__version__ = "2.1.2"
2222
__all__ = ["MT", "MTData", "MTCollection"]
2323

2424
# =============================================================================

mtpy/core/mt.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,8 @@ def to_dataframe(
986986
cols : list of str, optional
987987
Column names to include, by default None
988988
impedance_units : str, optional
989-
Impedance units, "mt" [mV/km/nT] or "ohm" [Ohms], by default "mt"
989+
Impedance output units in the dataframe,
990+
"mt" [mV/km/nT] or "ohm" [Ohms], by default "mt"
990991
991992
Returns
992993
-------
@@ -1017,8 +1018,10 @@ def to_dataframe(
10171018
mt_df.dataframe.loc[:, "period"] = self.period
10181019
if self.has_impedance():
10191020
z_object = self.Z
1020-
z_object.units = impedance_units
1021-
mt_df.from_z_object(z_object)
1021+
# Keep Z internal storage in mt units while exposing dataframe values
1022+
# in requested output units.
1023+
z_object.output_units = impedance_units
1024+
mt_df.from_z_object(z_object, units=impedance_units)
10221025
if self.has_tipper():
10231026
mt_df.from_t_object(self.Tipper)
10241027

@@ -1035,7 +1038,8 @@ def from_dataframe(
10351038
mt_df : MTDataFrame or DataFrame-like
10361039
Dataframe containing MT data for a single station
10371040
impedance_units : str, optional
1038-
Impedance units, "mt" [mV/km/nT] or "ohm" [Ohms], by default "mt"
1041+
Impedance units represented in the dataframe,
1042+
"mt" [mV/km/nT] or "ohm" [Ohms], by default "mt"
10391043
10401044
Raises
10411045
------
@@ -1077,8 +1081,9 @@ def from_dataframe(
10771081

10781082
self.tf_id = self.station
10791083

1080-
z_obj = mt_df.to_z_object()
1081-
z_obj.units = impedance_units
1084+
# Construct with dataframe units so incoming arrays are interpreted
1085+
# correctly, and keep output units aligned with MT impedance units.
1086+
z_obj = mt_df.to_z_object(units=impedance_units)
10821087
self.Z = z_obj
10831088
self.Tipper = mt_df.to_t_object()
10841089

@@ -1542,7 +1547,9 @@ def to_occam1d(
15421547

15431548
return occam_data
15441549

1545-
def to_simpeg_1d(self, mode: str = "det", **kwargs: Any) -> Simpeg1D:
1550+
def to_simpeg_1d(
1551+
self, mode: str = "det", resistivity_error=10, phase_error=2.5, **kwargs: Any
1552+
) -> Simpeg1D:
15461553
"""
15471554
Run a 1D inversion using SimPEG.
15481555
@@ -1577,7 +1584,13 @@ def to_simpeg_1d(self, mode: str = "det", **kwargs: Any) -> Simpeg1D:
15771584
if not self.Z._has_tf_model_error():
15781585
self.compute_model_z_errors()
15791586
self.logger.info("Using default errors for impedance")
1580-
simpeg_1d = Simpeg1D(self.to_dataframe(), mode=mode, **kwargs)
1587+
simpeg_1d = Simpeg1D(
1588+
self.to_dataframe(impedance_units="mt"),
1589+
mode=mode,
1590+
resistivity_error=resistivity_error,
1591+
phase_error=phase_error,
1592+
**kwargs,
1593+
)
15811594
simpeg_1d.run_fixed_layer_inversion(**kwargs)
15821595
simpeg_1d.plot_model_fitting(fig_num=1)
15831596
simpeg_1d.plot_response(fig_num=2, **kwargs)

mtpy/core/mt_data.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,12 @@ def rotate_dask(
12521252
) -> "MTData":
12531253
"""Rotate stations using dask-delayed execution.
12541254
1255+
**Note**: Rotation is defined as a rotation of the coordinate reference frame
1256+
of the transfer function, not a rotation of the physical measurement. So,
1257+
for example, a 90 degree rotation of a station with NED coordinates would
1258+
swap the North and East components of the transfer function and change the
1259+
coordinate reference frame to ENU.
1260+
12551261
Parameters
12561262
----------
12571263
rotation_angle : float or ndarray
@@ -1720,9 +1726,10 @@ def _station_dataset_to_dataframe(
17201726
z_error=tf_error,
17211727
frequency=1.0 / period,
17221728
z_model_error=tf_model_error,
1723-
units=impedance_units,
1729+
input_units="mt",
1730+
output_units=impedance_units,
17241731
)
1725-
station_df.from_z_object(z_object)
1732+
station_df.from_z_object(z_object, units=impedance_units)
17261733

17271734
t_output = self._pick_channel_labels(output_labels, ["hz", "z"], 1)
17281735
t_inputs = self._pick_channel_labels(
@@ -3318,6 +3325,16 @@ def rotate(
33183325
) -> "MTData" | None:
33193326
"""Rotate all station transfer functions.
33203327
3328+
**Note**: This method is not intended for general coordinate
3329+
rotation of station locations. It is designed to rotate the
3330+
impedance and tipper channels of each station dataset
3331+
according to the specified angle and coordinate reference frame.
3332+
The station location coordinates are not modified by this method.
3333+
Rotation is off the coordinate system therefore a positive
3334+
clockwise rotation of 10 degrees will rotate the coordinate
3335+
system 10 degrees and the estimated strike angle will be 10
3336+
degrees less than the strike angle before rotation.
3337+
33213338
Parameters
33223339
----------
33233340
rotation_angle : float or ndarray

mtpy/core/mt_dataframe.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,34 +1049,39 @@ def _fill_data(
10491049
column,
10501050
] = data_array[:, index["ii"], index["jj"]]
10511051

1052-
def from_z_object(self, z_object: Z) -> None:
1052+
def from_z_object(self, z_object: Z, units="mt") -> None:
10531053
"""
10541054
Fill dataframe from a Z impedance object.
10551055
10561056
Parameters
10571057
----------
10581058
z_object : Z
10591059
Z object containing impedance tensor data
1060+
units : str, optional
1061+
Units to write impedance values into the dataframe,
1062+
by default "mt"
10601063
10611064
Notes
10621065
-----
10631066
Populates impedance, resistivity, phase, and phase tensor columns
10641067
10651068
"""
1069+
z_export = z_object.copy()
1070+
z_export.output_units = units
10661071

10671072
self.dataframe.loc[self.dataframe.station == self.station, "period"] = (
1068-
z_object.period
1073+
z_export.period
10691074
)
10701075

10711076
# should make a copy of the phase tensor otherwise it gets calculated
10721077
# multiple times and becomes a time sink.
1073-
pt_object = z_object.phase_tensor.copy()
1078+
pt_object = z_export.phase_tensor.copy()
10741079

10751080
for error in ["", "_error", "_model_error"]:
1076-
if getattr(z_object, f"_has_tf{error}")():
1081+
if getattr(z_export, f"_has_tf{error}")():
10771082
for key in ["z", "res", "phase"]:
10781083
obj_key = self._key_dict[key]
1079-
data_array = getattr(z_object, f"{obj_key}{error}").copy()
1084+
data_array = getattr(z_export, f"{obj_key}{error}").copy()
10801085
for comp in ["xx", "xy", "yx", "yy"]:
10811086
index = self._get_index(comp)
10821087
# if key in ["pt"]:
@@ -1150,7 +1155,9 @@ def to_z_object(self, units: str = "mt") -> Z:
11501155
Parameters
11511156
----------
11521157
units : str, optional
1153-
Impedance units ('mt' or 'ohm'), by default 'mt'
1158+
Units represented by impedance values in the dataframe
1159+
and returned by the resulting Z object ('mt' or 'ohm'),
1160+
by default 'mt'
11541161
11551162
Returns
11561163
-------
@@ -1191,7 +1198,14 @@ def to_z_object(self, units: str = "mt") -> Z:
11911198
self.dataframe.station == self.station, f"z_{comp}_model_error"
11921199
]
11931200

1194-
z_object = Z(z, z_err, self.frequency, z_model_err, units=units)
1201+
z_object = Z(
1202+
z,
1203+
z_err,
1204+
self.frequency,
1205+
z_model_err,
1206+
input_units=units,
1207+
output_units=units,
1208+
)
11951209

11961210
if (z == 0).all():
11971211
for comp in ["xx", "xy", "yx", "yy"]:
@@ -1224,6 +1238,8 @@ def to_z_object(self, units: str = "mt") -> Z:
12241238

12251239
if not (res == 0).all():
12261240
if not (phase == 0).all():
1241+
# Resistivity/phase-to-Z computation returns mt-based impedance.
1242+
z_object.input_units = "mt"
12271243
z_object.set_resistivity_phase(
12281244
res,
12291245
phase,

mtpy/core/transfer_function/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,14 @@ def rotate(
610610
positive angle. In this coordinate system the rotation matrix is
611611
the inverse of the conventional rotation matrix.
612612
613+
**Note**: Rotation is a rotation of the coordinate reference
614+
frame of the transfer function, not a rotation of the physical measurement.
615+
So, for example, if you have a transfer function that is referenced to
616+
a coordinate system that is rotated 30 degrees clockwise from north,
617+
then you would rotate by -30 degrees to get the transfer function in
618+
a coordinate system that is aligned with north. This is because the
619+
rotation is of the reference frame, not the physical measurement.
620+
613621
Parameters
614622
----------
615623
alpha : float or int or str or array-like

mtpy/core/transfer_function/tf_helpers.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,31 @@
66

77
import mtpy.utils.calculator as MTcc
88

9+
MT_TO_OHM_FACTOR = 1.0 / np.pi * np.sqrt(5.0 / 8.0) * 10**3.5
10+
IMPEDANCE_UNITS = {"mt": 1, "ohm": MT_TO_OHM_FACTOR}
11+
912

1013
def compute_resistivity(
11-
z_mt: np.ndarray | None, frequency: np.ndarray
14+
z_mt: np.ndarray | None, frequency: np.ndarray, z_units: str = "mt"
1215
) -> np.ndarray | None:
1316
"""Compute apparent resistivity from impedance in mt units."""
1417
if z_mt is None:
1518
return None
1619

17-
return np.apply_along_axis(
18-
lambda values: np.abs(values) ** 2 / frequency * 0.2,
19-
0,
20-
z_mt,
21-
)
20+
if z_units == "mt":
21+
return np.apply_along_axis(
22+
lambda values: np.abs(values) ** 2 / frequency * 0.2,
23+
0,
24+
z_mt,
25+
)
26+
elif z_units == "ohm":
27+
return np.apply_along_axis(
28+
lambda values: np.abs(values / IMPEDANCE_UNITS["ohm"]) ** 2
29+
* frequency
30+
* 0.2,
31+
0,
32+
z_mt,
33+
)
2234

2335

2436
def compute_phase(z_mt: np.ndarray | None) -> np.ndarray | None:
@@ -33,29 +45,43 @@ def compute_resistivity_error(
3345
z_mt: np.ndarray | None,
3446
z_error_mt: np.ndarray | None,
3547
frequency: np.ndarray,
48+
z_units: str = "mt",
3649
) -> np.ndarray | None:
3750
"""Compute apparent resistivity error from impedance and standard deviation."""
3851
if z_mt is None or z_error_mt is None:
3952
return None
4053

4154
with np.errstate(divide="ignore", invalid="ignore"):
42-
return np.apply_along_axis(
43-
lambda values: values / frequency * 0.2,
44-
0,
45-
2 * z_error_mt * np.abs(z_mt),
46-
)
55+
if z_units == "mt":
56+
return np.apply_along_axis(
57+
lambda values: values / frequency * 0.2,
58+
0,
59+
2 * z_error_mt * np.abs(z_mt),
60+
)
61+
elif z_units == "ohm":
62+
return np.apply_along_axis(
63+
lambda values: values / frequency * 0.2,
64+
0,
65+
2 * z_error_mt * np.abs(z_mt / IMPEDANCE_UNITS["ohm"]),
66+
)
4767

4868

4969
def compute_phase_error(
5070
z_mt: np.ndarray | None,
5171
z_error: np.ndarray | None,
72+
z_units: str = "mt",
5273
) -> np.ndarray | None:
5374
"""Compute impedance phase error in degrees from complex impedance error."""
5475
if z_mt is None or z_error is None:
5576
return None
5677

5778
with np.errstate(divide="ignore", invalid="ignore"):
58-
return np.degrees(np.arctan(z_error / np.abs(z_mt)))
79+
if z_units == "mt":
80+
return np.degrees(np.arctan(z_error / np.abs(z_mt)))
81+
elif z_units == "ohm":
82+
return np.degrees(
83+
np.arctan(z_error / np.abs(z_mt / IMPEDANCE_UNITS["ohm"]))
84+
)
5985

6086

6187
def compute_impedance_error(

0 commit comments

Comments
 (0)