Skip to content

Commit 58b211f

Browse files
authored
Various small fixes (Loop3D#211)
* fix: fault segment structural frame was intiialised incorrectly * fix: adding copy method as a required method for all geological features * fix: allow feature to be initialised without a builder this means the feature won't check if the interpolator is up to date. In order for this to occur a builder needs to have an up_to_date method * fix: bug with pyvista glyphs when working in a real world coordinate system. Fixed by reprojecting into local coordinates for the glyphing and then reprojecting out of the local coords after glyping. requires a bounding box to be passed to vtk method for any vector calls. Added a project/reproject method to the bounding box * fix: rescale/scale no longer default to in place and will copy the points given to them * fix: interpolator factory with data works * ci: adding icon to docs * fix: removing config details for release please * fix: interface constraints used wrong reference to interpolation matrix * fix: throw error if interpolation support doesn't have 3 steps * style: style fixes by ruff and autoformatting by black * typo, steps_vector instead of nsteps * fix: order of arguments incorrect for fault segment * fix: add copy to intrusion * style: style fixes by ruff and autoformatting by black * fix: bb plotting in correct place when not using global origin * fix: inequality pairs being used by FD interpolator * style: style fixes by ruff and autoformatting by black * style: remove comments/old code * fix: add a feature to project a vector onto a plane * style: style fixes by ruff and autoformatting by black * fix: updating transformer to take angle and translation as input * style: style fixes by ruff and autoformatting by black * fix: disable axis_function and limb_function for folds --------- Co-authored-by: lachlangrose <lachlangrose@users.noreply.github.com> Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com>
1 parent ec52888 commit 58b211f

22 files changed

+407
-73
lines changed

.github/workflows/documentation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build documentation and deploy
1+
name: "📚 Build documentation and deploy "
22

33
on:
44
workflow_dispatch:

LoopStructural/datatypes/_bounding_box.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,15 @@ def vtk(self):
410410
import pyvista as pv
411411
except ImportError:
412412
raise ImportError("pyvista is required for vtk support")
413-
x = np.linspace(self.global_origin[0], self.global_maximum[0], self.nsteps[0])
414-
y = np.linspace(self.global_origin[1], self.global_maximum[1], self.nsteps[1])
415-
z = np.linspace(self.global_origin[2], self.global_maximum[2], self.nsteps[2])
413+
x = np.linspace(
414+
self.global_origin[0] + self.origin[0], self.global_maximum[0], self.nsteps[0]
415+
)
416+
y = np.linspace(
417+
self.global_origin[1] + self.origin[1], self.global_maximum[1], self.nsteps[1]
418+
)
419+
z = np.linspace(
420+
self.global_origin[2] + self.origin[2], self.global_maximum[2], self.nsteps[2]
421+
)
416422
return pv.RectilinearGrid(
417423
x,
418424
y,
@@ -435,3 +441,52 @@ def structured_grid(
435441
properties=_vertex_data,
436442
name=name,
437443
)
444+
445+
def project(self, xyz):
446+
"""Project a point into the bounding box
447+
448+
Parameters
449+
----------
450+
xyz : np.ndarray
451+
point to project
452+
453+
Returns
454+
-------
455+
np.ndarray
456+
projected point
457+
"""
458+
459+
return (xyz - self.global_origin) / np.max(
460+
(self.global_maximum - self.global_origin)
461+
) # np.clip(xyz, self.origin, self.maximum)
462+
463+
def reproject(self, xyz):
464+
"""Reproject a point from the bounding box to the global space
465+
466+
Parameters
467+
----------
468+
xyz : np.ndarray
469+
point to reproject
470+
471+
Returns
472+
-------
473+
np.ndarray
474+
reprojected point
475+
"""
476+
477+
return xyz * np.max((self.global_maximum - self.global_origin)) + self.global_origin
478+
479+
def __repr__(self):
480+
return f"BoundingBox({self.origin}, {self.maximum}, {self.nsteps})"
481+
482+
def __str__(self):
483+
return f"BoundingBox({self.origin}, {self.maximum}, {self.nsteps})"
484+
485+
def __eq__(self, other):
486+
if not isinstance(other, BoundingBox):
487+
return False
488+
return (
489+
np.allclose(self.origin, other.origin)
490+
and np.allclose(self.maximum, other.maximum)
491+
and np.allclose(self.nsteps, other.nsteps)
492+
)

LoopStructural/datatypes/_point.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ def to_dict(self):
2525
),
2626
}
2727

