Skip to content

Commit

Permalink
Various small fixes (#211)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: lachlangrose <[email protected]>
  • Loading branch information
3 people authored Jan 13, 2025
1 parent ec52888 commit 58b211f
Show file tree
Hide file tree
Showing 22 changed files with 407 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build documentation and deploy
name: "📚 Build documentation and deploy "

on:
workflow_dispatch:
Expand Down
61 changes: 58 additions & 3 deletions LoopStructural/datatypes/_bounding_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,15 @@ def vtk(self):
import pyvista as pv
except ImportError:
raise ImportError("pyvista is required for vtk support")
x = np.linspace(self.global_origin[0], self.global_maximum[0], self.nsteps[0])
y = np.linspace(self.global_origin[1], self.global_maximum[1], self.nsteps[1])
z = np.linspace(self.global_origin[2], self.global_maximum[2], self.nsteps[2])
x = np.linspace(
self.global_origin[0] + self.origin[0], self.global_maximum[0], self.nsteps[0]
)
y = np.linspace(
self.global_origin[1] + self.origin[1], self.global_maximum[1], self.nsteps[1]
)
z = np.linspace(
self.global_origin[2] + self.origin[2], self.global_maximum[2], self.nsteps[2]
)
return pv.RectilinearGrid(
x,
y,
Expand All @@ -435,3 +441,52 @@ def structured_grid(
properties=_vertex_data,
name=name,
)

def project(self, xyz):
"""Project a point into the bounding box
Parameters
----------
xyz : np.ndarray
point to project
Returns
-------
np.ndarray
projected point
"""

return (xyz - self.global_origin) / np.max(
(self.global_maximum - self.global_origin)
) # np.clip(xyz, self.origin, self.maximum)

def reproject(self, xyz):
"""Reproject a point from the bounding box to the global space
Parameters
----------
xyz : np.ndarray
point to reproject
Returns
-------
np.ndarray
reprojected point
"""

return xyz * np.max((self.global_maximum - self.global_origin)) + self.global_origin

def __repr__(self):
return f"BoundingBox({self.origin}, {self.maximum}, {self.nsteps})"

def __str__(self):
return f"BoundingBox({self.origin}, {self.maximum}, {self.nsteps})"

def __eq__(self, other):
if not isinstance(other, BoundingBox):
return False
return (
np.allclose(self.origin, other.origin)
and np.allclose(self.maximum, other.maximum)
and np.allclose(self.nsteps, other.nsteps)
)
38 changes: 32 additions & 6 deletions LoopStructural/datatypes/_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ def to_dict(self):
),
}

def vtk(self):
def vtk(self, scalars=None):
import pyvista as pv

points = pv.PolyData(self.locations)
points["values"] = self.values
if scalars is not None and len(scalars) == len(self.locations):
points.point_data['scalars'] = scalars
else:
points["values"] = self.values
return points

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

def vtk(self, geom='arrow', scale=1.0, scale_function=None, normalise=True, tolerance=0.05):
def vtk(
self,
geom='arrow',
scale=0.10,
scale_function=None,
normalise=True,
tolerance=0.05,
bb=None,
scalars=None,
):
import pyvista as pv

_projected = False
vectors = np.copy(self.vectors)
if normalise:
norm = np.linalg.norm(vectors, axis=1)
vectors[norm > 0, :] /= norm[norm > 0][:, None]
if scale_function is not None:
# vectors /= np.linalg.norm(vectors, axis=1)[:, None]
vectors *= scale_function(self.locations)[:, None]
points = pv.PolyData(self.locations)
locations = self.locations
if bb is not None:
try:
locations = bb.project(locations)
_projected = True
except Exception as e:
logger.error(f'Failed to project points to bounding box: {e}')
logger.error('Using unprojected points, this may cause issues with the glyphing')
points = pv.PolyData(locations)
if scalars is not None and len(scalars) == len(self.locations):
points['scalars'] = scalars
points.point_data.set_vectors(vectors, 'vectors')
if geom == 'arrow':
geom = pv.Arrow(scale=scale)
elif geom == 'disc':
geom = pv.Disc(inner=0, outer=scale)
geom = pv.Disc(inner=0, outer=scale).rotate_y(90)

