|
| 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)) |
0 commit comments