28-
def vtk(self):
28+
def vtk(self, scalars=None):
2929
import pyvista as pv
3030

3131
points = pv.PolyData(self.locations)
32-
points["values"] = self.values
32+
if scalars is not None and len(scalars) == len(self.locations):
33+
points.point_data['scalars'] = scalars
34+
else:
35+
points["values"] = self.values
3336
return points
3437

3538
def plot(self, pyvista_kwargs={}):
@@ -123,25 +126,48 @@ def to_dict(self):
123126
def from_dict(self, d):
124127
return VectorPoints(d['locations'], d['vectors'], d['name'], d.get('properties', None))
125128

126-
def vtk(self, geom='arrow', scale=1.0, scale_function=None, normalise=True, tolerance=0.05):
129+
def vtk(
130+
self,
131+
geom='arrow',
132+
scale=0.10,
133+
scale_function=None,
134+
normalise=True,
135+
tolerance=0.05,
136+
bb=None,
137+
scalars=None,
138+
):
127139
import pyvista as pv
128140

141+
_projected = False
129142
vectors = np.copy(self.vectors)
130143
if normalise:
131144
norm = np.linalg.norm(vectors, axis=1)
132145
vectors[norm > 0, :] /= norm[norm > 0][:, None]
133146
if scale_function is not None:
134147
# vectors /= np.linalg.norm(vectors, axis=1)[:, None]
135148
vectors *= scale_function(self.locations)[:, None]
136-
points = pv.PolyData(self.locations)
149+
locations = self.locations
150+
if bb is not None:
151+
try:
152+
locations = bb.project(locations)
153+
_projected = True
154+
except Exception as e:
155+
logger.error(f'Failed to project points to bounding box: {e}')
156+
logger.error('Using unprojected points, this may cause issues with the glyphing')
157+
points = pv.PolyData(locations)
158+
if scalars is not None and len(scalars) == len(self.locations):
159+
points['scalars'] = scalars
137160
points.point_data.set_vectors(vectors, 'vectors')
138161
if geom == 'arrow':
139162
geom = pv.Arrow(scale=scale)
140163
elif geom == 'disc':
141-
geom = pv.Disc(inner=0, outer=scale)
164+
geom = pv.Disc(inner=0, outer=scale).rotate_y(90)
142165

143166
# Perform the glyph
144-
return points.glyph(orient="vectors", geom=geom, tolerance=tolerance)
167+
glyphed = points.glyph(orient="vectors", geom=geom, tolerance=tolerance, scale=False)
168+
if _projected:
169+
glyphed.points = bb.reproject(glyphed.points)
170+
return glyphed
145171

