Skip to content

Commit 797a922

Browse files
authored
Merge pull request #57 from MTgeophysics/fix_rotations_again
Fixing rotations, again. We have added tests, documentation, and a little flexibility in the rotations. Rotation is North into East in a NEZ+down reference frame (NED).
2 parents 8a2cb64 + ad24576 commit 797a922

File tree

19 files changed

+2146
-218
lines changed

19 files changed

+2146
-218
lines changed

docs/source/notebooks/rotations_conventions.ipynb

Lines changed: 1397 additions & 0 deletions
Large diffs are not rendered by default.

mtpy/core/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,20 @@
1313
"MTStations",
1414
"MTDataFrame",
1515
]
16+
17+
# coordinate reference frames
18+
COORDINATE_REFERENCE_FRAME_OPTIONS = {
19+
"+": "ned",
20+
"-": "enu",
21+
"z+": "ned",
22+
"z-": "enu",
23+
"nez+": "ned",
24+
"enz-": "enu",
25+
"ned": "ned",
26+
"enu": "enu",
27+
"exp(+ i\\omega t)": "ned",
28+
"exp(+i\\omega t)": "ned",
29+
"exp(- i\\omega t)": "enu",
30+
"exp(-i\\omega t)": "enu",
31+
None: "ned",
32+
}

mtpy/core/mt.py

Lines changed: 139 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from mt_metadata.transfer_functions.core import TF
1616

17-
from mtpy.core import Z, Tipper
17+
from mtpy.core import Z, Tipper, COORDINATE_REFERENCE_FRAME_OPTIONS
1818
from mtpy.core.mt_location import MTLocation
1919
from mtpy.core.mt_dataframe import MTDataFrame
2020
from mtpy.utils.estimate_tf_quality_factor import EMTFStats
@@ -38,8 +38,40 @@ class MT(TF, MTLocation):
3838
letter represents the output channels and the second letter represents
3939
the input channels.
4040
41-
For exampe for an input of Hx and an output of Ey the impedance tensor
41+
For example for an input of Hx and an output of Ey the impedance tensor
4242
element is Zyx.
43+
44+
Coordinate reference frame of the transfer function is by defualt is NED
45+
46+
- x = North
47+
- y = East
48+
- z = + Down
49+
50+
The other option is ENU
51+
52+
- x = East
53+
- y = North
54+
- z = + Up
55+
56+
Other input options for the NED are:
57+
58+
- "+"
59+
- "z+"
60+
- "nez+"
61+
- "ned"
62+
- "exp(+ i\\omega t)"
63+
- "exp(+i\\omega t)"
64+
- None
65+
66+
And for ENU:
67+
68+
- "-"
69+
- "z-"
70+
- "enz-"
71+
- "enu"
72+
- "exp(- i\\omega t)"
73+
- "exp(-i\\omega t)"
74+
4375
"""
4476

4577
def __init__(self, fn=None, **kwargs):
@@ -65,9 +97,6 @@ def __init__(self, fn=None, **kwargs):
6597
TF.__init__(self, **tf_kwargs)
6698
MTLocation.__init__(self, survey_metadata=self._survey_metadata)
6799

68-
# MTLocation.__init__(self)
69-
# TF.__init__(self)
70-
71100
self.fn = fn
72101

73102
self._Z = Z()
@@ -76,6 +105,14 @@ def __init__(self, fn=None, **kwargs):
76105

77106
self.save_dir = Path.cwd()
78107

108+
self._coordinate_reference_frame_options = (
109+
COORDINATE_REFERENCE_FRAME_OPTIONS
110+
)
111+
112+
self.coordinate_reference_frame = (
113+
self.station_metadata.transfer_function.sign_convention
114+
)
115+
79116
for key, value in kwargs.items():
80117
setattr(self, key, value)
81118

@@ -115,9 +152,68 @@ def copy(self):
115152
"""Copy function."""
116153
return deepcopy(self)
117154

155+
@property
156+
def coordinate_reference_frame(self):
157+
f"""Coordinate reference frame of the transfer function
158+
159+
Deafualt is NED
160+
161+
- x = North
162+
- y = East
163+
- z = + down
164+
165+
Options are:
166+
167+
{self._coordinate_reference_frame_options}
168+
169+
"""
170+
171+
return self._coordinate_reference_frame_options[
172+
self.station_metadata.transfer_function.sign_convention
173+
].upper()
174+
175+
@coordinate_reference_frame.setter
176+
def coordinate_reference_frame(self, value):
177+
"""set coordinate_reference_frame
178+
179+
options are NED, ENU
180+
181+
NED
182+
183+
- x = North
184+
- y = East
185+
- z = + down
186+
187+
ENU
188+
189+
- x = East
190+
- y = North
191+
- z = + up
192+
"""
193+
194+
if value is None:
195+
value = "+"
196+
if value.lower() not in self._coordinate_reference_frame_options:
197+
raise ValueError(
198+
f"{value} is not understood as a reference frame. "
199+
f"Options are {self._coordinate_reference_frame_options}"
200+
)
201+
if value in ["ned"] or "+" in value:
202+
value = "+"
203+
elif value in ["enu"] or "-" in value:
204+
value = "-"
205+
self.logger.warning(
206+
"MTpy-v2 is assumes a NED coordinate system where x=North, "
207+
"y=East, z=+down. By changing to ENU there maybe some "
208+
"incorrect values for angles and derivative products of the "
209+
"impedance tensor."
210+
)
211+
212+
self.station_metadata.transfer_function.sign_convention = value
213+
118214
@property
119215
def rotation_angle(self):
120-
"""Rotation angle in degrees from north."""
216+
"""Rotation angle in degrees from north. In the coordinate reference frame"""
121217
return self._rotation_angle
122218

123219
@rotation_angle.setter
@@ -127,7 +223,7 @@ def rotation_angle(self, theta_r):
127223
128224
upon setting rotates Z and Tipper
129225
130-
TODO figure this out with xarray
226+
TODO: figure this out with xarray
131227
"""
132228

