Skip to content

Commit a270c30

Browse files
authored
Merge pull request #5 from tbenthompson/projection_transform
EFCS <--> TDCS and projection transformations.
2 parents 4a198f9 + 0fe466f commit a270c30

File tree

6 files changed

+167
-3
lines changed

6 files changed

+167
-3
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
fail-fast: false
99
matrix:
1010
os: ["ubuntu-latest", "macos-latest"]
11-
python-version: ["3.6", "3.7", "3.8", "3.9"]
11+
python-version: ["3.8", "3.9"]
1212
name: Test (${{ matrix.python-version }}, ${{ matrix.os }})
1313
runs-on: ${{ matrix.os }}
1414
defaults:

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
*.swp
22
*.swo
3-
*.png
43
tags
54
__pycache__
65
.pytest_cache

cutde/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@
66
strain_all_pairs,
77
strain_to_stress,
88
)
9+
from .geometry import ( # noqa: F401
10+
compute_efcs_to_tdcs_rotations,
11+
compute_normal_vectors,
12+
compute_projection_transforms,
13+
)

cutde/geometry.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import numpy as np
2+
import pyproj
3+
4+
5+
def compute_normal_vectors(tri_pts) -> np.ndarray:
6+
"""
7+
Compute normal vectors for each triangle.
8+
9+
Parameters
10+
----------
11+
tri_pts : {array-like}, shape (n_triangles, 3, 3)
12+
The vertices of the triangles.
13+
14+
Returns
15+
-------
16+
normals : np.ndarray, shape (n_triangles, 3)
17+
The normal vectors for each triangle.
18+
"""
19+
leg1 = tri_pts[:, 1] - tri_pts[:, 0]
20+
leg2 = tri_pts[:, 2] - tri_pts[:, 0]
21+
# The normal vector is one axis of the TDCS and can be
22+
# computed as the cross product of the two corner-corner tangent vectors.
23+
Vnormal = np.cross(leg1, leg2, axis=1)
24+
# And it should be normalized to have unit length of course!
25+
Vnormal /= np.linalg.norm(Vnormal, axis=1)[:, None]
26+
return Vnormal
27+
28+
29+
def compute_projection_transforms(
30+
origins, transformer: pyproj.Transformer
31+
) -> np.ndarray:
32+
"""
33+
Convert vectors from one coordinate system to another. Unlike positions,
34+
this cannot be done with a simple pyproj call. We first need to set up a
35+
vector start and end point, convert those into the new coordinate system
36+
and then recompute the direction/distance between the start and end point.
37+
38+
The output matrices are not pure rotation matrices because there is also
39+
a scaling of vector lengths. For example, converting from latitude to
40+
meters will result in a large scale factor.
41+
42+
You can obtain the inverse transformation either by computing the inverse
43+
of the matrix or by passing an inverse pyproj.Transformer.
44+
45+
Parameters
46+
----------
47+
origins : {array-like}, shape (N, 3)
48+
The points at which we will compute rotation matrices
49+
transformer : pyproj.Transformer
50+
A pyproj.Transformer that will perform the necessary projection step.
51+
52+
Returns
53+
-------
54+
transform_mats : np.ndarray, shape (n_triangles, 3, 3)
55+
The 3x3 rotation and scaling matrices that transform vectors from the
56+
EFCS to TDCS.
57+
"""
58+
59+
out = np.empty((origins.shape[0], 3, 3), dtype=origins.dtype)
60+
for d in range(3):
61+
eps = 1.0
62+
targets = origins.copy()
63+
targets[:, d] += eps
64+
proj_origins = np.array(
65+
transformer.transform(origins[:, 0], origins[:, 1], origins[:, 2])
66+
).T.copy()
67+
proj_targets = np.array(
68+
transformer.transform(targets[:, 0], targets[:, 1], targets[:, 2])
69+
).T.copy()
70+
out[:, :, d] = proj_targets - proj_origins
71+
return out
72+
73+
74+
def compute_efcs_to_tdcs_rotations(tri_pts) -> np.ndarray:
75+
"""
76+
Build rotation matrices that convert from an Earth-fixed coordinate system
77+
(EFCS) to a triangular dislocation coordinate system (TDCS).
78+
79+
In the EFCS, the vectors will be directions/length in a map projection or
80+
an elliptical coordinate system.
81+
82+
In the TDCS, the coordinates/vectors will be separated into:
83+
`(along-strike-distance, along-dip-distance, tensile-distance)`
84+
85+
Note that in the Nikhoo and Walter 2015 and the Okada convention, the dip
86+
vector points upwards. This is different from the standard geologic
87+
convention where the dip vector points downwards.
88+
89+
It may be useful to extract normal, dip or strike vectors from the rotation
90+
matrices that are returned by this function. The strike vectors are:
91+
`rot_mats[:, 0, :]`, the dip vectors are `rot_mats[:, 1, :]` and the normal
92+
vectors are `rot_mats[:, 2, :]`.
93+
94+
To transform from TDCS back to EFCS, we simply need the transpose of the
95+
rotation matrices because the inverse of an orthogonal matrix is its
96+
transpose. To get this you can run `np.transpose(rot_mats, (0, 2, 1))`.
97+
98+
Parameters
99+
----------
100+
tri_pts : {array-like}, shape (n_triangles, 3, 3)
101+
The vertices of the triangles.
102+
103+
Returns
104+
-------
105+
rot_mats : np.ndarray, shape (n_triangles, 3, 3)
106+
The 3x3 rotation matrices that transform vectors from the EFCS to TDCS.
107+
"""
108+
Vnormal = compute_normal_vectors(tri_pts)
109+
eY = np.array([0, 1, 0])
110+
eZ = np.array([0, 0, 1])
111+
# The strike vector is defined as orthogonal to both the (0,0,1) vector and
112+
# the normal.
113+
Vstrike_raw = np.cross(eZ[None, :], Vnormal, axis=1)
114+
Vstrike_length = np.linalg.norm(Vstrike_raw, axis=1)
115+
116+
# If eZ == Vnormal, we will get Vstrike = (0,0,0). In this case, just set
117+
# Vstrike equal to (0,±1,0).
118+
Vstrike = np.where(
119+
Vstrike_length[:, None] == 0, eY[None, :] * Vnormal[:, 2, None], Vstrike_raw
120+
)
121+
Vstrike /= np.linalg.norm(Vstrike, axis=1)[:, None]
122+
Vdip = np.cross(Vnormal, Vstrike, axis=1)
123+
return np.transpose(np.array([Vstrike, Vdip, Vnormal]), (1, 0, 2))

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ dependencies:
55
- numpy
66
- scipy
77
- matplotlib
8-
- jinja2
8+
- pyproj
99
- black
1010
- flake8
1111
- isort

