Skip to content

Commit 4b9f90f

Browse files
Fix/docs and cleanup (#217)
* docs: add pyvista plot directives and update some documentations * fix: interpolatorbuilder syntax/method naming * fix: bb global origin should be 0 if it is not being used. force origin/max to be numpy arrays. Change default ordering of regular grid to be fortran renaming centers to centres for english adding cell centres to structure grid/nodes * fix: allow fitting of rotation to be disabled for the euclidean transformation and ensure dimensionality is used * update copy of points Co-authored-by: Copilot <[email protected]> * Add Optional back Co-authored-by: Copilot <[email protected]> * fix: geoh5 format for grids * fix: adding spatially varying regularisation for fdi * fix: interpolator builder use bounding box geometry if no other mesh details are given * style: style fixes by ruff and autoformatting by black * fix: change nelements for interpolator using builder kwargs * style: black * style: remove unused imports * fix: allowing nelements to be updated for an interpolator * fix: allow small structured grid. needed for tetra * fix: adding distance to bounding box * style: style fixes by ruff and autoformatting by black * style: style fixes by ruff and autoformatting by black * fix: don't scale fdi regularisation * fix: add helper to get interpolator support as vtk with solution as node values * fix: if interpolation geometry changed, make sure build args are updated * style: style fixes by ruff and autoformatting by black * style: black autoformat * style: style fixes by ruff and autoformatting by black * tests: add fdi test for structural frame and make isclose less sensitive * tests: reducing sensitivity further... --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: lachlangrose <[email protected]>
1 parent dc4158b commit 4b9f90f

39 files changed

+815
-212
lines changed

LoopStructural/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
loggers = {}
2121
from .modelling.core.geological_model import GeologicalModel
2222
from .interpolators._api import LoopInterpolator
23+
from .interpolators import InterpolatorBuilder
2324
from .datatypes import BoundingBox
2425
from .utils import log_to_console, log_to_file, getLogger, rng, get_levels
2526

LoopStructural/datatypes/_bounding_box.py

+47-10
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(
4343
if maximum is None and nsteps is not None and step_vector is not None:
4444
maximum = origin + nsteps * step_vector
4545
if origin is not None and global_origin is None:
46-
global_origin = origin
46+
global_origin = np.zeros(3)
4747
self._origin = np.array(origin)
4848
self._maximum = np.array(maximum)
4949
self.dimensions = dimensions
@@ -90,7 +90,7 @@ def global_origin(self, global_origin):
9090

9191
@property
9292
def global_maximum(self):
93-
return self.maximum - self.origin + self._global_origin
93+
return self.maximum + self.global_origin
9494

9595
@property
9696
def valid(self):
@@ -242,6 +242,8 @@ def fit(self, locations: np.ndarray, local_coordinate: bool = False) -> Bounding
242242
)
243243
origin = locations.min(axis=0)
244244
maximum = locations.max(axis=0)
245+
origin = np.array(origin)
246+
maximum = np.array(maximum)
245247
if local_coordinate:
246248
self.global_origin = origin
247249
self.origin = np.zeros(3)
@@ -273,15 +275,50 @@ def with_buffer(self, buffer: float = 0.2) -> BoundingBox:
273275
if self.origin is None or self.maximum is None:
274276
raise LoopValueError("Cannot create bounding box with buffer, no origin or maximum")
275277
# local coordinates, rescale into the original bounding boxes global coordinates
276-
origin = self.origin - buffer * (self.maximum - self.origin)
277-
maximum = self.maximum + buffer * (self.maximum - self.origin)
278+
origin = self.origin - buffer * np.max(self.maximum - self.origin)
279+
maximum = self.maximum + buffer * np.max(self.maximum - self.origin)
278280
return BoundingBox(
279281
origin=origin,
280282
maximum=maximum,
281-
global_origin=self.global_origin + origin,
283+
global_origin=self.global_origin,
282284
dimensions=self.dimensions,
283285
)
284286

287+
# def __call__(self, xyz):
288+
# xyz = np.array(xyz)
289+
# if len(xyz.shape) == 1:
290+
# xyz = xyz.reshape((1, -1))
291+
292+
# distances = np.maximum(0,
293+
# np.maximum(self.global_origin+self.origin - xyz,
294+
# xyz - self.global_maximum))
295+
# distance = np.linalg.norm(distances, axis=1)
296+
# distance[self.is_inside(xyz)] = -1
297+
# return distance
298+
299+
def __call__(self, xyz):
300+
# Calculate center and half-extents of the box
301+
center = (self.maximum + self.global_origin + self.origin) / 2
302+
half_extents = (self.maximum - self.global_origin + self.origin) / 2
303+
304+
# Calculate the distance from point to center
305+
offset = np.abs(xyz - center) - half_extents
306+
307+
# Inside distance: negative value based on the smallest penetration
308+
inside_distance = np.min(half_extents - np.abs(xyz - center), axis=1)
309+
310+
# Outside distance: length of the positive components of offset
311+
outside_distance = np.linalg.norm(np.maximum(offset, 0))
312+
313+
# If any component of offset is positive, we're outside
314+
# Otherwise, we're inside and return the negative penetration distance
315+
distance = np.zeros(xyz.shape[0])
316+
mask = np.any(offset > 0, axis=1)
317+
distance[mask] = outside_distance
318+
distance[~mask] = -inside_distance[~mask]
319+
return distance
320+
# return outside_distance if np.any(offset > 0) else -inside_distance
321+
285322
def get_value(self, name):
286323
ix, iy = self.name_map.get(name, (-1, -1))
287324
if ix == -1 and iy == -1:
@@ -319,7 +356,7 @@ def regular_grid(
319356
self,
320357
nsteps: Optional[Union[list, np.ndarray]] = None,
321358
shuffle: bool = False,
322-
order: str = "C",
359+
order: str = "F",
323360
local: bool = True,
324361
) -> np.ndarray:
325362
"""Get the grid of points from the bounding box
@@ -361,8 +398,8 @@ def regular_grid(
361398
rng.shuffle(locs)
362399
return locs
363400

364-
def cell_centers(self, order: str = "F") -> np.ndarray:
365-
"""Get the cell centers of a regular grid
401+
def cell_centres(self, order: str = "F") -> np.ndarray:
402+
"""Get the cell centres of a regular grid
366403
367404
Parameters
368405
----------
@@ -372,7 +409,7 @@ def cell_centers(self, order: str = "F") -> np.ndarray:
372409
Returns
373410
-------
374411
np.ndarray
375-
array of cell centers
412+
array of cell centres
376413
"""
377414
locs = self.regular_grid(order=order, nsteps=self.nsteps - 1)
378415

@@ -434,7 +471,7 @@ def structured_grid(
434471
_cell_data = copy.deepcopy(cell_data)
435472
_vertex_data = copy.deepcopy(vertex_data)
436473
return StructuredGrid(
437-
origin=self.global_origin,
474+
origin=self.global_origin + self.origin,
438475
step_vector=self.step_vector,
439476
nsteps=self.nsteps,
440477
cell_properties=_cell_data,

LoopStructural/datatypes/_structured_grid.py

+30-2
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def vtk(self):
4444
z,
4545
)
4646
for name, data in self.properties.items():
47-
grid[name] = data.flatten(order="F")
47+
grid[name] = data.reshape((grid.n_points, -1), order="F")
4848
for name, data in self.cell_properties.items():
49-
grid.cell_data[name] = data.flatten(order="F")
49+
grid.cell_data[name] = data.reshape((grid.n_cells, -1), order="F")
5050
return grid
5151

5252
def plot(self, pyvista_kwargs={}):
@@ -63,6 +63,34 @@ def plot(self, pyvista_kwargs={}):
6363
except ImportError:
6464
logger.error("pyvista is required for vtk")
6565

66+
@property
67+
def cell_centres(self):
68+
x = np.linspace(
69+
self.origin[0] + self.step_vector[0] * 0.5,
70+
self.maximum[0] + self.step_vector[0] * 0.5,
71+
self.nsteps[0] - 1,
72+
)
73+
y = np.linspace(
74+
self.origin[1] + self.step_vector[1] * 0.5,
75+
self.maximum[1] - self.step_vector[1] * 0.5,
76+
self.nsteps[1] - 1,
77+
)
78+
z = np.linspace(
79+
self.origin[2] + self.step_vector[2] * 0.5,
80+
self.maximum[2] - self.step_vector[2] * 0.5,
81+
self.nsteps[2] - 1,
82+
)
83+
x, y, z = np.meshgrid(x, y, z, indexing="ij")
84+
return np.vstack([x.flatten(order='f'), y.flatten(order='f'), z.flatten(order='f')]).T
85+
86+
@property
87+
def nodes(self):
88+
x = np.linspace(self.origin[0], self.maximum[0], self.nsteps[0])
89+
y = np.linspace(self.origin[1], self.maximum[1], self.nsteps[1])
90+
z = np.linspace(self.origin[2], self.maximum[2], self.nsteps[2])
91+
x, y, z = np.meshgrid(x, y, z, indexing="ij")
92+
return np.vstack([x.flatten(order='f'), y.flatten(order='f'), z.flatten(order='f')]).T
93+
6694
def merge(self, other):
6795
if not np.all(np.isclose(self.origin, other.origin)):
6896
raise ValueError("Origin of grids must be the same")

LoopStructural/interpolators/_discrete_interpolator.py

+18
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ def __init__(self, support, data={}, c=None, up_to_date=False):
6363
logger.info("Creating discrete interpolator with {} degrees of freedom".format(self.nx))
6464
self.type = InterpolatorType.BASE_DISCRETE
6565

66+
def set_nelements(self, nelements: int) -> int:
67+
return self.support.set_nelements(nelements)
68+
69+
@property
70+
def n_elements(self) -> int:
71+
"""Number of elements in the interpolator
72+
73+
Returns
74+
-------
75+
int
76+
number of elements, positive
77+
"""
78+
return self.support.n_elements
79+
6680
@property
6781
def nx(self) -> int:
6882
"""Number of degrees of freedom for the interpolator
@@ -161,6 +175,7 @@ def reset(self):
161175
"""
162176
self.constraints = {}
163177
self.c_ = 0
178+
self.regularisation_scale = np.ones(self.nx)
164179
logger.info("Resetting interpolation constraints")
165180

166181
def add_constraints_to_least_squares(self, A, B, idc, w=1.0, name="undefined"):
@@ -737,3 +752,6 @@ def to_dict(self):
737752
**super().to_dict(),
738753
# 'region_function':self.region_function,
739754
}
755+
756+
def vtk(self):
757+
return self.support.vtk({'c': self.c})

LoopStructural/interpolators/_finite_difference_interpolator.py

+64-11
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,37 @@
77
from ..utils import get_vectors
88
from ._discrete_interpolator import DiscreteInterpolator
99
from ..interpolators import InterpolatorType
10-
10+
from scipy.spatial import KDTree
1111
from LoopStructural.utils import getLogger
1212

1313
logger = getLogger(__name__)
1414

1515

16+
def compute_weighting(grid_points, gradient_constraint_points, alpha=10.0, sigma=1.0):
17+
"""
18+
Compute weights for second derivative regularization based on proximity to gradient constraints.
19+
20+
Parameters:
21+
grid_points (ndarray): (N, 3) array of 3D coordinates for grid cells.
22+
gradient_constraint_points (ndarray): (M, 3) array of 3D coordinates for gradient constraints.
23+
alpha (float): Strength of weighting increase.
24+
sigma (float): Decay parameter for Gaussian-like influence.
25+
26+
Returns:
27+
weights (ndarray): (N,) array of weights for each grid point.
28+
"""
29+
# Build a KDTree with the gradient constraint locations
30+
tree = KDTree(gradient_constraint_points)
31+
32+
# Find the distance from each grid point to the nearest gradient constraint
33+
distances, _ = tree.query(grid_points, k=1)
34+
35+
# Compute weighting function (higher weight for nearby points)
36+
weights = 1 + alpha * np.exp(-(distances**2) / (2 * sigma**2))
37+
38+
return weights
39+
40+
1641
class FiniteDifferenceInterpolator(DiscreteInterpolator):
1742
def __init__(self, grid, data={}):
1843
"""
@@ -44,6 +69,7 @@ def __init__(self, grid, data={}):
4469
)
4570

4671
self.type = InterpolatorType.FINITE_DIFFERENCE
72+
self.use_regularisation_weight_scale = False
4773

4874
def setup_interpolator(self, **kwargs):
4975
"""
@@ -76,20 +102,19 @@ def setup_interpolator(self, **kwargs):
76102
for key in kwargs:
77103
self.up_to_date = False
78104
if "regularisation" in kwargs:
79-
self.interpolation_weights["dxy"] = 0.1 * kwargs["regularisation"]
80-
self.interpolation_weights["dyz"] = 0.1 * kwargs["regularisation"]
81-
self.interpolation_weights["dxz"] = 0.1 * kwargs["regularisation"]
82-
self.interpolation_weights["dxx"] = 0.1 * kwargs["regularisation"]
83-
self.interpolation_weights["dyy"] = 0.1 * kwargs["regularisation"]
84-
self.interpolation_weights["dzz"] = 0.1 * kwargs["regularisation"]
105+
self.interpolation_weights["dxy"] = kwargs["regularisation"]
106+
self.interpolation_weights["dyz"] = kwargs["regularisation"]
107+
self.interpolation_weights["dxz"] = kwargs["regularisation"]
108+
self.interpolation_weights["dxx"] = kwargs["regularisation"]
109+
self.interpolation_weights["dyy"] = kwargs["regularisation"]
110+
self.interpolation_weights["dzz"] = kwargs["regularisation"]
85111
self.interpolation_weights[key] = kwargs[key]
86112
# either use the default operators or the ones passed to the function
87113
operators = kwargs.get(
88114
"operators", self.support.get_operators(weights=self.interpolation_weights)
89115
)
90-
for k, o in operators.items():
91-
self.assemble_inner(o[0], o[1], name=k)
92116

117+
self.use_regularisation_weight_scale = kwargs.get('use_regularisation_weight_scale', False)
93118
self.add_norm_constraints(self.interpolation_weights["npw"])
94119
self.add_gradient_constraints(self.interpolation_weights["gpw"])
95120
self.add_value_constraints(self.interpolation_weights["cpw"])
@@ -101,6 +126,8 @@ def setup_interpolator(self, **kwargs):
101126
upper_bound=kwargs.get('inequality_pair_upper_bound', np.finfo(float).eps),
102127
lower_bound=kwargs.get('inequality_pair_lower_bound', -np.inf),
103128
)
129+
for k, o in operators.items():
130+
self.assemble_inner(o[0], o[1], name=k)
104131

