LoopStructural 1.6.11__py3-none-any.whl → 1.6.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of LoopStructural might be problematic. Click here for more details.
- LoopStructural/datatypes/_bounding_box.py +2 -4
- LoopStructural/datatypes/_point.py +2 -2
- LoopStructural/interpolators/_finite_difference_interpolator.py +18 -3
- LoopStructural/interpolators/supports/_3d_base_structured.py +3 -3
- LoopStructural/interpolators/supports/_3d_structured_tetra.py +3 -3
- LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +3 -3
- LoopStructural/interpolators/supports/_face_table.py +3 -3
- LoopStructural/modelling/core/geological_model.py +7 -7
- LoopStructural/modelling/features/_base_geological_feature.py +1 -1
- LoopStructural/modelling/features/_lambda_geological_feature.py +67 -9
- LoopStructural/modelling/features/builders/_fault_builder.py +131 -144
- LoopStructural/modelling/features/fault/_fault_function.py +18 -24
- LoopStructural/modelling/features/fault/_fault_segment.py +12 -6
- LoopStructural/modelling/input/process_data.py +6 -6
- LoopStructural/utils/_surface.py +1 -1
- LoopStructural/utils/regions.py +45 -41
- LoopStructural/version.py +1 -1
- {loopstructural-1.6.11.dist-info → loopstructural-1.6.13.dist-info}/METADATA +1 -1
- {loopstructural-1.6.11.dist-info → loopstructural-1.6.13.dist-info}/RECORD +22 -22
- {loopstructural-1.6.11.dist-info → loopstructural-1.6.13.dist-info}/WHEEL +1 -1
- {loopstructural-1.6.11.dist-info → loopstructural-1.6.13.dist-info}/licenses/LICENSE +0 -0
- {loopstructural-1.6.11.dist-info → loopstructural-1.6.13.dist-info}/top_level.txt +0 -0
|
@@ -326,9 +326,7 @@ class BoundingBox:
|
|
|
326
326
|
if iy == -1:
|
|
327
327
|
return self.origin[ix]
|
|
328
328
|
|
|
329
|
-
return self.bb[
|
|
330
|
-
ix,
|
|
331
|
-
]
|
|
329
|
+
return self.bb[ix,]
|
|
332
330
|
|
|
333
331
|
def __getitem__(self, name):
|
|
334
332
|
if isinstance(name, str):
|
|
@@ -389,7 +387,7 @@ class BoundingBox:
|
|
|
389
387
|
|
|
390
388
|
if not local:
|
|
391
389
|
coordinates = [
|
|
392
|
-
np.linspace(self.global_origin[i], self.global_maximum[i], nsteps[i])
|
|
390
|
+
np.linspace(self.global_origin[i]+self.origin[i], self.global_maximum[i], nsteps[i])
|
|
393
391
|
for i in range(self.dimensions)
|
|
394
392
|
]
|
|
395
393
|
coordinate_grid = np.meshgrid(*coordinates, indexing="ij")
|
|
@@ -147,8 +147,8 @@ class VectorPoints:
|
|
|
147
147
|
else:
|
|
148
148
|
norm = np.linalg.norm(vectors, axis=1)
|
|
149
149
|
vectors[norm > 0, :] /= norm[norm > 0][:, None]
|
|
150
|
-
norm = norm[norm > 0] / norm[norm > 0].max()
|
|
151
|
-
vectors *= norm[
|
|
150
|
+
norm[norm > 0] = norm[norm > 0] / norm[norm > 0].max()
|
|
151
|
+
vectors[norm > 0, :] *= norm[norm > 0, None]
|
|
152
152
|
if scale_function is not None:
|
|
153
153
|
# vectors /= np.linalg.norm(vectors, axis=1)[:, None]
|
|
154
154
|
vectors *= scale_function(self.locations)[:, None]
|
|
@@ -272,7 +272,12 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
|
|
|
272
272
|
idc[inside, :] = gi[node_idx[inside, :]]
|
|
273
273
|
inside = np.logical_and(~np.any(idc == -1, axis=1), inside)
|
|
274
274
|
|
|
275
|
-
(
|
|
275
|
+
(
|
|
276
|
+
vertices,
|
|
277
|
+
T,
|
|
278
|
+
elements,
|
|
279
|
+
inside_,
|
|
280
|
+
) = self.support.get_element_gradient_for_location(
|
|
276
281
|
points[inside, : self.support.dimension]
|
|
277
282
|
)
|
|
278
283
|
# normalise constraint vector and scale element matrix by this
|
|
@@ -335,7 +340,12 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
|
|
|
335
340
|
# calculate unit vector for node gradients
|
|
336
341
|
# this means we are only constraining direction of grad not the
|
|
337
342
|
# magnitude
|
|
338
|
-
(
|
|
343
|
+
(
|
|
344
|
+
vertices,
|
|
345
|
+
T,
|
|
346
|
+
elements,
|
|
347
|
+
inside_,
|
|
348
|
+
) = self.support.get_element_gradient_for_location(
|
|
339
349
|
points[inside, : self.support.dimension]
|
|
340
350
|
)
|
|
341
351
|
# T*=np.product(self.support.step_vector)
|
|
@@ -422,7 +432,12 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
|
|
|
422
432
|
vectors[norm > 0, :] /= norm[norm > 0, None]
|
|
423
433
|
|
|
424
434
|
# normalise element vector to unit vector for dot product
|
|
425
|
-
(
|
|
435
|
+
(
|
|
436
|
+
vertices,
|
|
437
|
+
T,
|
|
438
|
+
elements,
|
|
439
|
+
inside_,
|
|
440
|
+
) = self.support.get_element_gradient_for_location(
|
|
426
441
|
points[inside, : self.support.dimension]
|
|
427
442
|
)
|
|
428
443
|
T[norm > 0, :, :] /= norm[norm > 0, None, None]
|
|
@@ -162,9 +162,9 @@ class BaseStructuredSupport(BaseSupport):
|
|
|
162
162
|
length = self.maximum - origin
|
|
163
163
|
length /= self.step_vector
|
|
164
164
|
self._nsteps = np.ceil(length).astype(np.int64)
|
|
165
|
-
self._nsteps[
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
self._nsteps[self._nsteps == 0] = (
|
|
166
|
+
3 # need to have a minimum of 3 elements to apply the finite difference mask
|
|
167
|
+
)
|
|
168
168
|
if np.any(~(self._nsteps > 0)):
|
|
169
169
|
logger.error(
|
|
170
170
|
f"Cannot resize the interpolation support. The proposed number of steps is {self._nsteps}, these must be all > 0"
|
|
@@ -166,9 +166,9 @@ class TetMesh(BaseStructuredSupport):
|
|
|
166
166
|
shared_face_index[:] = -1
|
|
167
167
|
shared_face_index[row.reshape(-1, 3)[:, 0], :] = col.reshape(-1, 3)
|
|
168
168
|
|
|
169
|
-
self.shared_elements[
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
self.shared_elements[np.arange(self.shared_element_relationships.shape[0]), :] = (
|
|
170
|
+
shared_face_index
|
|
171
|
+
)
|
|
172
172
|
# resize
|
|
173
173
|
self.shared_elements = self.shared_elements[: len(self.shared_element_relationships), :]
|
|
174
174
|
|
|
@@ -173,9 +173,9 @@ class UnStructuredTetMesh(BaseSupport):
|
|
|
173
173
|
shared_face_index[:] = -1
|
|
174
174
|
shared_face_index[row.reshape(-1, 3)[:, 0], :] = col.reshape(-1, 3)
|
|
175
175
|
|
|
176
|
-
self.shared_elements[
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
self.shared_elements[np.arange(self.shared_element_relationships.shape[0]), :] = (
|
|
177
|
+
shared_face_index
|
|
178
|
+
)
|
|
179
179
|
# resize
|
|
180
180
|
self.shared_elements = self.shared_elements[: len(self.shared_element_relationships), :]
|
|
181
181
|
# flag = np.zeros(self.elements.shape[0])
|
|
@@ -63,8 +63,8 @@ def _init_face_table(grid):
|
|
|
63
63
|
shared_face_index = np.zeros((shared_faces.shape[0], grid.dimension), dtype=int)
|
|
64
64
|
shared_face_index[:] = -1
|
|
65
65
|
shared_face_index[row.reshape(-1, grid.dimension)[:, 0], :] = col.reshape(-1, grid.dimension)
|
|
66
|
-
grid._shared_elements[
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
grid._shared_elements[np.arange(grid.shared_element_relationships.shape[0]), :] = (
|
|
67
|
+
shared_face_index
|
|
68
|
+
)
|
|
69
69
|
# resize
|
|
70
70
|
grid._shared_elements = grid.shared_elements[: len(grid.shared_element_relationships), :]
|
|
@@ -598,12 +598,10 @@ class GeologicalModel:
|
|
|
598
598
|
* self._data.loc[mask, "polarity"].to_numpy()[:, None]
|
|
599
599
|
)
|
|
600
600
|
self._data.drop(["strike", "dip"], axis=1, inplace=True)
|
|
601
|
-
self._data[
|
|
602
|
-
[
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
].astype(
|
|
606
|
-
float
|
|
601
|
+
self._data[['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']] = (
|
|
602
|
+
self._data[
|
|
603
|
+
['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']
|
|
604
|
+
].astype(float)
|
|
607
605
|
)
|
|
608
606
|
|
|
609
607
|
def set_model_data(self, data):
|
|
@@ -1805,7 +1803,9 @@ class GeologicalModel:
|
|
|
1805
1803
|
values.to_list(),
|
|
1806
1804
|
self.bounding_box,
|
|
1807
1805
|
name=names.loc[values.index].to_list(),
|
|
1808
|
-
colours=unit_table.loc[unit_table['feature_name'] == u, 'colour'].tolist()[
|
|
1806
|
+
colours=unit_table.loc[unit_table['feature_name'] == u, 'colour'].tolist()[
|
|
1807
|
+
1:
|
|
1808
|
+
], # we don't isosurface basement, no value
|
|
1809
1809
|
)
|
|
1810
1810
|
)
|
|
1811
1811
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Geological features
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
from LoopStructural.utils.maths import regular_tetraherdron_for_points, gradient_from_tetrahedron
|
|
5
5
|
from ...modelling.features import BaseFeature
|
|
6
6
|
from ...utils import getLogger
|
|
7
7
|
from ...modelling.features import FeatureType
|
|
8
8
|
import numpy as np
|
|
9
9
|
from typing import Callable, Optional
|
|
10
|
+
from ...utils import LoopValueError
|
|
10
11
|
|
|
11
12
|
logger = getLogger(__name__)
|
|
12
13
|
|
|
@@ -18,8 +19,8 @@ class LambdaGeologicalFeature(BaseFeature):
|
|
|
18
19
|
name: str = "unnamed_lambda",
|
|
19
20
|
gradient_function: Optional[Callable[[np.ndarray], np.ndarray]] = None,
|
|
20
21
|
model=None,
|
|
21
|
-
regions: list =
|
|
22
|
-
faults: list =
|
|
22
|
+
regions: Optional[list] = None,
|
|
23
|
+
faults: Optional[list] = None,
|
|
23
24
|
builder=None,
|
|
24
25
|
):
|
|
25
26
|
"""A lambda geological feature is a wrapper for a geological
|
|
@@ -43,10 +44,11 @@ class LambdaGeologicalFeature(BaseFeature):
|
|
|
43
44
|
builder : _type_, optional
|
|
44
45
|
_description_, by default None
|
|
45
46
|
"""
|
|
46
|
-
BaseFeature.__init__(self, name, model, faults, regions, builder)
|
|
47
|
+
BaseFeature.__init__(self, name, model, faults if faults is not None else [], regions if regions is not None else [], builder)
|
|
47
48
|
self.type = FeatureType.LAMBDA
|
|
48
49
|
self.function = function
|
|
49
50
|
self.gradient_function = gradient_function
|
|
51
|
+
self.regions = regions if regions is not None else []
|
|
50
52
|
|
|
51
53
|
def evaluate_value(self, pos: np.ndarray, ignore_regions=False) -> np.ndarray:
|
|
52
54
|
"""_summary_
|
|
@@ -62,13 +64,16 @@ class LambdaGeologicalFeature(BaseFeature):
|
|
|
62
64
|
_description_
|
|
63
65
|
"""
|
|
64
66
|
v = np.zeros((pos.shape[0]))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
v[:] = np.nan
|
|
68
|
+
|
|
69
|
+
mask = self._calculate_mask(pos, ignore_regions=ignore_regions)
|
|
70
|
+
pos = self._apply_faults(pos)
|
|
71
|
+
if self.function is not None:
|
|
72
|
+
|
|
73
|
+
v[mask] = self.function(pos[mask,:])
|
|
69
74
|
return v
|
|
70
75
|
|
|
71
|
-
def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False) -> np.ndarray:
|
|
76
|
+
def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False,element_scale_parameter=None) -> np.ndarray:
|
|
72
77
|
"""_summary_
|
|
73
78
|
|
|
74
79
|
Parameters
|
|
@@ -81,7 +86,60 @@ class LambdaGeologicalFeature(BaseFeature):
|
|
|
81
86
|
np.ndarray
|
|
82
87
|
_description_
|
|
83
88
|
"""
|
|
89
|
+
if pos.shape[1] != 3:
|
|
90
|
+
raise LoopValueError("Need Nx3 array of xyz points to evaluate gradient")
|
|
91
|
+
logger.info(f'Calculating gradient for {self.name}')
|
|
92
|
+
if element_scale_parameter is None:
|
|
93
|
+
if self.model is not None:
|
|
94
|
+
element_scale_parameter = np.min(self.model.bounding_box.step_vector) / 10
|
|
95
|
+
else:
|
|
96
|
+
element_scale_parameter = 1
|
|
97
|
+
else:
|
|
98
|
+
try:
|
|
99
|
+
element_scale_parameter = float(element_scale_parameter)
|
|
100
|
+
except ValueError:
|
|
101
|
+
logger.error("element_scale_parameter must be a float")
|
|
102
|
+
element_scale_parameter = 1
|
|
84
103
|
v = np.zeros((pos.shape[0], 3))
|
|
104
|
+
v = np.zeros(pos.shape)
|
|
105
|
+
v[:] = np.nan
|
|
106
|
+
mask = self._calculate_mask(pos, ignore_regions=ignore_regions)
|
|
107
|
+
# evaluate the faults on the nodes of the faulted feature support
|
|
108
|
+
# then evaluate the gradient at these points
|
|
109
|
+
if len(self.faults) > 0:
|
|
110
|
+
# generate a regular tetrahedron for each point
|
|
111
|
+
# we will then move these points by the fault and then recalculate the gradient.
|
|
112
|
+
# this should work...
|
|
113
|
+
resolved = False
|
|
114
|
+
tetrahedron = regular_tetraherdron_for_points(pos, element_scale_parameter)
|
|
115
|
+
|
|
116
|
+
while resolved:
|
|
117
|
+
for f in self.faults:
|
|
118
|
+
v = (
|
|
119
|
+
f[0]
|
|
120
|
+
.evaluate_value(tetrahedron.reshape(-1, 3), fillnan='nearest')
|
|
121
|
+
.reshape(tetrahedron.shape[0], 4)
|
|
122
|
+
)
|
|
123
|
+
flag = np.logical_or(np.all(v > 0, axis=1), np.all(v < 0, axis=1))
|
|
124
|
+
if np.any(~flag):
|
|
125
|
+
logger.warning(
|
|
126
|
+
f"Points are too close to fault {f[0].name}. Refining the tetrahedron"
|
|
127
|
+
)
|
|
128
|
+
element_scale_parameter *= 0.5
|
|
129
|
+
tetrahedron = regular_tetraherdron_for_points(pos, element_scale_parameter)
|
|
130
|
+
|
|
131
|
+
resolved = True
|
|
132
|
+
|
|
133
|
+
tetrahedron_faulted = self._apply_faults(np.array(tetrahedron.reshape(-1, 3))).reshape(
|
|
134
|
+
tetrahedron.shape
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
values = self.function(tetrahedron_faulted.reshape(-1, 3)).reshape(
|
|
138
|
+
(-1, 4)
|
|
139
|
+
)
|
|
140
|
+
v[mask, :] = gradient_from_tetrahedron(tetrahedron[mask, :, :], values[mask])
|
|
141
|
+
|
|
142
|
+
return v
|
|
85
143
|
if self.gradient_function is None:
|
|
86
144
|
v[:, :] = np.nan
|
|
87
145
|
else:
|
|
@@ -3,7 +3,6 @@ from typing import Union
|
|
|
3
3
|
from LoopStructural.utils.maths import rotation
|
|
4
4
|
from ._structural_frame_builder import StructuralFrameBuilder
|
|
5
5
|
from .. import AnalyticalGeologicalFeature
|
|
6
|
-
from LoopStructural.utils import get_vectors
|
|
7
6
|
import numpy as np
|
|
8
7
|
import pandas as pd
|
|
9
8
|
from ....utils import getLogger
|
|
@@ -52,8 +51,11 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
52
51
|
self.frame.model = model
|
|
53
52
|
self.origin = np.array([np.nan, np.nan, np.nan])
|
|
54
53
|
self.maximum = np.array([np.nan, np.nan, np.nan]) # self.model.bounding_box[1, :]
|
|
54
|
+
|
|
55
|
+
if bounding_box is None:
|
|
56
|
+
raise ValueError("BoundingBox cannot be None")
|
|
57
|
+
|
|
55
58
|
# define a maximum area to mesh adding buffer to model
|
|
56
|
-
# buffer = .2
|
|
57
59
|
self.minimum_origin = bounding_box.with_buffer(fault_bounding_box_buffer).origin
|
|
58
60
|
self.maximum_maximum = bounding_box.with_buffer(fault_bounding_box_buffer).maximum
|
|
59
61
|
|
|
@@ -66,8 +68,23 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
66
68
|
self.fault_centre = None
|
|
67
69
|
|
|
68
70
|
def update_geometry(self, points):
|
|
71
|
+
"""
|
|
72
|
+
Update the geometry of the fault by adjusting the origin and maximum bounds
|
|
73
|
+
based on the provided points.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
points : numpy.ndarray
|
|
78
|
+
Array of points used to update the fault geometry.
|
|
79
|
+
"""
|
|
80
|
+
if self.origin is None or self.maximum is None:
|
|
81
|
+
raise ValueError("Origin and maximum must be initialized before updating geometry.")
|
|
82
|
+
|
|
69
83
|
self.origin = np.nanmin(np.array([np.min(points, axis=0), self.origin]), axis=0)
|
|
70
84
|
self.maximum = np.nanmax(np.array([np.max(points, axis=0), self.maximum]), axis=0)
|
|
85
|
+
# add a small buffer 10% of current length to the origin and maximum
|
|
86
|
+
self.origin = self.origin - 0.1 * (self.maximum - self.origin)
|
|
87
|
+
self.maximum = self.maximum + 0.1 * (self.maximum - self.origin)
|
|
71
88
|
self.origin[self.origin < self.minimum_origin] = self.minimum_origin[
|
|
72
89
|
self.origin < self.minimum_origin
|
|
73
90
|
]
|
|
@@ -93,144 +110,109 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
93
110
|
fault_dip_anisotropy=1.0,
|
|
94
111
|
fault_pitch=None,
|
|
95
112
|
):
|
|
96
|
-
"""
|
|
97
|
-
specified parameters
|
|
113
|
+
"""
|
|
114
|
+
Generate the required data for building a fault frame with the specified parameters.
|
|
98
115
|
|
|
99
116
|
Parameters
|
|
100
117
|
----------
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
fault_center :
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
fault
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
fault_frame_data : pandas.DataFrame
|
|
119
|
+
DataFrame containing fault frame data.
|
|
120
|
+
fault_center : array-like, optional
|
|
121
|
+
Coordinates of the fault center.
|
|
122
|
+
fault_normal_vector : array-like, optional
|
|
123
|
+
Normal vector of the fault.
|
|
124
|
+
fault_slip_vector : array-like, optional
|
|
125
|
+
Slip vector of the fault.
|
|
126
|
+
minor_axis : float, optional
|
|
127
|
+
Minor axis length of the fault.
|
|
128
|
+
major_axis : float, optional
|
|
129
|
+
Major axis length of the fault.
|
|
130
|
+
intermediate_axis : float, optional
|
|
131
|
+
Intermediate axis length of the fault.
|
|
132
|
+
w : float, default=1.0
|
|
133
|
+
Weighting factor for the fault data.
|
|
134
|
+
points : bool, default=False
|
|
135
|
+
Whether to include points in the fault data.
|
|
136
|
+
force_mesh_geometry : bool, default=False
|
|
137
|
+
Whether to force the use of mesh geometry.
|
|
138
|
+
fault_buffer : float, default=0.2
|
|
139
|
+
Buffer size around the fault.
|
|
140
|
+
fault_trace_anisotropy : float, default=1.0
|
|
141
|
+
Anisotropy factor for the fault trace.
|
|
142
|
+
fault_dip : float, default=90
|
|
143
|
+
Dip angle of the fault in degrees.
|
|
144
|
+
fault_dip_anisotropy : float, default=1.0
|
|
145
|
+
Anisotropy factor for the fault dip.
|
|
146
|
+
fault_pitch : float, optional
|
|
147
|
+
Pitch angle of the fault.
|
|
117
148
|
"""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
raise ValueError("There are no points on the fault trace")
|
|
123
|
-
# find the middle point on the fault trace if center is not provided
|
|
124
|
-
if fault_center is None:
|
|
125
|
-
trace_mask = np.logical_and(
|
|
126
|
-
fault_frame_data["coord"] == 0, fault_frame_data["val"] == 0
|
|
127
|
-
)
|
|
128
|
-
fault_center = fault_frame_data.loc[trace_mask, ["X", "Y", "Z"]].mean(axis=0).to_numpy()
|
|
129
|
-
dist = np.linalg.norm(
|
|
130
|
-
fault_center - fault_frame_data.loc[trace_mask, ["X", "Y", "Z"]].to_numpy(), axis=1
|
|
131
|
-
)
|
|
132
|
-
# make the nan points greater than the max dist 10 is arbitrary and doesn't matter
|
|
133
|
-
dist[np.isnan(dist)] = np.nanmax(dist) + 10
|
|
134
|
-
fault_center = fault_frame_data.loc[trace_mask, ["X", "Y", "Z"]].to_numpy()[
|
|
135
|
-
np.argmin(dist), :
|
|
136
|
-
]
|
|
137
|
-
# get all of the gradient data associated with the fault trace
|
|
149
|
+
fault_trace = fault_frame_data.loc[
|
|
150
|
+
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["val"] == 0),
|
|
151
|
+
["X", "Y"],
|
|
152
|
+
].to_numpy()
|
|
138
153
|
if fault_normal_vector is None:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
vector_data = np.vstack(
|
|
147
|
-
[
|
|
148
|
-
vector_data,
|
|
149
|
-
fault_frame_data.loc[normal_mask, ["nx", "ny", "nz"]].to_numpy(),
|
|
150
|
-
]
|
|
151
|
-
)
|
|
154
|
+
if fault_frame_data.loc[
|
|
155
|
+
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna())].shape[0]>0:
|
|
156
|
+
fault_normal_vector = fault_frame_data.loc[
|
|
157
|
+
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna()),
|
|
158
|
+
["nx", "ny", "nz"],
|
|
159
|
+
].to_numpy().mean(axis=0)
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
logger.warning(
|
|
155
|
-
f"No orientation data for fault\n\
|
|
156
|
-
Defaulting to a dip of {fault_dip}vertical fault"
|
|
157
|
-
)
|
|
158
|
-
# if the line is long enough, estimate the normal vector
|
|
159
|
-
# by finding the centre point of the line and calculating the tangnent
|
|
160
|
-
# of the two points
|
|
161
|
-
if fault_frame_data.loc[trace_mask, :].shape[0] > 3:
|
|
162
|
-
|
|
163
|
-
pts = fault_frame_data.loc[trace_mask, ["X", "Y", "Z"]].to_numpy()
|
|
164
|
-
dist = np.abs(np.linalg.norm(fault_center - pts, axis=1))
|
|
165
|
-
# any nans just make them max distance + a bit
|
|
166
|
-
dist[np.isnan(dist)] = np.nanmax(dist) + 10
|
|
167
|
-
# idx = np.argsort(dist)
|
|
168
|
-
# direction_vector = pts[idx[-1]] - pts[idx[-2]]
|
|
169
|
-
# coefficients = np.polyfit(
|
|
170
|
-
# fault_frame_data.loc[trace_mask, "X"],
|
|
171
|
-
# fault_frame_data.loc[trace_mask, "Y"],
|
|
172
|
-
# 1,
|
|
173
|
-
# )
|
|
174
|
-
# slope, intercept = coefficients
|
|
175
|
-
slope, intercept = np.polyfit(
|
|
176
|
-
pts[dist < 0.25 * np.nanmax(dist), 0],
|
|
177
|
-
pts[dist < 0.25 * np.nanmax(dist), 1],
|
|
178
|
-
1,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
# # Create a direction vector using the slope
|
|
182
|
-
direction_vector = np.array([1, slope, 0])
|
|
183
|
-
direction_vector /= np.linalg.norm(direction_vector)
|
|
184
|
-
rotation_matrix = rotation(direction_vector[None, :], [90 - fault_dip])
|
|
185
|
-
vector_data = np.array(
|
|
186
|
-
[
|
|
187
|
-
[
|
|
188
|
-
direction_vector[1],
|
|
189
|
-
-direction_vector[0],
|
|
190
|
-
0,
|
|
191
|
-
]
|
|
192
|
-
]
|
|
193
|
-
)
|
|
194
|
-
vector_data /= np.linalg.norm(vector_data, axis=1)
|
|
195
|
-
vector_data = np.einsum("ijk,ik->ij", rotation_matrix, vector_data)
|
|
196
|
-
|
|
197
|
-
vector_data /= np.linalg.norm(vector_data, axis=1)
|
|
198
|
-
fault_normal_vector = np.mean(vector_data, axis=0)
|
|
161
|
+
else:
|
|
199
162
|
|
|
200
|
-
|
|
163
|
+
# Calculate fault strike using eigenvectors
|
|
164
|
+
pts = fault_trace - fault_trace.mean(axis=0)
|
|
165
|
+
# Calculate covariance matrix
|
|
166
|
+
cov_matrix = pts.T @ pts
|
|
167
|
+
# Get eigenvectors and eigenvalues
|
|
168
|
+
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
|
|
169
|
+
# Use eigenvector with largest eigenvalue as strike direction
|
|
170
|
+
strike_vector = eigenvectors[:, np.argmax(eigenvalues)]
|
|
171
|
+
strike_vector = np.append(strike_vector, 0) # Add z component
|
|
172
|
+
strike_vector /= np.linalg.norm(strike_vector)
|
|
173
|
+
|
|
174
|
+
fault_normal_vector = np.cross(strike_vector, [0, 0, 1])
|
|
175
|
+
# Rotate the fault normal vector according to the fault dip
|
|
176
|
+
rotation_matrix = rotation(strike_vector[None, :], np.array([90 - fault_dip]))
|
|
177
|
+
fault_normal_vector = np.einsum("ijk,ik->ij", rotation_matrix, fault_normal_vector[None, :])[0]
|
|
178
|
+
|
|
179
|
+
if not isinstance(fault_normal_vector, np.ndarray):
|
|
180
|
+
fault_normal_vector = np.array(fault_normal_vector)
|
|
181
|
+
|
|
182
|
+
if fault_pitch is not None:
|
|
183
|
+
rotation_matrix = rotation(fault_normal_vector[None, :], np.array([fault_pitch]))
|
|
184
|
+
fault_slip_vector = np.einsum("ijk,ik->ij", rotation_matrix, fault_normal_vector[None, :])[0]
|
|
201
185
|
|
|
202
|
-
# estimate the fault slip vector
|
|
203
186
|
if fault_slip_vector is None:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
logger.warning(
|
|
211
|
-
"There is no slip vector data for the fault, using vertical slip vector\n\
|
|
212
|
-
projected onto fault surface estimating from fault normal"
|
|
213
|
-
)
|
|
214
|
-
strike_vector, dip_vector = get_vectors(fault_normal_vector[None, :])
|
|
215
|
-
fault_slip_vector = dip_vector[:, 0]
|
|
216
|
-
if fault_pitch is not None:
|
|
217
|
-
print('using pitch')
|
|
218
|
-
rotm = rotation(fault_normal_vector[None,:],[fault_pitch])
|
|
219
|
-
print(rotm.shape,fault_slip_vector.shape)
|
|
220
|
-
fault_slip_vector = np.einsum("ijk,k->ij", rotm, fault_slip_vector)[0,:]
|
|
221
|
-
logger.info(f"Estimated fault slip vector: {fault_slip_vector}")
|
|
222
|
-
else:
|
|
223
|
-
fault_slip_vector = fault_slip_data.mean(axis=0).to_numpy()
|
|
187
|
+
if fault_frame_data.loc[
|
|
188
|
+
np.logical_and(fault_frame_data["coord"] == 1, fault_frame_data["nx"].notna())].shape[0]>0:
|
|
189
|
+
fault_slip_vector = fault_frame_data.loc[
|
|
190
|
+
np.logical_and(fault_frame_data["coord"] == 1, fault_frame_data["nx"].notna()),
|
|
191
|
+
["nx", "ny", "nz"],
|
|
192
|
+
].to_numpy().mean(axis=0)
|
|
224
193
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
194
|
+
else:
|
|
195
|
+
fault_slip_vector = np.cross(fault_normal_vector, [1., 0., 0.])
|
|
196
|
+
if np.linalg.norm(fault_slip_vector) == 0:
|
|
197
|
+
fault_slip_vector = np.cross(fault_normal_vector, [0., 1., 0.])
|
|
198
|
+
fault_slip_vector /= np.linalg.norm(fault_slip_vector)
|
|
199
|
+
if fault_center is None:
|
|
230
200
|
fault_trace = fault_frame_data.loc[
|
|
231
201
|
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["val"] == 0),
|
|
232
202
|
["X", "Y"],
|
|
233
203
|
].to_numpy()
|
|
204
|
+
fault_center = fault_trace.mean(axis=0)
|
|
205
|
+
fault_center = np.array([fault_center[0], fault_center[1], 0.0])
|
|
206
|
+
if not isinstance(fault_center, np.ndarray):
|
|
207
|
+
fault_center = np.array(fault_center)
|
|
208
|
+
if fault_center.shape[0] != 3:
|
|
209
|
+
raise ValueError("fault_center must be a 3 element array")
|
|
210
|
+
self.fault_normal_vector = fault_normal_vector / np.linalg.norm(fault_normal_vector)
|
|
211
|
+
self.fault_slip_vector = fault_slip_vector / np.linalg.norm(fault_slip_vector)
|
|
212
|
+
|
|
213
|
+
self.fault_centre = fault_center
|
|
214
|
+
if major_axis is None:
|
|
215
|
+
|
|
234
216
|
distance = np.linalg.norm(fault_trace[:, None, :] - fault_trace[None, :, :], axis=2)
|
|
235
217
|
if len(distance) == 0 or np.sum(distance) == 0:
|
|
236
218
|
logger.warning("There is no fault trace for {}".format(self.name))
|
|
@@ -382,6 +364,7 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
382
364
|
0,
|
|
383
365
|
w,
|
|
384
366
|
]
|
|
367
|
+
|
|
385
368
|
if major_axis is not None:
|
|
386
369
|
fault_tips[0, :] = fault_center[:3] + strike_vector * 0.5 * major_axis
|
|
387
370
|
fault_tips[1, :] = fault_center[:3] - strike_vector * 0.5 * major_axis
|
|
@@ -503,16 +486,14 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
503
486
|
self.origin - length * buffer, self.maximum + length * buffer, rotation
|
|
504
487
|
)
|
|
505
488
|
|
|
506
|
-
def add_splay(self, splay,
|
|
507
|
-
if
|
|
489
|
+
def add_splay(self, splay, splay_region=None):
|
|
490
|
+
if splay_region is None:
|
|
508
491
|
|
|
509
|
-
def
|
|
492
|
+
def default_splay_region(xyz):
|
|
510
493
|
pts = (
|
|
511
|
-
self.builders[0].data[
|
|
494
|
+
self.builders[0].data["X", "Y", "Z", "val"].to_numpy()
|
|
512
495
|
) # get_value_constraints()
|
|
513
496
|
pts = pts[pts[:, 3] == 0, :]
|
|
514
|
-
# check whether the fault is on the hanging wall or footwall of splay fault
|
|
515
|
-
|
|
516
497
|
ext_field = splay[2].evaluate_value(pts[:, :3])
|
|
517
498
|
surf_field = splay[0].evaluate_value(pts[:, :3])
|
|
518
499
|
intersection_value = ext_field[np.nanargmin(np.abs(surf_field))]
|
|
@@ -531,19 +512,24 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
531
512
|
)
|
|
532
513
|
return mask
|
|
533
514
|
|
|
515
|
+
splay_region = default_splay_region
|
|
516
|
+
|
|
534
517
|
scalefactor = splay.fault_major_axis / self.fault_major_axis
|
|
535
|
-
self.builders[0].add_equality_constraints(splay,
|
|
536
|
-
return
|
|
518
|
+
self.builders[0].add_equality_constraints(splay, splay_region, scalefactor)
|
|
519
|
+
return splay_region
|
|
537
520
|
|
|
538
521
|
def add_fault_trace_anisotropy(self, w: float = 1.0):
|
|
539
|
-
"""
|
|
522
|
+
"""
|
|
523
|
+
Add fault trace anisotropy to the model.
|
|
540
524
|
|
|
541
525
|
Parameters
|
|
542
526
|
----------
|
|
543
527
|
w : float, optional
|
|
544
|
-
|
|
528
|
+
Weighting factor for the anisotropy, by default 1.0
|
|
545
529
|
"""
|
|
546
530
|
if w > 0:
|
|
531
|
+
if self.fault_normal_vector is None:
|
|
532
|
+
raise ValueError("fault_normal_vector must be initialized before adding anisotropy.")
|
|
547
533
|
|
|
548
534
|
plane = np.array([0, 0, 1])
|
|
549
535
|
strike_vector = (
|
|
@@ -553,24 +539,25 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
553
539
|
strike_vector = np.array([strike_vector[1], -strike_vector[0], 0])
|
|
554
540
|
|
|
555
541
|
anisotropy_feature = AnalyticalGeologicalFeature(
|
|
556
|
-
vector=strike_vector, origin=[0, 0, 0], name="fault_trace_anisotropy"
|
|
542
|
+
vector=strike_vector, origin=np.array([0, 0, 0]), name="fault_trace_anisotropy"
|
|
557
543
|
)
|
|
558
|
-
# print('adding fault trace anisotropy')
|
|
559
544
|
self.builders[0].add_orthogonal_feature(
|
|
560
545
|
anisotropy_feature, w=w, region=None, step=1, B=0
|
|
561
546
|
)
|
|
562
547
|
|
|
563
548
|
def add_fault_dip_anisotropy(self, w: float = 1.0):
|
|
564
|
-
"""
|
|
549
|
+
"""
|
|
550
|
+
Add fault dip anisotropy to the model.
|
|
565
551
|
|
|
566
552
|
Parameters
|
|
567
553
|
----------
|
|
568
|
-
dip : np.ndarray
|
|
569
|
-
_description_
|
|
570
554
|
w : float, optional
|
|
571
|
-
|
|
555
|
+
Weighting factor for the anisotropy, by default 1.0
|
|
572
556
|
"""
|
|
573
557
|
if w > 0:
|
|
558
|
+
if self.fault_normal_vector is None:
|
|
559
|
+
raise ValueError("fault_normal_vector must be initialized before adding anisotropy.")
|
|
560
|
+
|
|
574
561
|
plane = np.array([0, 0, 1])
|
|
575
562
|
strike_vector = (
|
|
576
563
|
self.fault_normal_vector - np.dot(self.fault_normal_vector, plane) * plane
|
|
@@ -579,11 +566,11 @@ class FaultBuilder(StructuralFrameBuilder):
|
|
|
579
566
|
strike_vector = np.array([strike_vector[1], -strike_vector[0], 0])
|
|
580
567
|
|
|
581
568
|
dip_vector = np.cross(strike_vector, self.fault_normal_vector)
|
|
569
|
+
dip_vector /= np.linalg.norm(dip_vector)
|
|
582
570
|
|
|
583
571
|
anisotropy_feature = AnalyticalGeologicalFeature(
|
|
584
|
-
vector=dip_vector, origin=[0, 0, 0], name="fault_dip_anisotropy"
|
|
572
|
+
vector=dip_vector, origin=np.array([0, 0, 0]), name="fault_dip_anisotropy"
|
|
585
573
|
)
|
|
586
|
-
# print(f'adding fault dip anisotropy {anisotropy_feature.name}')
|
|
587
574
|
self.builders[0].add_orthogonal_feature(
|
|
588
575
|
anisotropy_feature, w=w, region=None, step=1, B=0
|
|
589
576
|
)
|
|
@@ -9,6 +9,12 @@ from ....utils import getLogger
|
|
|
9
9
|
logger = getLogger(__name__)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def smooth_peak(x):
|
|
13
|
+
v = np.zeros(x.shape)
|
|
14
|
+
mask = np.logical_and(x >= -1, x <= 1)
|
|
15
|
+
v[mask] = 0.25 * x[mask] ** 2 + 0.5 * x[mask] ** 4 - 1.75 * x[mask] ** 2 + 1
|
|
16
|
+
return v
|
|
17
|
+
|
|
12
18
|
class FaultProfileFunction(metaclass=ABCMeta):
|
|
13
19
|
def __init__(self):
|
|
14
20
|
self.lim = [-1, 1]
|
|
@@ -412,14 +418,7 @@ class BaseFault(object):
|
|
|
412
418
|
# gyf.add_min(-1)
|
|
413
419
|
# gyf.add_max(1)
|
|
414
420
|
gyf = Ones()
|
|
415
|
-
gzf =
|
|
416
|
-
gzf.add_cstr(-1, 0)
|
|
417
|
-
gzf.add_cstr(1, 0)
|
|
418
|
-
gzf.add_cstr(-0.2, 1)
|
|
419
|
-
gzf.add_cstr(0.2, 1)
|
|
420
|
-
gzf.add_grad(0, 0)
|
|
421
|
-
gzf.add_min(-1)
|
|
422
|
-
gzf.add_max(1)
|
|
421
|
+
gzf = smooth_peak
|
|
423
422
|
gxf = Composite(hw, fw)
|
|
424
423
|
fault_displacement = FaultDisplacement(gx=gxf, gy=gyf, gz=gzf)
|
|
425
424
|
|
|
@@ -441,22 +440,17 @@ class BaseFault3D(object):
|
|
|
441
440
|
fw.add_cstr(-1, 0)
|
|
442
441
|
fw.add_grad(-1, 0)
|
|
443
442
|
fw.add_min(-1)
|
|
444
|
-
|
|
445
|
-
gyf
|
|
446
|
-
|
|
447
|
-
gyf.add_cstr(-
|
|
448
|
-
gyf.add_cstr(
|
|
449
|
-
gyf.
|
|
450
|
-
gyf.
|
|
451
|
-
gyf.
|
|
443
|
+
|
|
444
|
+
gyf = smooth_peak
|
|
445
|
+
# CubicFunction()
|
|
446
|
+
# gyf.add_cstr(-1, 0)
|
|
447
|
+
# gyf.add_cstr(1, 0)
|
|
448
|
+
# gyf.add_cstr(-0.2, 1)
|
|
449
|
+
# gyf.add_cstr(0.2, 1)
|
|
450
|
+
# gyf.add_grad(0, 0)
|
|
451
|
+
# gyf.add_min(-1)
|
|
452
|
+
# gyf.add_max(1)
|
|
452
453
|
# gyf = Ones()
|
|
453
|
-
gzf =
|
|
454
|
-
gzf.add_cstr(-1, 0)
|
|
455
|
-
gzf.add_cstr(1, 0)
|
|
456
|
-
gzf.add_cstr(-0.2, 1)
|
|
457
|
-
gzf.add_cstr(0.2, 1)
|
|
458
|
-
gzf.add_grad(0, 0)
|
|
459
|
-
gzf.add_min(-1)
|
|
460
|
-
gzf.add_max(1)
|
|
454
|
+
gzf = smooth_peak
|
|
461
455
|
gxf = Composite(hw, fw)
|
|
462
456
|
fault_displacement = FaultDisplacement(gx=gxf, gy=gyf, gz=gzf)
|
|
@@ -125,11 +125,17 @@ class FaultSegment(StructuralFrame):
|
|
|
125
125
|
try:
|
|
126
126
|
import pyvista as pv
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
self.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
if self.model is None:
|
|
129
|
+
pts = self.fault_centre[None, :]
|
|
130
|
+
else:
|
|
131
|
+
pts = self.model.rescale(self.fault_centre[None, :], inplace=False)
|
|
132
|
+
# pts = self.fault_centre[None, :]
|
|
133
|
+
fault_ellipsoid = pv.PolyData(pts)
|
|
134
|
+
# fault_ellipsoid = pv.PolyData(
|
|
135
|
+
# self.model.rescale(self.fault_centre[None, :], inplace=False)
|
|
136
|
+
# )
|
|
137
|
+
fault_ellipsoid["norm"] = self.fault_normal_vector[None, :]
|
|
138
|
+
fault_ellipsoid['norm'] /= np.linalg.norm(fault_ellipsoid['norm'], axis=1)[:, None]
|
|
133
139
|
geom = pv.ParametricEllipsoid(
|
|
134
140
|
self.fault_minor_axis,
|
|
135
141
|
self.fault_major_axis,
|
|
@@ -461,7 +467,7 @@ class FaultSegment(StructuralFrame):
|
|
|
461
467
|
)
|
|
462
468
|
self.abut[abutting_fault_feature.name] = abutting_region
|
|
463
469
|
self.__getitem__(0).add_region(abutting_region)
|
|
464
|
-
|
|
470
|
+
return abutting_region
|
|
465
471
|
def save(self, filename, scalar_field=True, slip_vector=True, surface=True):
|
|
466
472
|
"""
|
|
467
473
|
Save the fault to a file
|
|
@@ -299,9 +299,9 @@ class ProcessInputData:
|
|
|
299
299
|
pts = self.fault_locations.loc[
|
|
300
300
|
self.fault_locations["feature_name"] == fname, ["X", "Y", "Z"]
|
|
301
301
|
]
|
|
302
|
-
fault_properties.loc[
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
fault_properties.loc[fname, ["centreEasting", "centreNorthing", "centreAltitude"]] = (
|
|
303
|
+
np.nanmean(pts, axis=0)
|
|
304
|
+
)
|
|
305
305
|
if (
|
|
306
306
|
"avgNormalEasting" not in fault_properties.columns
|
|
307
307
|
or "avgNormalNorthing" not in fault_properties.columns
|
|
@@ -449,9 +449,9 @@ class ProcessInputData:
|
|
|
449
449
|
for _name, sg in self.stratigraphic_order:
|
|
450
450
|
value = 0.0 # reset for each supergroup
|
|
451
451
|
if sg[0] not in self.thicknesses or self.thicknesses[sg[0]] <= 0:
|
|
452
|
-
self.thicknesses[
|
|
453
|
-
|
|
454
|
-
|
|
452
|
+
self.thicknesses[sg[0]] = (
|
|
453
|
+
np.inf
|
|
454
|
+
) # make the top unit infinite as it should extend to the top of the model
|
|
455
455
|
for g in reversed(
|
|
456
456
|
sg[:-1]
|
|
457
457
|
): # don't add the last unit as we never see the base of this unit.
|
LoopStructural/utils/_surface.py
CHANGED
|
@@ -145,7 +145,7 @@ class LoopIsosurfacer:
|
|
|
145
145
|
values = np.zeros(verts.shape[0]) + isovalue
|
|
146
146
|
# need to add both global and local origin. If the bb is a buffer the local
|
|
147
147
|
# origin may not be 0
|
|
148
|
-
verts += self.bounding_box.global_origin
|
|
148
|
+
verts += self.bounding_box.global_origin+self.bounding_box.origin
|
|
149
149
|
surfaces.append(
|
|
150
150
|
Surface(
|
|
151
151
|
vertices=verts,
|
LoopStructural/utils/regions.py
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Tuple
|
|
2
4
|
|
|
5
|
+
class BaseRegion(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def __init__(self, feature, vector=None, point=None):
|
|
8
|
+
self.feature = feature
|
|
9
|
+
self.vector = vector
|
|
10
|
+
self.point = point
|
|
11
|
+
self.name = None
|
|
12
|
+
self.parent = None
|
|
3
13
|
|
|
4
|
-
|
|
5
|
-
def
|
|
6
|
-
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def __call__(self, xyz) -> np.ndarray:
|
|
16
|
+
"""Evaluate the region based on the input coordinates."""
|
|
17
|
+
pass
|
|
7
18
|
|
|
8
19
|
|
|
9
20
|
class RegionEverywhere(BaseRegion):
|
|
@@ -23,19 +34,18 @@ class RegionFunction(BaseRegion):
|
|
|
23
34
|
def __call__(self, xyz):
|
|
24
35
|
return self.function(xyz)
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
class PositiveRegion:
|
|
37
|
+
class BaseSignRegion(BaseRegion):
|
|
28
38
|
"""Helper class for evaluating whether you are in the positive region of a scalar field.
|
|
29
39
|
If its outside of the support it will interpolate the average gradient at a point on the 0 isovalue
|
|
30
40
|
and calculate the distance from this. Alternatively, a point and vector can be used to save computational time
|
|
31
41
|
"""
|
|
32
42
|
|
|
33
43
|
def __init__(self, feature, vector=None, point=None):
|
|
34
|
-
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
44
|
+
super().__init__(feature, vector, point)
|
|
45
|
+
self.name = 'PositiveRegion'
|
|
46
|
+
self.parent = feature
|
|
37
47
|
|
|
38
|
-
def
|
|
48
|
+
def _calculate_value_and_distance(self, xyz)-> Tuple[np.ndarray, np.ndarray]:
|
|
39
49
|
val = self.feature.evaluate_value(xyz)
|
|
40
50
|
# find a point on/near 0 isosurface
|
|
41
51
|
if self.point is None:
|
|
@@ -52,51 +62,45 @@ class PositiveRegion:
|
|
|
52
62
|
average_gradient /= np.linalg.norm(average_gradient)
|
|
53
63
|
else:
|
|
54
64
|
average_gradient = self.vector
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
|
|
66
|
+
distance = np.einsum(
|
|
67
|
+
"ij,j->i", centre[None, :] - xyz, average_gradient.reshape(-1, 3)[0, :]
|
|
68
|
+
)
|
|
69
|
+
return val, distance
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PositiveRegion(BaseSignRegion):
|
|
73
|
+
"""Helper class for evaluating whether you are in the positive region of a scalar field.
|
|
74
|
+
If its outside of the support it will interpolate the average gradient at a point on the 0 isovalue
|
|
75
|
+
and calculate the distance from this. Alternatively, a point and vector can be used to save computational time
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, feature, vector=None, point=None):
|
|
79
|
+
super().__init__(feature, vector, point)
|
|
80
|
+
self.name = 'PositiveRegion'
|
|
81
|
+
self.parent = feature
|
|
82
|
+
|
|
83
|
+
def __call__(self, xyz) -> np.ndarray:
|
|
84
|
+
val, distance = self._calculate_value_and_distance(xyz)
|
|
59
85
|
return np.logical_or(
|
|
60
86
|
np.logical_and(~np.isnan(val), val > 0),
|
|
61
87
|
np.logical_and(np.isnan(val), distance > 0),
|
|
62
88
|
)
|
|
63
89
|
|
|
64
90
|
|
|
65
|
-
class NegativeRegion:
|
|
91
|
+
class NegativeRegion(BaseSignRegion):
|
|
66
92
|
"""Helper class for evaluating whether you are in the positive region of a scalar field.
|
|
67
93
|
If its outside of the support it will interpolate the average gradient at a point on the 0 isovalue
|
|
68
94
|
and calculate the distance from this. Alternatively, a point and vector can be used to save computational time
|
|
69
95
|
"""
|
|
70
96
|
|
|
71
97
|
def __init__(self, feature, vector=None, point=None):
|
|
72
|
-
|
|
73
|
-
self.
|
|
74
|
-
self.
|
|
98
|
+
super().__init__(feature, vector, point)
|
|
99
|
+
self.name = 'NegativeRegion'
|
|
100
|
+
self.parent = feature
|
|
75
101
|
|
|
76
|
-
def __call__(self, xyz):
|
|
77
|
-
val = self.
|
|
78
|
-
# find a point on/near 0 isosurface
|
|
79
|
-
if self.point is None:
|
|
80
|
-
mask = np.zeros(xyz.shape[0], dtype="bool")
|
|
81
|
-
mask[:] = val < 0
|
|
82
|
-
if np.sum(mask) == 0:
|
|
83
|
-
raise ValueError("Cannot find point on surface")
|
|
84
|
-
centre = xyz[mask, :][0, :]
|
|
85
|
-
else:
|
|
86
|
-
centre = self.point
|
|
87
|
-
if self.vector is None:
|
|
88
|
-
average_gradient = self.feature.evaluate_gradient(np.array([centre]))[0]
|
|
89
|
-
average_gradient[2] = 0
|
|
90
|
-
average_gradient /= np.linalg.norm(average_gradient)
|
|
91
|
-
else:
|
|
92
|
-
average_gradient = self.vector
|
|
93
|
-
distance = np.einsum("ij,j->i", xyz - centre[None, :], average_gradient)
|
|
94
|
-
# distance = ((xyz[:,0] - centre[None,0])*average_gradient[0] +
|
|
95
|
-
# (xyz[:,1] - centre[None,1])*average_gradient[1] +
|
|
96
|
-
# ( xyz[:,2] - centre[None,2])*average_gradient[2])
|
|
97
|
-
# return np.logical_or(np.logical_and(~np.isnan(val),val
|
|
98
|
-
# < 0),
|
|
99
|
-
# np.logical_and(np.isnan(val),distance>0))
|
|
102
|
+
def __call__(self, xyz) -> np.ndarray:
|
|
103
|
+
val, distance = self._calculate_value_and_distance(xyz)
|
|
100
104
|
return np.logical_or(
|
|
101
105
|
np.logical_and(~np.isnan(val), val < 0),
|
|
102
106
|
np.logical_and(np.isnan(val), distance < 0),
|
LoopStructural/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.6.
|
|
1
|
+
__version__ = "1.6.13"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
LoopStructural/__init__.py,sha256=fg_Vm1aMDYIf_CffTFopLsTx21u6deLaI7JMVpRYdOI,1378
|
|
2
|
-
LoopStructural/version.py,sha256=
|
|
2
|
+
LoopStructural/version.py,sha256=SVPBACE1uUZFOAHuNVOfFUNhVa6mv28StfR80qCbEYs,23
|
|
3
3
|
LoopStructural/datasets/__init__.py,sha256=ylb7fzJU_DyQ73LlwQos7VamqkDSGITbbnoKg7KAOmE,677
|
|
4
4
|
LoopStructural/datasets/_base.py,sha256=FB_D5ybBYHoaNbycdkpZcRffzjrrL1xp9X0k-pyob9Y,7618
|
|
5
5
|
LoopStructural/datasets/_example_models.py,sha256=Zg33IeUyh4C-lC0DRMLqCDP2IrX8L-gNV1WxJwBGjzM,113
|
|
@@ -29,8 +29,8 @@ LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv,sha256=
|
|
|
29
29
|
LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv,sha256=RysyqUAIjY6iIDUfTh11n9QUQWXB_qxKnZeN_DqNzlY,26745
|
|
30
30
|
LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv,sha256=pnSmG-wL8-kxuoHo_pgpJrfTmsZOzc8L0vxpBRh3r8A,355
|
|
31
31
|
LoopStructural/datatypes/__init__.py,sha256=lVg64DnynMm58qvYTjLrcyWH7vk2ngr9JGMo5FaiALI,160
|
|
32
|
-
LoopStructural/datatypes/_bounding_box.py,sha256=
|
|
33
|
-
LoopStructural/datatypes/_point.py,sha256=
|
|
32
|
+
LoopStructural/datatypes/_bounding_box.py,sha256=0bTujF745De-MzIch5Wzb7PD1J9j65PqHkamhLW5dZ4,18368
|
|
33
|
+
LoopStructural/datatypes/_point.py,sha256=qg3lXUA1rnu1N1cEWG0WvhvJuENfDgpEDIeYldWBaG8,7740
|
|
34
34
|
LoopStructural/datatypes/_structured_grid.py,sha256=mc-UM1Gh_BjHFItuPE4FF5wvGzJnSqF2MTx_xvrwcTk,5088
|
|
35
35
|
LoopStructural/datatypes/_surface.py,sha256=5BpPKVS4X3Kq1k3YxxAofKMgxdXhnOIcDi6NzKn2p2Q,6652
|
|
36
36
|
LoopStructural/export/exporters.py,sha256=BniZu-PqQvHqCU6GIuJQ5FPzI9Dx_T6rI8EW1pykois,17209
|
|
@@ -43,7 +43,7 @@ LoopStructural/interpolators/_api.py,sha256=EC4ogG2uPq-z_pgNGd_eTieTl92eaZ-rjyoF
|
|
|
43
43
|
LoopStructural/interpolators/_builders.py,sha256=B49KsxB8RRN6IHDfGT43nXWe_Av1SVVT8vm2Nh1oEiQ,6758
|
|
44
44
|
LoopStructural/interpolators/_discrete_fold_interpolator.py,sha256=eDe0R1lcQ0AuMcv7zlpu5c-soCv7AybIqQAuN2vFE3M,6542
|
|
45
45
|
LoopStructural/interpolators/_discrete_interpolator.py,sha256=i_joZ8HOf_s6Q2L8gHFnhkdtgyED1SjATxRsRd1HxRU,26038
|
|
46
|
-
LoopStructural/interpolators/_finite_difference_interpolator.py,sha256=
|
|
46
|
+
LoopStructural/interpolators/_finite_difference_interpolator.py,sha256=mZ89FWQZ5RbzhL9UYH8VzWME-28dy331KXtYtpqepHo,18351
|
|
47
47
|
LoopStructural/interpolators/_geological_interpolator.py,sha256=hcQuyv1zYakJ7mcDFlLj-YarjnMQvlP6pVbK1KuxBWs,11195
|
|
48
48
|
LoopStructural/interpolators/_interpolator_builder.py,sha256=Z8bhmco5aSQX19A8It2SB_rG61wnlyshWfp3ivm8rU0,4586
|
|
49
49
|
LoopStructural/interpolators/_interpolator_factory.py,sha256=fbjebXSe5IgTol1tnBlnsw9gD426v-TGkX3gquIg7LI,2782
|
|
@@ -57,39 +57,39 @@ LoopStructural/interpolators/supports/_2d_p1_unstructured.py,sha256=okcy56nyjued
|
|
|
57
57
|
LoopStructural/interpolators/supports/_2d_p2_unstructured.py,sha256=TeBVtT1PMV7CKzmnFZke37acMoFxouer20cskS7pVoE,10422
|
|
58
58
|
LoopStructural/interpolators/supports/_2d_structured_grid.py,sha256=Pt9fiXyTS-RTd3mxXr3EUQfB6DhKChHQ5zbWub54nW0,16347
|
|
59
59
|
LoopStructural/interpolators/supports/_2d_structured_tetra.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
|
-
LoopStructural/interpolators/supports/_3d_base_structured.py,sha256=
|
|
60
|
+
LoopStructural/interpolators/supports/_3d_base_structured.py,sha256=PYIkgWCTHk0OHn-T9wpF4ZFYnKyvXNZgUyqO7YcoFjU,16481
|
|
61
61
|
LoopStructural/interpolators/supports/_3d_p2_tetra.py,sha256=CqGVJRUMxbPQZDhhopNt_s9gVhMqh4YbjQyDZonoyxc,11574
|
|
62
62
|
LoopStructural/interpolators/supports/_3d_structured_grid.py,sha256=x9NoZRsl58iowcObavgb0nY_C335BmcIYgec9REsFpU,17366
|
|
63
|
-
LoopStructural/interpolators/supports/_3d_structured_tetra.py,sha256=
|
|
64
|
-
LoopStructural/interpolators/supports/_3d_unstructured_tetra.py,sha256=
|
|
63
|
+
LoopStructural/interpolators/supports/_3d_structured_tetra.py,sha256=5zUNtvEXDvbCHZCu6Fz9WjGbnrMaq-sYJqNUufyLcq8,26505
|
|
64
|
+
LoopStructural/interpolators/supports/_3d_unstructured_tetra.py,sha256=_peXMTMxctuWNOL74AHxzw0b_1sP5glvbJigIvIkK9I,23867
|
|
65
65
|
LoopStructural/interpolators/supports/__init__.py,sha256=V0JjixoBIUZVAo5MmqARR67xDOoQwnb4G3SXeOMRSyQ,1603
|
|
66
66
|
LoopStructural/interpolators/supports/_aabb.py,sha256=Z-kH_u6c6izak0aHG3Uo14PEKQeZmYlevLDC32Q06xk,3208
|
|
67
67
|
LoopStructural/interpolators/supports/_base_support.py,sha256=pYzsmeBu4kLaD9ZKsz_dfjVpfuAd00xENqOQC9Xw5QY,2501
|
|
68
|
-
LoopStructural/interpolators/supports/_face_table.py,sha256=
|
|
68
|
+
LoopStructural/interpolators/supports/_face_table.py,sha256=Hyj4Io63NkPRN8ab9uDHyec-2Kb8BLY_xBF6STNlvBw,3095
|
|
69
69
|
LoopStructural/interpolators/supports/_support_factory.py,sha256=XNAxnr-JS3KEhdsoZeJ-VaLTJwlvxgBuRMCqYrCDW18,1485
|
|
70
70
|
LoopStructural/modelling/__init__.py,sha256=oW7dz6c8K1A0VcW7-mVcyqcENUrtybCb3eVUNXFvMfA,656
|
|
71
71
|
LoopStructural/modelling/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
|
-
LoopStructural/modelling/core/geological_model.py,sha256=
|
|
72
|
+
LoopStructural/modelling/core/geological_model.py,sha256=okbEwjL8-wGKeWhvYE-1XSOE2C1nHX7M2AR__42pyAk,66464
|
|
73
73
|
LoopStructural/modelling/features/__init__.py,sha256=Vf-qd5EDBtJ1DpuXXyCcw2-wf6LWPRW5wzxDEO3vOc8,939
|
|
74
74
|
LoopStructural/modelling/features/_analytical_feature.py,sha256=U_g86LgQhYY2359rdsDqpvziYwqrWkc5EdvhJARiUWo,3597
|
|
75
|
-
LoopStructural/modelling/features/_base_geological_feature.py,sha256=
|
|
75
|
+
LoopStructural/modelling/features/_base_geological_feature.py,sha256=hx1EmDGWCE8YhypWuet6SUfpoGQCSecXj6zTF-CLRRg,12179
|
|
76
76
|
LoopStructural/modelling/features/_cross_product_geological_feature.py,sha256=GIyCHUdE6F-bse2e4puG9V2f7qRtDVfby5PRe2BboD4,3021
|
|
77
77
|
LoopStructural/modelling/features/_geological_feature.py,sha256=u6pbKj9BujX1Ijj5eVdhwGDNjrIAI16CpiAn5n8g3RY,11279
|
|
78
|
-
LoopStructural/modelling/features/_lambda_geological_feature.py,sha256=
|
|
78
|
+
LoopStructural/modelling/features/_lambda_geological_feature.py,sha256=GiB19l6v5WvvR8CitATZvCwaOfRyLuzchoXzpNupsfM,5743
|
|
79
79
|
LoopStructural/modelling/features/_projected_vector_feature.py,sha256=aifVLgn2spmK7GGlO0iHDewf1pFL-QoRzZEePTZwX1s,3017
|
|
80
80
|
LoopStructural/modelling/features/_region.py,sha256=TB4qnoTDQM2VgRjgyODN839fKe3kuRYLllJj0xnDKXo,478
|
|
81
81
|
LoopStructural/modelling/features/_structural_frame.py,sha256=e3QmNHLwuZc5PX3rLafocmBLNTclO90AXB4BRILCFC4,5044
|
|
82
82
|
LoopStructural/modelling/features/_unconformity_feature.py,sha256=2Bx0BI38YLdcNvDWuP9E1pKFN4orEUq9aC8b5xG1UVk,2362
|
|
83
83
|
LoopStructural/modelling/features/builders/__init__.py,sha256=Gqld1C-PcaXfJ8vpkWMDCmehmd3hZNYQk1knPtl59Bk,266
|
|
84
84
|
LoopStructural/modelling/features/builders/_base_builder.py,sha256=N3txGC98V08A8-k2TLdoIWgWLfblZ91kaTvciPq_QVM,3750
|
|
85
|
-
LoopStructural/modelling/features/builders/_fault_builder.py,sha256=
|
|
85
|
+
LoopStructural/modelling/features/builders/_fault_builder.py,sha256=CeQnvgDrgMIbyPV6nB0qnpY5PJG1OYTJIukRXv4df1E,25324
|
|
86
86
|
LoopStructural/modelling/features/builders/_folded_feature_builder.py,sha256=1_0BVTzcvmFl6K3_lX-jF0tiMFPmS8j6vPeSLn9MbrE,6607
|
|
87
87
|
LoopStructural/modelling/features/builders/_geological_feature_builder.py,sha256=jn2BiZlzXyWl0_TrsajpFR2wegGOpbuO5yFu2FamuYA,22014
|
|
88
88
|
LoopStructural/modelling/features/builders/_structural_frame_builder.py,sha256=ms3-fuFpDEarjzYU5W499TquOIlTwHPUibVxIypfmWY,8019
|
|
89
89
|
LoopStructural/modelling/features/fault/__init__.py,sha256=4u0KfYzmoO-ddFGo9qd9ov0gBoLqBiPAUsaw5zhEOAQ,189
|
|
90
|
-
LoopStructural/modelling/features/fault/_fault_function.py,sha256=
|
|
90
|
+
LoopStructural/modelling/features/fault/_fault_function.py,sha256=xvyoFA3dOtYilKycPotAzLkzYvy-kk-4qilhqt8Q8-8,12686
|
|
91
91
|
LoopStructural/modelling/features/fault/_fault_function_feature.py,sha256=4m0jVNx7ewrVI0pECI1wNciv8Cy8FzhZrYDjKJ_e2GU,2558
|
|
92
|
-
LoopStructural/modelling/features/fault/_fault_segment.py,sha256=
|
|
92
|
+
LoopStructural/modelling/features/fault/_fault_segment.py,sha256=dNTCY0ZyC8krrL1suSnhywSE_i5V_VZ4DJ2BieirkhI,18305
|
|
93
93
|
LoopStructural/modelling/features/fold/__init__.py,sha256=pOv20yQvshZozvmO_YFw2E7Prp9DExlm855N-0SnxbQ,175
|
|
94
94
|
LoopStructural/modelling/features/fold/_fold.py,sha256=bPnnLUSiF4uoMRg8aHoOSTPRgaM0JyLoRQPu5_A-J3w,5448
|
|
95
95
|
LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py,sha256=CXLbFRQ3CrTMAcHmfdbKcmSvvLs9_6TLe0Wqi1pK2tg,892
|
|
@@ -103,7 +103,7 @@ LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.
|
|
|
103
103
|
LoopStructural/modelling/input/__init__.py,sha256=HhJM3V5b-8_64LiRbF3Bd1pjWhJlcknxMSMPRrqZ0-I,153
|
|
104
104
|
LoopStructural/modelling/input/fault_network.py,sha256=0uxl7lOySdhMhNXoiOkuiHIXqAz1Ls0j-W65cmdQoP8,2348
|
|
105
105
|
LoopStructural/modelling/input/map2loop_processor.py,sha256=T7Fgqd7FNJWylLKvfIniRZBMRMeAoP8iU330-WYU8Fg,7031
|
|
106
|
-
LoopStructural/modelling/input/process_data.py,sha256=
|
|
106
|
+
LoopStructural/modelling/input/process_data.py,sha256=9xWZtFWbE5NjJzULSZDJt-qccV0GuZ-oNMi3nfxPT-M,26293
|
|
107
107
|
LoopStructural/modelling/input/project_file.py,sha256=WhJkMfDK9uE7MK7HK-YK6ZOBAdwLX5P7ThZgXj444Eg,4604
|
|
108
108
|
LoopStructural/modelling/intrusions/__init__.py,sha256=EpZK3cHJwGQhPUYIwKCKu8vkNdt_nOgWF0zfhiqDYDA,712
|
|
109
109
|
LoopStructural/modelling/intrusions/geom_conceptual_models.py,sha256=jwTlhYySUj7z4DEnJoi4AINZB_N3-SW6ONRFL66OsW0,3665
|
|
@@ -113,7 +113,7 @@ LoopStructural/modelling/intrusions/intrusion_feature.py,sha256=ESjtikHFJQzUnowb
|
|
|
113
113
|
LoopStructural/modelling/intrusions/intrusion_frame_builder.py,sha256=Q1TPHxREcrO7Rw71nUfACZHfYnISLjqlgkUNTPT324k,40143
|
|
114
114
|
LoopStructural/modelling/intrusions/intrusion_support_functions.py,sha256=wodakheMD62WJyoKnyX8UO-C1pje0I-5kHQEoDqShzo,13951
|
|
115
115
|
LoopStructural/utils/__init__.py,sha256=OJqNSu40SYJeC26IhoBBXDqQOogWjMGA-YokKVRrwMs,924
|
|
116
|
-
LoopStructural/utils/_surface.py,sha256=
|
|
116
|
+
LoopStructural/utils/_surface.py,sha256=Eg7x1GGfELl7bPe21_wU96Dn4JWJNReEFxwq-aIV4A4,6165
|
|
117
117
|
LoopStructural/utils/_transformation.py,sha256=peuLPH3BJ5DxnPbOuNKcqK4eXhAXdbT540L1OIsO3v0,5404
|
|
118
118
|
LoopStructural/utils/colours.py,sha256=-KRf1MXKx4L8TXnwyiunmKAX4tfy0qG68fRadyfn_bM,1163
|
|
119
119
|
LoopStructural/utils/config.py,sha256=ITGOtZTo2_QBwXkG_0AFANfE90J9siCXLzxypVmg9QA,414
|
|
@@ -125,12 +125,12 @@ LoopStructural/utils/json_encoder.py,sha256=5YNouf1TlhjEqOYgthd07MRXc0JLgxern-ny
|
|
|
125
125
|
LoopStructural/utils/linalg.py,sha256=tBXyu6NXcG2AcPuzUMnkVI4ncZWtE_MPHGj2PLXRwfY,123
|
|
126
126
|
LoopStructural/utils/logging.py,sha256=dIUWEsS2lT4G1dsf4ZYXknTR7eQkrgvGA4b_E0vMIRU,2402
|
|
127
127
|
LoopStructural/utils/maths.py,sha256=8iqdQdB2-bf14SzIzfFxvjWbzmPknqK9DI7CWEcW6XU,8402
|
|
128
|
-
LoopStructural/utils/regions.py,sha256=
|
|
128
|
+
LoopStructural/utils/regions.py,sha256=SjCC40GI7_n03G4mlcmvyrBgJFbxnvB3leBzXWco37o,3891
|
|
129
129
|
LoopStructural/utils/typing.py,sha256=29uVSTZdzXXH-jdlaYyBWZ1gQ2-nlZ2-XoVgG_PXNFY,157
|
|
130
130
|
LoopStructural/utils/utils.py,sha256=2Z4zVE6G752-SPmM29zebk82bROJxEwi_YiiJjcVED4,2438
|
|
131
131
|
LoopStructural/visualisation/__init__.py,sha256=5BDgKor8-ae6DrS7IZybJ3Wq_pTnCchxuY4EgzA7v1M,318
|
|
132
|
-
loopstructural-1.6.
|
|
133
|
-
loopstructural-1.6.
|
|
134
|
-
loopstructural-1.6.
|
|
135
|
-
loopstructural-1.6.
|
|
136
|
-
loopstructural-1.6.
|
|
132
|
+
loopstructural-1.6.13.dist-info/licenses/LICENSE,sha256=ZqGeNFOgmYevj7Ld7Q-kR4lAxWXuBRUdUmPC6XM_py8,1071
|
|
133
|
+
loopstructural-1.6.13.dist-info/METADATA,sha256=EwFSrenvuB1yMD-jpnkD6tmiESkC-xgJVTo7-lcmL3I,6454
|
|
134
|
+
loopstructural-1.6.13.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
|
135
|
+
loopstructural-1.6.13.dist-info/top_level.txt,sha256=QtQErKzYHfg6ddxTQ1NyaTxXBVM6qAqrM_vxEPyXZLg,15
|
|
136
|
+
loopstructural-1.6.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|