Skip to content

Commit 5701100

Browse files
2d interpolation (#176)
* fix: adding json exports * fix: moving and renaming conversion functions also adding some type hints and a generic numeric type * tests: adding tests for conversions need to add some better tests not just 0 and 1s * fix: remove process_map2loop not required, can be done using processor * fix: adding synthetic horizontal layers model * fix: adding corners property to bounding box * fix: substituting pli for p1 same interpolator but this implementation uses numpy instead of cython * tests: test that horizontal layers are working * fix: making structured tetra compatible with p1 interp * fix: making p1 interpolator work * fix: cast input to np array * fix: adding type enums * fix: adding sparse matrix neighbour calculator this means no more cython code * fix: adding some type hints * refactor: ➖ removing model builder there is no real advantage to using a builder design pattern for loopstructural * fix: ✨ adding ability to calculate normalised value of a feature this just makes it easier to scale scalar field between 0 and 1 * fix: adding map for interpolator to support type * fix: enabling support builder with interpolator factory * fix: updating imports for maths functions * fix: adding support creator from bbox * fix: when reset clear the constraints dictionary * fix: reset when setting up interpolator * fix: removing old pli interpolator * fix: fixing tetra indexing and masks * fix: removing unused imports * fix: speeding up element getter * fix: adding onGeometryChange function for supports * fix: reset change number of constraints * fix: adding gradient orthogonal to p1interpolator * fix: changing instrusion example to rescale points * fix: setting model tolerance using new bounding box * fix: updating fold to use p1 interpolator * fix: renaming variables * fix: adding name and vector args to min edge jumps * fix: removing old imports * fix: fixing orthogonal constraints for p1 * fix: moving fault setup into fault builder frm model * fix: flake8 error * fix: add bb origin to surfaces * fix: fold constraints added in setup interpolator this allows for interpolator to be reset and doesn't remove the fold constraints when resetting * fix: temp fix for issue with np types * Replace reqs with correct theme Theme being loaded by requirements.txt was wrong (an old one?) So have updated with the pydata_sphinx_theme so now builds correctly * Adding PyPI custom link to navigation * Replacing square icon with (nicer) round icon * Adding navbar options and metadata Image is a placeholder for now, but this is an (onvs opinionated) view on a nicer format. Adds a limit to the number of nav items, adds a logo, changes the title to look cleaner (and not say "documentation" at the end), adds metadata * Fixing svg img * Updating the contributors docs * removing accidental tab * docs: fixing missing automodule * fix: adding length to bbox * fix: removing dof and name from interpolator json * docs: adding explicit imports for submodules Without explicit import of submodules and using __ALL__ the majority of the code isn't imported so sphinx doesn't document it. * docs: adding citations to documentation * fix: removing reference to dtm creator and using get interpolator correctly * fix: face table resize when geometry reset * fix: elements property for base structured support * fix: adding imports to api * fix: changing from interpolator type string to enum * fix: removing cython * fix: only axial foliation is constrained by fold frame * Update release-please.yml * Update release-please.yml * Update release-please.yml * ci: updating to use pip wheel . * Update release-please.yml * fix: disable progress bar for model update * ci: remove numpy version for conda * fix: changing to interpolator factory required changing how fold interpolator is setup. Rather than initialising with a fold, the fold is added to the interpolator added map between stringinterpolator names and the interpolatortypes enum * fix: identify points that fall on face of shared tetras. Automatically chooses the second one. * fix: rename surface vtk property to vtk from pyvista * fix: add docstring and cast isinside to an array * docs: adding docs to surface datatype * fix: removing cython code * fix: removing model api for time being * build: adding conda builds to git ignore * typo * ci: using conda convert to create osx * ci: removing cython * ci: typo in yml * ci: again * ci: again * fix: removing analysis module, will create separate ptyhon library * style: style fixes by ruff and autoformatting by black * fix: changing import of bounding box * fix: migrating np.random.shuffle to use generator object * fix: removing axis/angle from apply faults * fix: calculate gradient of faulted feature returns true gradient * fix: bb import update and some formatting * style: style fixes by ruff and autoformatting by black * fix: adding a method to rotate vectors that are faulted * fix: store constraints as matrix rather than dict the matrices can then be stacked together. no real advantage to the previous approach. It is useful if you want to use the matrix for a constraint outside of the typical LS workflow. * fix: removing unused variable * fix: adding --user flag and removing cython from test suite * fix: changing discrete support to return element index previously there was a mix of returning the index or the cells in the index. making supports consistent * fix: global indexes for elements * fix: linting * style: style fixes by ruff and autoformatting by black * fix: changing bounding box import * ci: updating action version numbers * fix: moving face table and aabb to separate file * fix: adding abc for base support * fix: updating 2d grid to follow same pattern as 3d * fix: updating 2d tri mesh to same pattern as 3d tetra * fix: typo vaue to value * fix: adding abstract methods for interpolation functions * fix: changing add methods to abstract method names * fix: use abstract base class for supports * fix: don't swap axes for dn, not consistent with 3d * docs: adding type hints * fix: adding get element gradient for location * removing prints * fix: updating p2 2d support * fix: linting * docs: type hints * fix: adding implementation of required abstract methods * tests: updating 2d local coords to use array * fix: adding gradient orthogonal abstract method * fix: adding methods for setting inequality constraints * fix: adding element size abstract method * fix: removing unused operator code * fix: adding faults setter/getter * fix: making intrusion feature compatible with base feature * fix: linting * fix: count number of constraints before adding previously not counting constraints before tying to add * style: style fixes by ruff and autoformatting by black * tests: don't test base feature anymore * style: style fixes by ruff and autoformatting by black --------- Co-authored-by: Sam <samuel.joseph.roberts@hotmail.co.uk> Co-authored-by: lachlangrose <lachlangrose@users.noreply.github.com>
1 parent a76e718 commit 5701100

23 files changed

+896
-372
lines changed

.github/workflows/release-please.yml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Lint with ruff
1818
run: |
1919
ruff check LoopStructural --fix
20-
- uses: stefanzweifel/git-auto-commit-action@v4
20+
- uses: stefanzweifel/git-auto-commit-action@v5
2121
with:
2222
commit_message: "style: style fixes by ruff and autoformatting by black"
2323

@@ -31,7 +31,8 @@ jobs:
3131
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}}
3232
steps:
3333
- uses: actions/checkout@v4
34-
- uses: conda-incubator/setup-miniconda@v2
34+
- uses: conda-incubator/setup-miniconda@v3
35+
3536
with:
3637
python-version: ${{ matrix.python }}
3738
- name: Installing dependencies
@@ -100,7 +101,7 @@ jobs:
100101
os: ["ubuntu-latest"]
101102
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}}
102103
steps:
103-
- uses: conda-incubator/setup-miniconda@v2
104+
- uses: conda-incubator/setup-miniconda@v3
104105
with:
105106
auto-update-conda: true
106107
python-version: ${{ matrix.python-version }}
@@ -120,7 +121,7 @@ jobs:
120121
conda convert -p all conda/linux-64/*.tar.bz2 -f -o conda
121122
conda install anaconda-client -y
122123
- name: upload artifacts
123-
uses: actions/upload-artifact@v3
124+
uses: actions/upload-artifact@v4
124125
with:
125126
name: conda-${{ matrix.os }}-${{ matrix.python-version }}
126127
path: conda
@@ -138,11 +139,11 @@ jobs:
138139
python setup.py sdist
139140
pip wheel -w wheelhouse .
140141
141-
- uses: actions/upload-artifact@v3
142+
- uses: actions/upload-artifact@v4
142143
with:
143144
name: dist
144145
path: dist/*.tar.gz
145-
- uses: actions/upload-artifact@v3
146+
- uses: actions/upload-artifact@v4
146147
with:
147148
name: dist
148149
path: wheelhouse/*.whl
@@ -152,20 +153,20 @@ jobs:
152153
runs-on: ubuntu-latest
153154
if: ${{ needs.release-please.outputs.release_created }}
154155
steps:
155-
- uses: actions/download-artifact@v3
156+
- uses: actions/download-artifact@v4
156157
with:
157158
name: dist
158159
path: dist
159-
- uses: actions/download-artifact@v3
160+
- uses: actions/download-artifact@v4
160161
with:
161162
path: conda
162-
- uses: pypa/gh-action-pypi-publish@v1.6.4
163+
- uses: pypa/gh-action-pypi-publish@v1
163164
with:
164165
skip_existing: true
165166
verbose: true
166167
user: ${{ secrets.PYPI_USERNAME }}
167168
password: ${{ secrets.PYPI_PASSWORD }}
168-
- uses: conda-incubator/setup-miniconda@v2
169+
- uses: conda-incubator/setup-miniconda@v3
169170
- name: upload all files to conda-forge
170171
shell: bash -l {0}
171172
env:

LoopStructural/interpolators/_builders.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
StructuredGrid,
1111
TetMesh,
1212
)
13-
from LoopStructural.utils import BoundingBox
13+
from LoopStructural.datatypes import BoundingBox
1414
from LoopStructural.utils.logging import getLogger
1515

1616
logger = getLogger(__name__)
@@ -61,6 +61,7 @@ def get_interpolator(
6161
)
6262

6363
return P1Interpolator(support)
64+
return P1Interpolator(support)
6465
if interpolatortype == "P2":
6566
if support is not None:
6667
logger.info(
@@ -69,9 +70,7 @@ def get_interpolator(
6970
)
7071
return P2Interpolator(support)
7172
else:
72-
raise ValueError(
73-
"Cannot create P2 interpolator without support, try using PLI"
74-
)
73+
raise ValueError("Cannot create P2 interpolator without support, try using PLI")
7574

7675
if interpolatortype == "FDI":
7776
# find the volume of one element
@@ -96,8 +95,7 @@ def get_interpolator(
9695

9796
grid = StructuredGrid(origin=origin, nsteps=nsteps, step_vector=step_vector)
9897
logger.info(
99-
f"Creating regular grid with {grid.n_elements} elements \n"
100-
"for modelling using FDI"
98+
f"Creating regular grid with {grid.n_elements} elements \n" "for modelling using FDI"
10199
)
102100
return FiniteDifferenceInterpolator(grid)
103101
if interpolatortype == "DFI":

LoopStructural/interpolators/_discrete_interpolator.py

Lines changed: 9 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Discrete interpolator base for least squares
33
"""
44

5+
from abc import abstractmethod
56
import logging
67

78
from time import time
@@ -39,16 +40,12 @@ def __init__(self, support, data={}, c=None, up_to_date=False):
3940
else np.zeros(self.support.n_nodes)
4041
)
4142
self.region_function = lambda xyz: np.ones(xyz.shape[0], dtype=bool)
42-
# self.region_map[self.region] = np.array(range(0,
43-
# len(self.region_map[self.region])))
43+
4444
self.shape = "rectangular"
4545
if self.shape == "square":
4646
self.B = np.zeros(self.nx)
4747
self.c_ = 0
48-
# self.A = [] # sparse matrix storage coo format
49-
# self.col = []
50-
# self.row = [] # sparse matrix storage
51-
# self.w = []
48+
5249
self.solver = "cg"
5350

5451
self.eq_const_C = []
@@ -221,6 +218,12 @@ def add_constraints_to_least_squares(self, A, B, idc, w=1.0, name="undefined"):
221218
'w': w,
222219
}
223220

221+
@abstractmethod
222+
def add_gradient_orthogonal_constraints(
223+
self, points: np.ndarray, vectors: np.ndarray, w: float = 1.0
224+
):
225+
pass
226+
224227
def calculate_residual_for_constraints(self):
225228
"""Calculates Ax-B for all constraints added to the interpolator
226229
This could be a proxy to identify which constraints are controlling the model
@@ -252,7 +255,6 @@ def remove_constraints_from_least_squares(self, name="undefined", constraint_ids
252255
-------
253256
254257
"""
255-
256258
pass
257259

258260
def add_equality_constraints(self, node_idx, values, name="undefined"):
@@ -288,72 +290,6 @@ def add_equality_constraints(self, node_idx, values, name="undefined"):
288290
}
289291
self.eq_const_c += idc[outside].shape[0]
290292

291-
def add_inequality_constraints_to_matrix(self, A, l, u, idc, name="undefined"):
292-
"""Adds constraints for a matrix where the linear function
293-
l < Ax > u constrains the objective function
294-
295-
296-
Parameters
297-
----------
298-
A : numpy array
299-
matrix of coefficients
300-
l : numpy array
301-
lower bounds
302-
u : numpy array
303-
upper bounds
304-
idc : numpy array
305-
index of constraints
306-
Returns
307-
-------
308-
309-
"""
310-
# map from mesh node index to region node index
311-
gi = np.zeros(self.support.n_nodes, dtype=int)
312-
gi[:] = -1
313-
gi[self.region] = np.arange(0, self.nx, dtype=int)
314-
idc = gi[idc]
315-
rows = np.arange(self.ineq_const_c, self.ineq_const_c + idc.shape[0])
316-
rows = np.tile(rows, (A.shape[-1], 1)).T
317-
self.ineq_constraints[name] = {"A": A, "l": l, "col": idc, "u": u, "row": rows}
318-
self.ineq_const_c += idc.shape[0]
319-
320-
def add_inequality_feature(self, feature, lower=True, mask=None):
321-
"""Add an inequality constraint to the interpolator using an existing feature.
322-
This will make the interpolator greater than or less than the exising feature.
323-
Evaluate the feature at the interpolation nodes.
324-
Can provide a boolean mask to restrict to only some parts
325-
326-
Parameters
327-
----------
328-
feature : BaseFeature
329-
the feature that will be used to constraint the interpolator
330-
lower : bool, optional
331-
lower or upper constraint, by default True
332-
mask : np.ndarray, optional
333-
restrict the nodes to evaluate on, by default None
334-
"""
335-
# add inequality value for the nodes of the mesh
336-
# flag lower determines whether the feature is a lower bound or upper bound
337-
# mask is just a boolean array determining which nodes to apply it to
338-
339-
value = feature(self.support.nodes)
340-
if mask is None:
341-
mask = np.ones(value.shape[0], dtype=bool)
342-
l = np.zeros(value.shape[0]) - np.inf
343-
u = np.zeros(value.shape[0]) + np.inf
344-
mask = np.logical_and(mask, ~np.isnan(value))
345-
if lower:
346-
l[mask] = value[mask]
347-
if not lower:
348-
u[mask] = value[mask]
349-
350-
self.add_inequality_constraints_to_matrix(
351-
np.ones((value.shape[0], 1)),
352-
l,
353-
u,
354-
np.arange(0, self.nx, dtype=int),
355-
)
356-
357293
def add_tangent_constraints(self, w=1.0):
358294
"""Adds the constraints :math:`f(X)\cdotT=0`
359295

LoopStructural/interpolators/_finite_difference_interpolator.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def setup_interpolator(self, **kwargs):
121121
self.assemble_inner(operator, weight)
122122
self.add_norm_constraints(self.interpolation_weights["npw"])
123123
self.add_gradient_constraints(self.interpolation_weights["gpw"])
124-
self.add_vaue_constraints(self.interpolation_weights["cpw"])
124+
self.add_value_constraints(self.interpolation_weights["cpw"])
125125
self.add_tangent_constraints(self.interpolation_weights["tpw"])
126126
self.add_interface_constraints(self.interpolation_weights["ipw"])
127127
self.add_inequality_constraints()
@@ -136,7 +136,7 @@ def copy(self):
136136
"""
137137
return FiniteDifferenceInterpolator(self.support)
138138

139-
def add_vaue_constraints(self, w=1.0):
139+
def add_value_constraints(self, w=1.0):
140140
"""
141141
142142
Parameters
@@ -151,7 +151,9 @@ def add_vaue_constraints(self, w=1.0):
151151
points = self.get_value_constraints()
152152
# check that we have added some points
153153
if points.shape[0] > 0:
154-
node_idx, inside = self.support.position_to_cell_corners(points[:, :3])
154+
node_idx, inside = self.support.position_to_cell_corners(
155+
points[:, : self.support.dimension]
156+
)
155157
# print(points[inside,:].shape)
156158
gi = np.zeros(self.support.n_nodes, dtype=int)
157159
gi[:] = -1
@@ -160,14 +162,14 @@ def add_vaue_constraints(self, w=1.0):
160162
idc[:] = -1
161163
idc[inside, :] = gi[node_idx[inside, :]]
162164
inside = np.logical_and(~np.any(idc == -1, axis=1), inside)
163-
a = self.support.position_to_dof_coefs(points[inside, :3])
164-
# a*=w
165-
# a/=np.product(self.support.step_vector)
165+
a = self.support.position_to_dof_coefs(points[inside, : self.support.dimension])
166+
# a *= w
167+
# a/=self.support.enp.product(self.support.step_vector)
166168
self.add_constraints_to_least_squares(
167169
a,
168170
points[inside, 3],
169171
idc[inside, :],
170-
w=w * points[inside, 4],
172+
w=w * points[inside, 4] * self.support.element_size,
171173
name="value",
172174
)
173175
if np.sum(inside) <= 0:
@@ -233,7 +235,9 @@ def add_interface_constraints(self, w=1.0):
233235
# get elements for points
234236
points = self.get_interface_constraints()
235237
if points.shape[0] > 1:
236-
vertices, c, tetras, inside = self.support.get_element_for_location(points[:, :3])
238+
vertices, c, tetras, inside = self.support.get_element_for_location(
239+
points[:, : self.support.dimension]
240+
)
237241
# calculate volume of tetras
238242
# vecs = vertices[inside, 1:, :] - vertices[inside, 0, None, :]
239243
# vol = np.abs(np.linalg.det(vecs)) / 6
@@ -391,7 +395,7 @@ def add_norm_constraints(self, w=1.0):
391395
self.up_to_date = False
392396

393397
def add_gradient_orthogonal_constraints(
394-
self, points: np.ndarray, vector: np.ndarray, w: float = 1.0, B: float = 0
398+
self, points: np.ndarray, vectors: np.ndarray, w: float = 1.0, B: float = 0
395399
):
396400
"""
397401
constraints scalar field to be orthogonal to a given vector
@@ -424,8 +428,8 @@ def add_gradient_orthogonal_constraints(
424428
idc[inside, :] = gi[node_idx[inside, :]]
425429
inside = np.logical_and(~np.any(idc == -1, axis=1), inside)
426430
# normalise vector and scale element gradient matrix by norm as well
427-
norm = np.linalg.norm(vector, axis=1)
428-
vector[norm > 0, :] /= norm[norm > 0, None]
431+
norm = np.linalg.norm(vectors, axis=1)
432+
vectors[norm > 0, :] /= norm[norm > 0, None]
429433

430434
# normalise element vector to unit vector for dot product
431435
(
@@ -437,7 +441,7 @@ def add_gradient_orthogonal_constraints(
437441
T[norm > 0, :, :] /= norm[norm > 0, None, None]
438442

439443
# dot product of vector and element gradient = 0
440-
A = np.einsum("ij,ijk->ik", vector[inside, :3], T)
444+
A = np.einsum("ij,ijk->ik", vectors[inside, :3], T)
441445
B = np.zeros(points[inside, :].shape[0]) + B
442446
self.add_constraints_to_least_squares(
443447
A, B, idc[inside, :], w=w * self.vol, name="gradient orthogonal"
@@ -461,7 +465,7 @@ def add_regularisation(self, operator, w=0.1):
461465
-------
462466
463467
"""
464-
self.assemble_inner(operator)
468+
self.assemble_inner(operator, w)
465469
# self.assemble_borders()
466470

467471
# def assemble_borders(self, operator, w):

LoopStructural/interpolators/_geological_interpolator.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ def set_value_constraints(self, points: np.ndarray):
106106
107107
"""
108108
points = self.check_array(points)
109-
if points.shape[1] < 4:
110-
raise ValueError("Value points must at least have X,Y,Z,val")
109+
if points.shape[1] < 5:
110+
raise ValueError("Value points must at least have X,Y,Z,val,w")
111111
self.data["value"] = points
112112
self.n_i = points.shape[0]
113113
self.up_to_date = False
@@ -172,10 +172,20 @@ def set_interface_constraints(self, points: np.ndarray):
172172
self.data["interface"] = points
173173
self.up_to_date = False
174174

175-
def set_inequality_constraints(self, points: np.ndarray):
175+
def set_value_inequality_constraints(self, points: np.ndarray):
176+
if points.shape[1] < 5:
177+
raise ValueError("Inequality constraints must at least have X,Y,Z,lower,upper")
176178
self.data["inequality"] = points
177179
self.up_to_date = False
178180

181+
def set_inequality_pairs_constraints(self, pointsa: np.ndarray, pointsb: np.ndarray):
182+
if pointsa.shape[1] < 3:
183+
raise ValueError("Inequality pairs constraints must at least have X,Y,Z")
184+
if pointsb.shape[1] < 3:
185+
raise ValueError("Inequality pairs constraints must at least have X,Y,Z")
186+
self.data["inequality_pairs"] = {"a": pointsa, "b": pointsb}
187+
self.up_to_date = False
188+
179189
def get_value_constraints(self):
180190
"""
181191
@@ -274,6 +284,26 @@ def evaluate_gradient(self, locations: np.ndarray):
274284
def reset(self):
275285
pass
276286

287+
@abstractmethod
288+
def add_value_constraints(self, w: float = 1.0):
289+
pass
290+
291+
@abstractmethod
292+
def add_gradient_constraints(self, w: float = 1.0):
293+
pass
294+
295+
@abstractmethod
296+
def add_norm_constraints(self, w: float = 1.0):
297+
pass
298+
299+
@abstractmethod
300+
def add_tangent_constraints(self, w: float = 1.0):
301+
pass
302+
303+
@abstractmethod
304+
def add_interface_constraints(self, w: float = 1.0):
305+
pass
306+
277307
def to_dict(self):
278308
return {
279309
"type": self.type,

0 commit comments

Comments
 (0)