105132
def copy(self):
106133
"""
@@ -271,6 +298,11 @@ def add_gradient_constraints(self, w=1.0):
271298
self.add_constraints_to_least_squares(A, B, idc[inside, :], w=w, name="gradient")
272299
A = np.einsum("ij,ijk->ik", dip_vector.T, T)
273300
self.add_constraints_to_least_squares(A, B, idc[inside, :], w=w, name="gradient")
301+
# self.regularisation_scale += compute_weighting(
302+
# self.support.nodes,
303+
# points[inside, : self.support.dimension],
304+
# sigma=self.support.nsteps[0] * 10,
305+
# )
274306
if np.sum(inside) <= 0:
275307
logger.warning(
276308
f" {np.sum(~inside)} \
@@ -318,7 +350,24 @@ def add_norm_constraints(self, w=1.0):
318350
)
319351
# T*=np.product(self.support.step_vector)
320352
# T/=self.support.step_vector[0]
321-
353+
# indexes, inside2 = self.support.position_to_nearby_cell_indexes(
354+
# points[inside, : self.support.dimension]
355+
# )
356+
# indexes = indexes[inside2, :]
357+
358+
# corners = self.support.cell_corner_indexes(indexes)
359+
# node_indexes = corners.reshape(-1, 3)
360+
# indexes = self.support.global_node_indices(indexes)
361+
# self.regularisation_scale[indexes] =10
362+
363+
self.regularisation_scale += compute_weighting(
364+
self.support.nodes,
365+
points[inside, : self.support.dimension],
366+
sigma=self.support.nsteps[0] * 10,
367+
)
368+
# global_indexes = self.support.neighbour_global_indexes().T.astype(int)
369+
# close_indexes =
370+
# self.regularisation_scale[global_indexes[idc[inside,:].astype(int),]]=10
322371
w /= 3
323372
for d in range(self.support.dimension):
324373

@@ -454,7 +503,11 @@ def assemble_inner(self, operator, w, name='regularisation'):
454503
a[inside, :],
455504
B[inside],
456505
idc[inside, :],
457-
w=w,
506+
w=(
507+
self.regularisation_scale[idc[inside, 13].astype(int)] * w
508+
if self.use_regularisation_weight_scale
509+
else w
510+
),
458511
name=name,
459512
)
460513
return

LoopStructural/interpolators/_geological_interpolator.py

+9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ def __init__(self, data={}, up_to_date=False):
4646
self.dimensions = 3 # default to 3d
4747
self.support = None
4848

49+
@abstractmethod
50+
def set_nelements(self, nelements: int) -> int:
51+
pass
52+
53+
@property
54+
@abstractmethod
55+
def n_elements(self) -> int:
56+
pass
57+
4958
@property
5059
def data(self):
5160
return self._data

0 commit comments

Comments
 (0)