146172
def plot(self, pyvista_kwargs={}):
147173
"""Calls pyvista plot on the vtk object

LoopStructural/interpolators/_finite_difference_interpolator.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def setup_interpolator(self, **kwargs):
9797
self.add_interface_constraints(self.interpolation_weights["ipw"])
9898
self.add_value_inequality_constraints()
9999
self.add_inequality_pairs_constraints(
100-
kwargs.get('inequality_pairs', None),
100+
pairs=kwargs.get('inequality_pairs', None),
101101
upper_bound=kwargs.get('inequality_pair_upper_bound', np.finfo(float).eps),
102102
lower_bound=kwargs.get('inequality_pair_lower_bound', -np.inf),
103103
)
@@ -172,16 +172,18 @@ def add_interface_constraints(self, w=1.0):
172172
# get elements for points
173173
points = self.get_interface_constraints()
174174
if points.shape[0] > 1:
175-
vertices, c, tetras, inside = self.support.get_element_for_location(
175+
node_idx, inside = self.support.position_to_cell_corners(
176176
points[:, : self.support.dimension]
177177
)
178-
# calculate volume of tetras
179-
# vecs = vertices[inside, 1:, :] - vertices[inside, 0, None, :]
180-
# vol = np.abs(np.linalg.det(vecs)) / 6
181-
A = c[inside, :]
182-
# A *= vol[:,None]
183-
idc = tetras[inside, :]
184-
178+
gi = np.zeros(self.support.n_nodes, dtype=int)
179+
gi[:] = -1
180+
gi[self.region] = np.arange(0, self.nx, dtype=int)
181+
idc = np.zeros(node_idx.shape).astype(int)
182+
idc[:] = -1
183+
idc[inside, :] = gi[node_idx[inside, :]]
184+
inside = np.logical_and(~np.any(idc == -1, axis=1), inside)
185+
idc = idc[inside, :]
186+
A = self.support.position_to_dof_coefs(points[inside, : self.support.dimension])
185187
for unique_id in np.unique(
186188
points[
187189
np.logical_and(~np.isnan(points[:, self.support.dimension]), inside),
@@ -197,7 +199,6 @@ def add_interface_constraints(self, w=1.0):
197199
).T.reshape(-1, 2)
198200
interface_A = np.hstack([A[mask, :][ij[:, 0], :], -A[mask, :][ij[:, 1], :]])
199201
interface_idc = np.hstack([idc[mask, :][ij[:, 0], :], idc[mask, :][ij[:, 1], :]])
200-
201202
# now map the index from global to region create array size of mesh
202203
# initialise as np.nan, then map points inside region to 0->nx
203204
gi = np.zeros(self.support.n_nodes).astype(int)
@@ -229,7 +230,6 @@ def add_gradient_constraints(self, w=1.0):
229230
points = self.get_gradient_constraints()
230231
if points.shape[0] > 0:
231232
# calculate unit vector for orientation data
232-
# points[:,3:]/=np.linalg.norm(points[:,3:],axis=1)[:,None]
233233

234234
node_idx, inside = self.support.position_to_cell_corners(
235235
points[:, : self.support.dimension]

LoopStructural/interpolators/_interpolator_factory.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,14 @@ def create_interpolator_with_data(
6565
gradient_norm_constraints: Optional[np.ndarray] = None,
6666
gradient_constraints: Optional[np.ndarray] = None,
6767
):
68-
if interpolatortype is None:
69-
raise ValueError("No interpolator type specified")
70-
if boundingbox is None:
71-
raise ValueError("No bounding box specified")
72-
if nelements is None:
73-
raise ValueError("No number of elements specified")
74-
if isinstance(interpolatortype, str):
75-
interpolatortype = InterpolatorType._member_map_[interpolatortype].numerator
76-
if support is None:
77-
raise Exception("Support must be specified")
78-
# supporttype = support_interpolator_map[interpolatortype]
79-
# support = SupportFactory.create_support(
80-
# supporttype, boundingbox, nelements, element_volume
81-
# )
82-
interpolator = interpolator_map[interpolatortype](support)
68+
interpolator = InterpolatorFactory.create_interpolator(
69+
interpolatortype, boundingbox, nelements, element_volume, support
70+
)
8371
if value_constraints is not None:
84-
interpolator.add_value_constraints(value_constraints)
72+
interpolator.set_value_constraints(value_constraints)
8573
if gradient_norm_constraints is not None:
86-
interpolator.add_gradient_constraints(gradient_norm_constraints)
74+
interpolator.set_normal_constraints(gradient_norm_constraints)
8775
if gradient_constraints is not None:
88-
interpolator.add_gradient_constraints(gradient_constraints)
76+
interpolator.set_gradient_constraints(gradient_constraints)
77+
interpolator.setup()
8978
return interpolator

LoopStructural/interpolators/supports/_3d_base_structured.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def __init__(
4141
raise LoopException("nsteps cannot be zero")
4242
if np.any(nsteps < 0):
4343
raise LoopException("nsteps cannot be negative")
44+
if np.any(nsteps < 3):
45+
raise LoopException(
46+
"step vector cannot be less than 3. Try increasing the resolution of the interpolator"
47+
)
4448
self._nsteps = np.array(nsteps, dtype=int) + 1
4549
self._step_vector = np.array(step_vector)
4650
self._origin = np.array(origin)

LoopStructural/modelling/core/geological_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,7 +1417,7 @@ def create_and_add_fault(
14171417
return fault
14181418

14191419
# TODO move rescale to bounding box/transformer
1420-
def rescale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
1420+
def rescale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
14211421
"""
14221422
Convert from model scale to real world scale - in the future this
14231423
should also do transformations?
@@ -1440,7 +1440,7 @@ def rescale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
14401440
return points
14411441