tests/test_geometry.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import numpy as np
2+
3+
from cutde import compute_efcs_to_tdcs_rotations, compute_projection_transforms
4+
5+
6+
def test_efcs_to_tdcs_orthogonal():
7+
tp = np.random.random_sample((1, 3, 3))
8+
R = compute_efcs_to_tdcs_rotations(tp)
9+
for d1 in range(3):
10+
for d2 in range(d1, 3):
11+
c = np.sum(R[:, d1, :] * R[:, d2, :], axis=1)
12+
true = 1 if d1 == d2 else 0
13+
np.testing.assert_almost_equal(c, true)
14+
15+
16+
def test_projection_transforms():
17+
from pyproj import Transformer
18+
19+
transformer = Transformer.from_crs(
20+
"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs",
21+
"+proj=geocent +datum=WGS84 +units=m +no_defs",
22+
)
23+
rand_pt = np.random.random_sample(3)
24+
pts = np.array([rand_pt, rand_pt + np.array([0, 0, -1000.0])])
25+
transforms = compute_projection_transforms(pts, transformer)
26+
27+
# The vertical component should transform without change in scale.
28+
np.testing.assert_almost_equal(np.linalg.norm(transforms[:, :, 2], axis=1), 1.0)
29+
30+
# The transformation should be identical for vertical motions
31+
np.testing.assert_allclose(
32+
transforms[0, :, [0, 1]], transforms[1, :, [0, 1]], rtol=1e-2
33+
)
34+
35+
# Check length of degree of latitude and longitude. Approximate.
36+
np.testing.assert_allclose(transforms[0, 1, 0], 1.11e5, rtol=5e-3)
37+
np.testing.assert_allclose(transforms[0, 2, 1], 1.11e5, rtol=5e-3)

0 commit comments

Comments
 (0)