# Perform the glyph
return points.glyph(orient="vectors", geom=geom, tolerance=tolerance)
glyphed = points.glyph(orient="vectors", geom=geom, tolerance=tolerance, scale=False)
if _projected:
glyphed.points = bb.reproject(glyphed.points)
return glyphed

def plot(self, pyvista_kwargs={}):
"""Calls pyvista plot on the vtk object
Expand Down
22 changes: 11 additions & 11 deletions LoopStructural/interpolators/_finite_difference_interpolator.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def setup_interpolator(self, **kwargs):
self.add_interface_constraints(self.interpolation_weights["ipw"])
self.add_value_inequality_constraints()
self.add_inequality_pairs_constraints(
kwargs.get('inequality_pairs', None),
pairs=kwargs.get('inequality_pairs', None),
upper_bound=kwargs.get('inequality_pair_upper_bound', np.finfo(float).eps),
lower_bound=kwargs.get('inequality_pair_lower_bound', -np.inf),
)
Expand Down Expand Up @@ -172,16 +172,18 @@ def add_interface_constraints(self, w=1.0):
# get elements for points
points = self.get_interface_constraints()
if points.shape[0] > 1:
vertices, c, tetras, inside = self.support.get_element_for_location(
node_idx, inside = self.support.position_to_cell_corners(
points[:, : self.support.dimension]
)
# calculate volume of tetras
# vecs = vertices[inside, 1:, :] - vertices[inside, 0, None, :]
# vol = np.abs(np.linalg.det(vecs)) / 6
A = c[inside, :]
# A *= vol[:,None]
idc = tetras[inside, :]

gi = np.zeros(self.support.n_nodes, dtype=int)
gi[:] = -1
gi[self.region] = np.arange(0, self.nx, dtype=int)
idc = np.zeros(node_idx.shape).astype(int)
idc[:] = -1
idc[inside, :] = gi[node_idx[inside, :]]
inside = np.logical_and(~np.any(idc == -1, axis=1), inside)
idc = idc[inside, :]
A = self.support.position_to_dof_coefs(points[inside, : self.support.dimension])
for unique_id in np.unique(
points[
np.logical_and(~np.isnan(points[:, self.support.dimension]), inside),
Expand All @@ -197,7 +199,6 @@ def add_interface_constraints(self, w=1.0):
).T.reshape(-1, 2)
interface_A = np.hstack([A[mask, :][ij[:, 0], :], -A[mask, :][ij[:, 1], :]])
interface_idc = np.hstack([idc[mask, :][ij[:, 0], :], idc[mask, :][ij[:, 1], :]])

# now map the index from global to region create array size of mesh
# initialise as np.nan, then map points inside region to 0->nx
gi = np.zeros(self.support.n_nodes).astype(int)
Expand Down Expand Up @@ -229,7 +230,6 @@ def add_gradient_constraints(self, w=1.0):
points = self.get_gradient_constraints()
if points.shape[0] > 0:
# calculate unit vector for orientation data
# points[:,3:]/=np.linalg.norm(points[:,3:],axis=1)[:,None]

node_idx, inside = self.support.position_to_cell_corners(
points[:, : self.support.dimension]
Expand Down
25 changes: 7 additions & 18 deletions LoopStructural/interpolators/_interpolator_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,14 @@ def create_interpolator_with_data(
gradient_norm_constraints: Optional[np.ndarray] = None,
gradient_constraints: Optional[np.ndarray] = None,
):
if interpolatortype is None:
raise ValueError("No interpolator type specified")
if boundingbox is None:
raise ValueError("No bounding box specified")
if nelements is None:
raise ValueError("No number of elements specified")
if isinstance(interpolatortype, str):
interpolatortype = InterpolatorType._member_map_[interpolatortype].numerator
if support is None:
raise Exception("Support must be specified")
# supporttype = support_interpolator_map[interpolatortype]
# support = SupportFactory.create_support(
# supporttype, boundingbox, nelements, element_volume
# )
interpolator = interpolator_map[interpolatortype](support)
interpolator = InterpolatorFactory.create_interpolator(
interpolatortype, boundingbox, nelements, element_volume, support
)
if value_constraints is not None:
interpolator.add_value_constraints(value_constraints)
interpolator.set_value_constraints(value_constraints)
if gradient_norm_constraints is not None:
interpolator.add_gradient_constraints(gradient_norm_constraints)
interpolator.set_normal_constraints(gradient_norm_constraints)
if gradient_constraints is not None:
interpolator.add_gradient_constraints(gradient_constraints)
interpolator.set_gradient_constraints(gradient_constraints)
interpolator.setup()
return interpolator
4 changes: 4 additions & 0 deletions LoopStructural/interpolators/supports/_3d_base_structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def __init__(
raise LoopException("nsteps cannot be zero")
if np.any(nsteps < 0):
raise LoopException("nsteps cannot be negative")
if np.any(nsteps < 3):
raise LoopException(
"step vector cannot be less than 3. Try increasing the resolution of the interpolator"
)
self._nsteps = np.array(nsteps, dtype=int) + 1
self._step_vector = np.array(step_vector)
self._origin = np.array(origin)
Expand Down
4 changes: 2 additions & 2 deletions LoopStructural/modelling/core/geological_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1417,7 +1417,7 @@ def create_and_add_fault(
return fault

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

# TODO move scale to bounding box/transformer
def scale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
"""Take points in UTM coordinates and reproject
into scaled model space
Expand Down
1 change: 1 addition & 0 deletions LoopStructural/modelling/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ class FeatureType(IntEnum):