14421442
# TODO move scale to bounding box/transformer
1443-
def scale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
1443+
def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
14441444
"""Take points in UTM coordinates and reproject
14451445
into scaled model space
14461446

LoopStructural/modelling/features/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ class FeatureType(IntEnum):
3030

3131
from ._unconformity_feature import UnconformityFeature
3232
from ._analytical_feature import AnalyticalGeologicalFeature
33+
from ._projected_vector_feature import ProjectedVectorFeature

LoopStructural/modelling/features/_analytical_feature.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ def __init__(
3939
builder=None,
4040
):
4141
BaseFeature.__init__(self, name, model, faults, regions, builder)
42-
self.vector = np.array(vector, dtype=float)
43-
self.origin = np.array(origin, dtype=float)
42+
try:
43+
self.vector = np.array(vector, dtype=float).reshape(3)
44+
except ValueError:
45+
logger.error("AnalyticalGeologicalFeature: vector must be a 3 element array")
46+
raise ValueError("vector must be a 3 element array")
47+
try:
48+
self.origin = np.array(origin, dtype=float).reshape(3)
49+
except ValueError:
50+
logger.error("AnalyticalGeologicalFeature: origin must be a 3 element array")
51+
raise ValueError("origin must be a 3 element array")
4452
self.type = FeatureType.ANALYTICAL
4553

4654
def to_json(self):
@@ -86,3 +94,16 @@ def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False):
8694

8795
def get_data(self, value_map: Optional[dict] = None):
8896
return
97+
98+
def copy(self, name: Optional[str] = None):
99+
if name is None:
100+
name = self.name
101+
return AnalyticalGeologicalFeature(
102+
name,
103+
self.vector.copy(),
104+
self.origin.copy(),
105+
list(self.regions),
106+
list(self.faults),
107+
self.model,
108+
self.builder,
109+
)

LoopStructural/modelling/features/_base_geological_feature.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,12 @@ def surfaces(
296296
self.regions = [
297297
r for r in self.regions if r.name != self.name and r.parent.name != self.name
298298
]
299-
callable = lambda xyz: self.evaluate_value(self.model.scale(xyz))
299+
300+
callable = lambda xyz: (
301+
self.evaluate_value(self.model.scale(xyz))
302+
if self.model is not None
303+
else self.evaluate_value(xyz)
304+
)
300305
isosurfacer = LoopIsosurfacer(bounding_box, callable=callable)
301306
if name is None and self.name is not None:
302307
name = self.name
@@ -375,3 +380,14 @@ def get_data(self, value_map: Optional[dict] = None):
375380
dictionary of data
376381
"""
377382
raise NotImplementedError
383+
384+
@abstractmethod
385+
def copy(self, name: Optional[str] = None):
386+
"""Copy the feature
387+
388+
Returns
389+
-------
390+
BaseFeature
391+
copied feature
392+
"""
393+
raise NotImplementedError

0 commit comments

Comments
 (0)