Skip to content

Commit 58cb00f

Browse files
authored
Merge branch 'master' into fix/stratigraphic-column
2 parents e42cf35 + 66a6f1d commit 58cb00f

19 files changed

Lines changed: 405 additions & 75 deletions

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"LoopStructural": "1.6.14"
2+
"LoopStructural": "1.6.15"
33
}

LoopStructural/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [1.6.15](https://github.com/Loop3D/LoopStructural/compare/v1.6.14...v1.6.15) (2025-07-08)
4+
5+
6+
### Bug Fixes
7+
8+
* add option to pass a dataframe directly to the create and add methods ([f756054](https://github.com/Loop3D/LoopStructural/commit/f7560545048331137f35172ebd1324895f12faf6))
9+
* add parameter separator to clean up api ([7d8b0b1](https://github.com/Loop3D/LoopStructural/commit/7d8b0b1c2f0c30b4d9119d9438de3ded44b04bd8))
10+
* add visualisation to plot gradient norm ([e774f95](https://github.com/Loop3D/LoopStructural/commit/e774f95934b8fbec84ff37e73a098d36c6620f23))
11+
* adding constant norm interpolators ([29570f1](https://github.com/Loop3D/LoopStructural/commit/29570f1543f2a4aa04efda2fb4d894a7dc1ed9bf))
12+
* allow data to be specified in create and add function. ([f6db4a5](https://github.com/Loop3D/LoopStructural/commit/f6db4a5b6aabc75b7a061014b93288ec91791c57))
13+
* update vector scaling ([0a1e18e](https://github.com/Loop3D/LoopStructural/commit/0a1e18e9740ab8fc72fe2a27231bf6dc8fe2fece))
14+
* updating bounding box project/reproject to just translate to origin ([7606864](https://github.com/Loop3D/LoopStructural/commit/760686432710c92ec2653cafe226012c64c24551))
15+
316
## [1.6.14](https://github.com/Loop3D/LoopStructural/compare/v1.6.13...v1.6.14) (2025-05-28)
417

518

LoopStructural/datatypes/_point.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ def vtk(
157157
try:
158158
locations = bb.project(locations)
159159
_projected = True
160-
scale = bb.scale_by_projection_factor(scale)
161160
except Exception as e:
162161
logger.error(f'Failed to project points to bounding box: {e}')
163162
logger.error('Using unprojected points, this may cause issues with the glyphing')

LoopStructural/interpolators/__init__.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
44
"""
55

6+
67
__all__ = [
78
"InterpolatorType",
89
"GeologicalInterpolator",
@@ -21,39 +22,12 @@
2122
"StructuredGrid2D",
2223
"P2UnstructuredTetMesh",
2324
]
24-
from enum import IntEnum
25+
from ._interpolatortype import InterpolatorType
2526

2627
from ..utils import getLogger
2728

2829
logger = getLogger(__name__)
2930

30-
31-
class InterpolatorType(IntEnum):
32-
"""
33-
Enum for the different interpolator types
34-
35-
1-9 should cover interpolators with supports
36-
9+ are data supported
37-
"""
38-
39-
BASE = 0
40-
BASE_DISCRETE = 1
41-
FINITE_DIFFERENCE = 2
42-
DISCRETE_FOLD = 3
43-
PIECEWISE_LINEAR = 4
44-
PIECEWISE_QUADRATIC = 5
45-
BASE_DATA_SUPPORTED = 10
46-
SURFE = 11
47-
48-
49-
interpolator_string_map = {
50-
"FDI": InterpolatorType.FINITE_DIFFERENCE,
51-
"PLI": InterpolatorType.PIECEWISE_LINEAR,
52-
"P2": InterpolatorType.PIECEWISE_QUADRATIC,
53-
"P1": InterpolatorType.PIECEWISE_LINEAR,
54-
"DFI": InterpolatorType.DISCRETE_FOLD,
55-
'surfe': InterpolatorType.SURFE,
56-
}
5731
from ..interpolators._geological_interpolator import GeologicalInterpolator
5832
from ..interpolators._discrete_interpolator import DiscreteInterpolator
5933
from ..interpolators.supports import (
@@ -79,7 +53,7 @@ class InterpolatorType(IntEnum):
7953
)
8054
from ..interpolators._p2interpolator import P2Interpolator
8155
from ..interpolators._p1interpolator import P1Interpolator
82-
56+
from ..interpolators._constant_norm import ConstantNormP1Interpolator, ConstantNormFDIInterpolator
8357
try:
8458
from ..interpolators._surfe_wrapper import SurfeRBFInterpolator
8559
except ImportError:
@@ -93,6 +67,24 @@ def __init__(self, *args, **kwargs):
9367
raise ImportError(
9468
"Surfe cannot be imported. Please install Surfe. pip install surfe/ conda install -c loop3d surfe"
9569
)
70+
71+
# Ensure compatibility between the fallback and imported class
72+
SurfeRBFInterpolator = SurfeRBFInterpolator
73+
74+
75+
interpolator_string_map = {
76+
"FDI": InterpolatorType.FINITE_DIFFERENCE,
77+
"PLI": InterpolatorType.PIECEWISE_LINEAR,
78+
"P2": InterpolatorType.PIECEWISE_QUADRATIC,
79+
"P1": InterpolatorType.PIECEWISE_LINEAR,
80+
"DFI": InterpolatorType.DISCRETE_FOLD,
81+
'surfe': InterpolatorType.SURFE,
82+
"FDI_CN": InterpolatorType.FINITE_DIFFERENCE_CONSTANT_NORM,
83+
"P1_CN": InterpolatorType.PIECEWISE_LINEAR_CONSTANT_NORM,
84+
85+
}
86+
87+
# Define the mapping after all imports
9688
interpolator_map = {
9789
InterpolatorType.BASE: GeologicalInterpolator,
9890
InterpolatorType.BASE_DISCRETE: DiscreteInterpolator,
@@ -102,6 +94,8 @@ def __init__(self, *args, **kwargs):
10294
InterpolatorType.PIECEWISE_QUADRATIC: P2Interpolator,
10395
InterpolatorType.BASE_DATA_SUPPORTED: GeologicalInterpolator,
10496
InterpolatorType.SURFE: SurfeRBFInterpolator,
97+
InterpolatorType.PIECEWISE_LINEAR_CONSTANT_NORM: ConstantNormP1Interpolator,
98+
InterpolatorType.FINITE_DIFFERENCE_CONSTANT_NORM: ConstantNormFDIInterpolator,
10599
}
106100

107101
support_interpolator_map = {
@@ -119,9 +113,18 @@ def __init__(self, *args, **kwargs):
119113
3: SupportType.DataSupported,
120114
2: SupportType.DataSupported,
121115
},
116+
InterpolatorType.PIECEWISE_LINEAR_CONSTANT_NORM:{
117+
3: SupportType.TetMesh,
118+
2: SupportType.P1Unstructured2d,
119+
},
120+
InterpolatorType.FINITE_DIFFERENCE_CONSTANT_NORM: {
121+
3: SupportType.StructuredGrid,
122+
2: SupportType.StructuredGrid2D,
123+
}
122124
}
123125

124126
from ._interpolator_factory import InterpolatorFactory
125127
from ._interpolator_builder import InterpolatorBuilder
126128

127-
# from ._api import LoopInterpolator
129+
130+
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import numpy as np
2+
3+
from LoopStructural.interpolators._discrete_interpolator import DiscreteInterpolator
4+
from LoopStructural.interpolators._finite_difference_interpolator import FiniteDifferenceInterpolator
5+
from ._p1interpolator import P1Interpolator
6+
from typing import Optional, Union, Callable
7+
from scipy import sparse
8+
from LoopStructural.utils import rng
9+
10+
class ConstantNormInterpolator:
11+
"""Adds a non linear constraint to an interpolator to constrain
12+
the norm of the gradient to be a set value.
13+
14+
Returns
15+
-------
16+
_type_
17+
_description_
18+
"""
19+
def __init__(self, interpolator: DiscreteInterpolator,basetype):
20+
"""Initialise the constant norm inteprolator
21+
with a discrete interpolator.
22+
23+
Parameters
24+
----------
25+
interpolator : DiscreteInterpolator
26+
The discrete interpolator to add constant norm to.
27+
"""
28+
self.basetype = basetype
29+
self.interpolator = interpolator
30+
self.support = interpolator.support
31+
self.random_subset = False
32+
self.norm_length = 1.0
33+
self.n_iterations = 20
34+
self.store_solution_history = False
35+
self.solution_history = []#np.zeros((self.n_iterations, self.support.n_nodes))
36+
self.gradient_constraint_store = []
37+
def add_constant_norm(self, w:float):
38+
"""Add a constraint to the interpolator to constrain the norm of the gradient
39+
to be a set value
40+
41+
Parameters
42+
----------
43+
w : float
44+
weighting of the constraint
45+
"""
46+
if "constant norm" in self.interpolator.constraints:
47+
_ = self.interpolator.constraints.pop("constant norm")
48+
49+
element_indices = np.arange(self.support.elements.shape[0])
50+
if self.random_subset:
51+
rng.shuffle(element_indices)
52+
element_indices = element_indices[: int(0.1 * self.support.elements.shape[0])]
53+
vertices, gradient, elements, inside = self.support.get_element_gradient_for_location(
54+
self.support.barycentre[element_indices]
55+
)
56+
57+
t_g = gradient[:, :, :]
58+
# t_n = gradient[self.support.shared_element_relationships[:, 1], :, :]
59+
v_t = np.einsum(
60+
"ijk,ik->ij",
61+
t_g,
62+
self.interpolator.c[self.support.elements[elements]],
63+
)
64+
65+
v_t = v_t / np.linalg.norm(v_t, axis=1)[:, np.newaxis]
66+
self.gradient_constraint_store.append(np.hstack([self.support.barycentre[element_indices],v_t]))
67+
A1 = np.einsum("ij,ijk->ik", v_t, t_g)
68+
volume = self.support.element_size[element_indices]
69+
A1 = A1 / volume[:, np.newaxis] # normalise by element size
70+
71+
b = np.zeros(A1.shape[0]) + self.norm_length
72+
b = b / volume # normalise by element size
73+
idc = np.hstack(
74+
[
75+
self.support.elements[elements],
76+
]
77+
)
78+
self.interpolator.add_constraints_to_least_squares(A1, b, idc, w=w, name="constant norm")
79+
80+
def solve_system(
81+
self,
82+
solver: Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]] = None,
83+
tol: Optional[float] = None,
84+
solver_kwargs: dict = {},
85+
) -> bool:
86+
"""Solve the system of equations iteratively for the constant norm interpolator.
87+
88+
Parameters
89+
----------
90+
solver : Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]], optional
91+
Solver function or name, by default None
92+
tol : Optional[float], optional
93+
Tolerance for the solver, by default None
94+
solver_kwargs : dict, optional
95+
Additional arguments for the solver, by default {}
96+
97+
Returns
98+
-------
99+
bool
100+
Success status of the solver
101+
"""
102+
success = True
103+
for i in range(self.n_iterations):
104+
if i > 0:
105+
self.add_constant_norm(w=(0.1 * i) ** 2 + 0.01)
106+
# Ensure the interpolator is cast to P1Interpolator before calling solve_system
107+
if isinstance(self.interpolator, self.basetype):
108+
success = self.basetype.solve_system(self.interpolator, solver=solver, tol=tol, solver_kwargs=solver_kwargs)
109+
if self.store_solution_history:
110+
111+
self.solution_history.append(self.interpolator.c)
112+
else:
113+
raise TypeError("self.interpolator is not an instance of P1Interpolator")
114+
if not success:
115+
break
116+
return success
117+
118+
class ConstantNormP1Interpolator(P1Interpolator, ConstantNormInterpolator):
119+
"""Constant norm interpolator using P1 base interpolator
120+
121+
Parameters
122+
----------
123+
P1Interpolator : class
124+
The P1Interpolator class.
125+
ConstantNormInterpolator : class
126+
The ConstantNormInterpolator class.
127+
"""
128+
def __init__(self, support):
129+
"""Initialise the constant norm P1 interpolator.
130+
131+
Parameters
132+
----------
133+
support : _type_
134+
_description_
135+
"""
136+
P1Interpolator.__init__(self, support)
137+
ConstantNormInterpolator.__init__(self, self, P1Interpolator)
138+
139+
def solve_system(
140+
self,
141+
solver: Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]] = None,
142+
tol: Optional[float] = None,
143+
solver_kwargs: dict = {},
144+
) -> bool:
145+
"""Solve the system of equations for the constant norm P1 interpolator.
146+
147+
Parameters
148+
----------
149+
solver : Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]], optional
150+
Solver function or name, by default None
151+
tol : Optional[float], optional
152+
Tolerance for the solver, by default None
153+
solver_kwargs : dict, optional
154+
Additional arguments for the solver, by default {}
155+
156+
Returns
157+
-------
158+
bool
159+
Success status of the solver
160+
"""
161+
return ConstantNormInterpolator.solve_system(self, solver=solver, tol=tol, solver_kwargs=solver_kwargs)
162+
163+
class ConstantNormFDIInterpolator(FiniteDifferenceInterpolator, ConstantNormInterpolator):
164+
"""Constant norm interpolator using finite difference base interpolator
165+
166+
Parameters
167+
----------
168+
FiniteDifferenceInterpolator : class
169+
The FiniteDifferenceInterpolator class.
170+
ConstantNormInterpolator : class
171+
The ConstantNormInterpolator class.
172+
"""
173+
def __init__(self, support):
174+
"""Initialise the constant norm finite difference interpolator.
175+
176+
Parameters
177+
----------
178+
support : _type_
179+
_description_
180+
"""
181+
FiniteDifferenceInterpolator.__init__(self, support)
182+
ConstantNormInterpolator.__init__(self, self, FiniteDifferenceInterpolator)
183+
def solve_system(
184+
self,
185+
solver: Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]] = None,
186+
tol: Optional[float] = None,
187+
solver_kwargs: dict = {},
188+
) -> bool:
189+
"""Solve the system of equations for the constant norm finite difference interpolator.
190+
191+
Parameters
192+
----------
193+
solver : Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]], optional
194+
Solver function or name, by default None
195+
tol : Optional[float], optional
196+
Tolerance for the solver, by default None
197+
solver_kwargs : dict, optional
198+
Additional arguments for the solver, by default {}
199+
200+
Returns
201+
-------
202+
bool
203+
Success status of the solver
204+
"""
205+
return ConstantNormInterpolator.solve_system(self, solver=solver, tol=tol, solver_kwargs=solver_kwargs)

0 commit comments

Comments
 (0)