from ._unconformity_feature import UnconformityFeature
from ._analytical_feature import AnalyticalGeologicalFeature
from ._projected_vector_feature import ProjectedVectorFeature
25 changes: 23 additions & 2 deletions LoopStructural/modelling/features/_analytical_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ def __init__(
builder=None,
):
BaseFeature.__init__(self, name, model, faults, regions, builder)
self.vector = np.array(vector, dtype=float)
self.origin = np.array(origin, dtype=float)
try:
self.vector = np.array(vector, dtype=float).reshape(3)
except ValueError:
logger.error("AnalyticalGeologicalFeature: vector must be a 3 element array")
raise ValueError("vector must be a 3 element array")
try:
self.origin = np.array(origin, dtype=float).reshape(3)
except ValueError:
logger.error("AnalyticalGeologicalFeature: origin must be a 3 element array")
raise ValueError("origin must be a 3 element array")
self.type = FeatureType.ANALYTICAL

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

def get_data(self, value_map: Optional[dict] = None):
return

def copy(self, name: Optional[str] = None):
if name is None:
name = self.name
return AnalyticalGeologicalFeature(
name,
self.vector.copy(),
self.origin.copy(),
list(self.regions),
list(self.faults),
self.model,
self.builder,
)
18 changes: 17 additions & 1 deletion LoopStructural/modelling/features/_base_geological_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,12 @@ def surfaces(
self.regions = [
r for r in self.regions if r.name != self.name and r.parent.name != self.name
]
callable = lambda xyz: self.evaluate_value(self.model.scale(xyz))

callable = lambda xyz: (
self.evaluate_value(self.model.scale(xyz))
if self.model is not None
else self.evaluate_value(xyz)
)
isosurfacer = LoopIsosurfacer(bounding_box, callable=callable)
if name is None and self.name is not None:
name = self.name
Expand Down Expand Up @@ -375,3 +380,14 @@ def get_data(self, value_map: Optional[dict] = None):
dictionary of data
"""
raise NotImplementedError

@abstractmethod
def copy(self, name: Optional[str] = None):
"""Copy the feature
Returns
-------
BaseFeature
copied feature
"""
raise NotImplementedError
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,10 @@ def max(self):

def get_data(self, value_map: Optional[dict] = None):
return

def copy(self, name: Optional[str] = None):
if name is None:
name = f'{self.name}_copy'
return CrossProductGeologicalFeature(
name, self.geological_feature_a, self.geological_feature_b
)
4 changes: 3 additions & 1 deletion LoopStructural/modelling/features/_geological_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ def evaluate_value(self, pos: np.ndarray, ignore_regions=False, fillnan=None) ->
# if evaluation_points is not a numpy array try and convert
# otherwise error
evaluation_points = np.asarray(pos)
self.builder.up_to_date()
# if there is a builder lets make sure that the feature is up to date
if self.builder is not None:
self.builder.up_to_date()
# check if the points are within the display region
v = np.zeros(evaluation_points.shape[0])
v[:] = np.nan
Expand Down
Loading

0 comments on commit 58b211f

Please sign in to comment.