133229
self.rotate(theta_r)
@@ -136,32 +232,49 @@ def rotation_angle(self, theta_r):
136232
def rotate(self, theta_r, inplace=True):
137233
"""Rotate the data in degrees assuming North is 0 measuring clockwise
138234
positive to East as 90.
139-
:param theta_r: DESCRIPTION.
140-
:type theta_r: TYPE
141-
:param inplace: DESCRIPTION, defaults to True.
142-
:type inplace: TYPE, optional
143-
:return: DESCRIPTION.
144-
:rtype: TYPE
235+
236+
:param theta_r: rotation angle to rotate by in degrees.
237+
:type theta_r: float
238+
:param inplace: rotate all transfer function in place, defaults to True.
239+
:type inplace: bool, optional
240+
:return: if inplace is False, returns a new MT object.
241+
:rtype: MT object
242+
145243
"""
146244

245+
if self.has_impedance():
246+
new_z = self.Z.rotate(
247+
theta_r,
248+
inplace=False,
249+
coordinate_reference_frame=self.coordinate_reference_frame,
250+
)
251+
if self.has_tipper():
252+
new_t = self.Tipper.rotate(
253+
theta_r,
254+
inplace=False,
255+
coordinate_reference_frame=self.coordinate_reference_frame,
256+
)
257+
147258
if inplace:
148259
if self.has_impedance():
149-
self.Z = self.Z.rotate(theta_r)
260+
self.Z = new_z
150261
if self.has_tipper():
151-
self.Tipper = self.Tipper.rotate(theta_r)
262+
self.Tipper = new_t
152263

153-
self._rotation_angle = theta_r
264+
self._rotation_angle += theta_r
154265

155266
self.logger.info(
156-
f"Rotated transfer function by: {self._rotation_angle:.3f} degrees clockwise"
267+
f"Rotated transfer function by: {self._rotation_angle:.3f} "
268+
"degrees clockwise in reference frame "
269+
f"{self.coordinate_reference_frame}."
157270
)
158271
else:
159272
new_m = self.clone_empty()
160273
if self.has_impedance():
161-
new_m.Z = self.Z.rotate(theta_r)
274+
new_m.Z = new_z
162275
if self.has_tipper():
163-
new_m.Tipper = self.Tipper.rotate(theta_r)
164-
new_m._rotation_angle = self._rotation_angle
276+
new_m.Tipper = new_t
277+
new_m._rotation_angle += theta_r
165278
return new_m
166279

167280
@property
@@ -959,7 +1072,9 @@ def add_white_noise(self, value, inplace=True):
9591072
] = self._transfer_function.transfer_function.real * (
9601073
noise_real
9611074
) + (
962-
1j * self._transfer_function.transfer_function.imag * noise_imag
1075+
1j
1076+
* self._transfer_function.transfer_function.imag
1077+
* noise_imag
9631078
)
9641079

9651080
self._transfer_function["transfer_function_error"] = (
@@ -973,7 +1088,9 @@ def add_white_noise(self, value, inplace=True):
9731088
] = self._transfer_function.transfer_function.real * (
9741089
noise_real
9751090
) + (
976-
1j * self._transfer_function.transfer_function.imag * noise_imag
1091+
1j
1092+
* self._transfer_function.transfer_function.imag
1093+
* noise_imag
9771094
)
9781095

9791096
self._transfer_function["transfer_function_error"] = (

mtpy/core/mt_data.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from .mt import MT
2222
from .mt_stations import MTStations
23-
from mtpy.core import MTDataFrame
23+
from mtpy.core import MTDataFrame, COORDINATE_REFERENCE_FRAME_OPTIONS
2424

2525
from mtpy.modeling.errors import ModelErrors
2626
from mtpy.modeling.modem import Data
@@ -96,6 +96,10 @@ def __init__(self, mt_list=None, **kwargs):
9696
"model_parameters",
9797
]
9898

99+
self._coordinate_reference_frame_options = (
100+
COORDINATE_REFERENCE_FRAME_OPTIONS
101+
)
102+
99103
def _validate_item(self, mt_obj):
100104
"""Make sure intpu is an MT object.
101105
@@ -206,6 +210,51 @@ def clone_empty(self):
206210

207211
return md
208212

213+
@property
214+
def coordinate_reference_frame(self):
215+
"""coordinate reference frame ned or enu"""
216+
return self._coordinate_reference_frame
217+
218+
@coordinate_reference_frame.setter
219+
def coordinate_reference_frame(self, value):
220+
"""set coordinate_reference_frame
221+
222+
options are NED, ENU
223+
224+
NED
225+
226+
- x = North
227+
- y = East
228+
- z = + down
229+
230+
ENU
231+
232+
- x = East
233+
- y = North
234+
- z = + up
235+
"""
236+
237+
if value.lower() not in self._coordinate_reference_frame_options:
238+
raise ValueError(
239+
f"{value} is not understood as a reference frame. "
240+
f"Options are {self._coordinate_reference_frame_options}"
241+
)
242+
if value in ["ned"] or "+" in value:
243+
value = "+"
244+
elif value in ["enu"] or "-" in value:
245+
value = "-"
246+
self.logger.warning(
247+
"MTpy-v2 is assumes a NED coordinate system where x=North, "
248+
"y=East, z=+down. By changing to ENU there maybe some "
249+
"incorrect values for angles and derivative products of the "
250+
"impedance tensor."
251+
)
252+
253+
for mt_obj in self.values():
254+
mt_obj.coordinate_reference_frame = value
255+
256+
self._coordinate_reference_frame = value
257+
209258
@property
210259
def mt_list(self):
211260
"""Mt list.
@@ -609,7 +658,9 @@ def interpolate(
609658
)
610659

611660
else:
612-
mt_data.add_station(new_mt_obj, compute_relative_location=False)
661+
mt_data.add_station(
662+
new_mt_obj, compute_relative_location=False
663+
)
613664

614665
if not inplace:
615666
return mt_data
@@ -628,12 +679,14 @@ def rotate(self, rotation_angle, inplace=True):
628679
self.data_rotation_angle = rotation_angle
629680

630681
if not inplace:
631-
mt_data = self.clone_empty
682+
mt_data = self.clone_empty()
632683
for mt_obj in self.values():
633684
if not inplace:
634685
rot_mt_obj = mt_obj.copy()
635686
rot_mt_obj.rotation_angle = rotation_angle
636-
mt_data.add_station(rot_mt_obj, compute_relative_location=False)
687+
mt_data.add_station(
688+
rot_mt_obj, compute_relative_location=False
689+
)
637690
else:
638691
mt_obj.rotation_angle = rotation_angle
639692

@@ -727,8 +780,12 @@ def compute_model_errors(
727780
self.t_model_error.floor = t_floor
728781

729782
for mt_obj in self.values():
730-
mt_obj.compute_model_z_errors(**self.z_model_error.error_parameters)
731-
mt_obj.compute_model_t_errors(**self.t_model_error.error_parameters)
783+
mt_obj.compute_model_z_errors(
784+
**self.z_model_error.error_parameters
785+
)
786+
mt_obj.compute_model_t_errors(
787+
**self.t_model_error.error_parameters
788+
)
732789

733790
def get_nearby_stations(self, station_key, radius, radius_units="m"):
734791
"""Get stations close to a given station.

0 commit comments

Comments
 (0)