LoopStructural 1.6.2__py3-none-any.whl → 1.6.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of LoopStructural might be problematic. Click here for more details.

Files changed (56) hide show
  1. LoopStructural/datatypes/_bounding_box.py +19 -4
  2. LoopStructural/datatypes/_point.py +31 -0
  3. LoopStructural/datatypes/_structured_grid.py +17 -0
  4. LoopStructural/datatypes/_surface.py +17 -0
  5. LoopStructural/export/omf_wrapper.py +49 -21
  6. LoopStructural/interpolators/__init__.py +13 -0
  7. LoopStructural/interpolators/_api.py +81 -13
  8. LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
  9. LoopStructural/interpolators/_discrete_interpolator.py +100 -53
  10. LoopStructural/interpolators/_finite_difference_interpolator.py +68 -78
  11. LoopStructural/interpolators/_geological_interpolator.py +27 -10
  12. LoopStructural/interpolators/_p1interpolator.py +3 -3
  13. LoopStructural/interpolators/_surfe_wrapper.py +42 -12
  14. LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
  15. LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
  16. LoopStructural/interpolators/supports/_3d_base_structured.py +24 -7
  17. LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -9
  18. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  19. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
  20. LoopStructural/interpolators/supports/__init__.py +7 -0
  21. LoopStructural/interpolators/supports/_base_support.py +7 -0
  22. LoopStructural/modelling/__init__.py +1 -1
  23. LoopStructural/modelling/core/geological_model.py +0 -2
  24. LoopStructural/modelling/features/_analytical_feature.py +25 -16
  25. LoopStructural/modelling/features/_geological_feature.py +47 -11
  26. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  27. LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
  28. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  29. LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
  30. LoopStructural/modelling/features/fault/__init__.py +1 -1
  31. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  32. LoopStructural/modelling/features/fault/_fault_segment.py +15 -49
  33. LoopStructural/modelling/features/fold/__init__.py +1 -2
  34. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  35. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  36. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  37. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  38. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  39. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  40. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  41. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  42. LoopStructural/modelling/input/process_data.py +6 -0
  43. LoopStructural/modelling/input/project_file.py +24 -3
  44. LoopStructural/utils/_surface.py +5 -2
  45. LoopStructural/utils/colours.py +26 -0
  46. LoopStructural/utils/features.py +5 -0
  47. LoopStructural/utils/maths.py +51 -0
  48. LoopStructural/version.py +1 -1
  49. LoopStructural-1.6.3.dist-info/METADATA +146 -0
  50. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/RECORD +53 -48
  51. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/WHEEL +1 -1
  52. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  53. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  54. LoopStructural-1.6.2.dist-info/METADATA +0 -81
  55. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/LICENSE +0 -0
  56. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@ from abc import abstractmethod
6
6
  from typing import Callable, Optional, Union
7
7
  import logging
8
8
 
9
- from time import time
10
9
  import numpy as np
11
10
  from scipy import sparse # import sparse.coo_matrix, sparse.bmat, sparse.eye
12
11
  from ..interpolators import InterpolatorType
@@ -33,6 +32,7 @@ class DiscreteInterpolator(GeologicalInterpolator):
33
32
  GeologicalInterpolator.__init__(self, data=data, up_to_date=up_to_date)
34
33
  self.B = []
35
34
  self.support = support
35
+ self.dimensions = support.dimension
36
36
  self.c = (
37
37
  np.array(c)
38
38
  if c is not None and np.array(c).shape[0] == self.support.n_nodes
@@ -62,7 +62,6 @@ class DiscreteInterpolator(GeologicalInterpolator):
62
62
  self.interpolation_weights = {}
63
63
  logger.info("Creating discrete interpolator with {} degrees of freedom".format(self.nx))
64
64
  self.type = InterpolatorType.BASE_DISCRETE
65
- self.c = np.zeros(self.support.n_nodes)
66
65
 
67
66
  @property
68
67
  def nx(self) -> int:
@@ -133,6 +132,28 @@ class DiscreteInterpolator(GeologicalInterpolator):
133
132
  self.up_to_date = False
134
133
  self.interpolation_weights[key] = weights[key]
135
134
 
135
+ def _pre_solve(self):
136
+ """
137
+ Pre solve function to be run before solving the interpolation
138
+ """
139
+ self.c = np.zeros(self.support.n_nodes)
140
+ self.c[:] = np.nan
141
+ return True
142
+
143
+ def _post_solve(self):
144
+ """Post solve function(s) to be run after the solver has been called"""
145
+ self.clear_constraints()
146
+ return True
147
+
148
+ def clear_constraints(self):
149
+ """
150
+ Clear the constraints from the interpolator, this makes sure we are not storing
151
+ the constraints after the solver has been run
152
+ """
153
+ self.constraints = {}
154
+ self.ineq_constraints = {}
155
+ self.equal_constraints = {}
156
+
136
157
  def reset(self):
137
158
  """
138
159
  Reset the interpolation constraints
@@ -140,7 +161,7 @@ class DiscreteInterpolator(GeologicalInterpolator):
140
161
  """
141
162
  self.constraints = {}
142
163
  self.c_ = 0
143
- logger.debug("Resetting interpolation constraints")
164
+ logger.info("Resetting interpolation constraints")
144
165
 
145
166
  def add_constraints_to_least_squares(self, A, B, idc, w=1.0, name="undefined"):
146
167
  """
@@ -181,7 +202,8 @@ class DiscreteInterpolator(GeologicalInterpolator):
181
202
  B = B.reshape((A.shape[0]))
182
203
  # w = w.reshape((A.shape[0]))
183
204
  # normalise by rows of A
184
- length = np.linalg.norm(A, axis=1) # .getcol(0).norm()
205
+ # Should this be done? It should make the solution more stable
206
+ length = np.linalg.norm(A, axis=1)
185
207
  B[length > 0] /= length[length > 0]
186
208
  # going to assume if any are nan they are all nan
187
209
  mask = np.any(np.isnan(A), axis=1)
@@ -193,9 +215,6 @@ class DiscreteInterpolator(GeologicalInterpolator):
193
215
  raise BaseException("w must be a numpy array")
194
216
 
195
217
  if w.shape[0] != A.shape[0]:
196
- # # make w the same size as A
197
- # w = np.tile(w,(A.shape[1],1)).T
198
- # else:
199
218
  raise BaseException("Weight array does not match number of constraints")
200
219
  if np.any(np.isnan(idc)) or np.any(np.isnan(A)) or np.any(np.isnan(B)):
201
220
  logger.warning("Constraints contain nan not adding constraints: {}".format(name))
@@ -286,31 +305,43 @@ class DiscreteInterpolator(GeologicalInterpolator):
286
305
  self.add_inequality_constraints_to_matrix(a, points[:, 3:5], cols, 'inequality_value')
287
306
 
288
307
  def add_inequality_pairs_constraints(
289
- self, w: float = 1.0, upper_bound=np.finfo(float).eps, lower_bound=-np.inf
308
+ self,
309
+ w: float = 1.0,
310
+ upper_bound=np.finfo(float).eps,
311
+ lower_bound=-np.inf,
312
+ pairs: Optional[list] = None,
290
313
  ):
291
314
 
292
315
  points = self.get_inequality_pairs_constraints()
293
316
  if points.shape[0] > 0:
294
-
295
317
  # assemble a list of pairs in the model
296
318
  # this will make pairs even across stratigraphic boundaries
297
319
  # TODO add option to only add stratigraphic pairs
298
- pairs = {}
299
- k = 0
300
- for i in np.unique(points[:, self.support.dimension]):
301
- for j in np.unique(points[:, self.support.dimension]):
302
- if i == j:
303
- continue
304
- if tuple(sorted([i, j])) not in pairs:
305
- pairs[tuple(sorted([i, j]))] = k
306
- k += 1
307
- pairs = list(pairs.keys())
320
+ if not pairs:
321
+ pairs = {}
322
+ k = 0
323
+ for i in np.unique(points[:, self.support.dimension]):
324
+ for j in np.unique(points[:, self.support.dimension]):
325
+ if i == j:
326
+ continue
327
+ if tuple(sorted([i, j])) not in pairs:
328
+ pairs[tuple(sorted([i, j]))] = k
329
+ k += 1
330
+ pairs = list(pairs.keys())
308
331
  for pair in pairs:
309
332
  upper_points = points[points[:, self.support.dimension] == pair[0]]
310
333
  lower_points = points[points[:, self.support.dimension] == pair[1]]
311
334
 
312
335
  upper_interpolation = self.support.get_element_for_location(upper_points)
313
336
  lower_interpolation = self.support.get_element_for_location(lower_points)
337
+ if (~upper_interpolation[3]).sum() > 0:
338
+ logger.warning(
339
+ f'Upper points not in mesh {upper_points[~upper_interpolation[3]]}'
340
+ )
341
+ if (~lower_interpolation[3]).sum() > 0:
342
+ logger.warning(
343
+ f'Lower points not in mesh {lower_points[~lower_interpolation[3]]}'
344
+ )
314
345
  ij = np.array(
315
346
  [
316
347
  *np.meshgrid(
@@ -325,7 +356,7 @@ class DiscreteInterpolator(GeologicalInterpolator):
325
356
  rows = np.arange(0, ij.shape[0], dtype=int)
326
357
  rows = np.tile(rows, (upper_interpolation[1].shape[-1], 1)).T
327
358
  rows = np.hstack([rows, rows])
328
- a = upper_interpolation[1][upper_interpolation[3]][ij[:, 0]] # np.ones(ij.shape[0])
359
+ a = upper_interpolation[1][upper_interpolation[3]][ij[:, 0]]
329
360
  a = np.hstack([a, -lower_interpolation[1][lower_interpolation[3]][ij[:, 1]]])
330
361
  cols = np.hstack(
331
362
  [
@@ -511,7 +542,7 @@ class DiscreteInterpolator(GeologicalInterpolator):
511
542
  mats.append(c['matrix'])
512
543
  bounds.append(c['bounds'])
513
544
  if len(mats) == 0:
514
- return None, None
545
+ return sparse.csr_matrix((0, self.nx), dtype=float), np.zeros((0, 3))
515
546
  Q = sparse.vstack(mats)
516
547
  bounds = np.vstack(bounds)
517
548
  return Q, bounds
@@ -519,6 +550,7 @@ class DiscreteInterpolator(GeologicalInterpolator):
519
550
  def solve_system(
520
551
  self,
521
552
  solver: Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]] = None,
553
+ tol: Optional[float] = None,
522
554
  solver_kwargs: dict = {},
523
555
  ) -> bool:
524
556
  """
@@ -539,19 +571,16 @@ class DiscreteInterpolator(GeologicalInterpolator):
539
571
  True if the interpolation is run
540
572
 
541
573
  """
542
- starttime = time()
543
- self.c = np.zeros(self.support.n_nodes)
544
- self.c[:] = np.nan
574
+ if not self._pre_solve():
575
+ raise ValueError("Pre solve failed")
576
+
545
577
  A, b = self.build_matrix()
546
578
  Q, bounds = self.build_inequality_matrix()
547
579
  if callable(solver):
548
580
  logger.warning('Using custom solver')
549
581
  self.c = solver(A.tocsr(), b)
550
582
  self.up_to_date = True
551
-
552
- return True
553
- ## solve with lsmr
554
- if isinstance(solver, str):
583
+ elif isinstance(solver, str) or solver is None:
555
584
  if solver not in ['cg', 'lsmr', 'admm']:
556
585
  logger.warning(
557
586
  f'Unknown solver {solver} using cg. \n Available solvers are cg and lsmr or a custom solver as a callable function'
@@ -559,18 +588,29 @@ class DiscreteInterpolator(GeologicalInterpolator):
559
588
  solver = 'cg'
560
589
  if solver == 'cg':
561
590
  logger.info("Solving using cg")
562
- ATA = A.T.dot(A)
563
- ATB = A.T.dot(b)
564
- res = sparse.linalg.cg(ATA, ATB, **solver_kwargs)
591
+ if 'atol' not in solver_kwargs or 'rtol' not in solver_kwargs:
592
+ if tol is not None:
593
+ solver_kwargs['atol'] = tol
594
+
595
+ logger.info(f"Solver kwargs: {solver_kwargs}")
596
+
597
+ res = sparse.linalg.cg(A.T @ A, A.T @ b, **solver_kwargs)
565
598
  if res[1] > 0:
566
599
  logger.warning(
567
600
  f'CG reached iteration limit ({res[1]})and did not converge, check input data. Setting solution to last iteration'
568
601
  )
569
602
  self.c = res[0]
570
603
  self.up_to_date = True
571
- return True
604
+
572
605
  elif solver == 'lsmr':
573
606
  logger.info("Solving using lsmr")
607
+ if 'atol' not in solver_kwargs:
608
+ if tol is not None:
609
+ solver_kwargs['atol'] = tol
610
+ if 'btol' not in solver_kwargs:
611
+ if tol is not None:
612
+ solver_kwargs['btol'] = tol
613
+ logger.info(f"Solver kwargs: {solver_kwargs}")
574
614
  res = sparse.linalg.lsmr(A, b, **solver_kwargs)
575
615
  if res[1] == 1 or res[1] == 4 or res[1] == 2 or res[1] == 5:
576
616
  self.c = res[0]
@@ -585,8 +625,7 @@ class DiscreteInterpolator(GeologicalInterpolator):
585
625
  )
586
626
  self.c = res[0]
587
627
  self.up_to_date = True
588
- logger.info("Interpolation took %f seconds" % (time() - starttime))
589
- return True
628
+
590
629
  elif solver == 'admm':
591
630
  logger.info("Solving using admm")
592
631
 
@@ -601,28 +640,35 @@ class DiscreteInterpolator(GeologicalInterpolator):
601
640
 
602
641
  try:
603
642
  from loopsolver import admm_solve
643
+
644
+ try:
645
+ linsys_solver = solver_kwargs.pop('linsys_solver', 'lsmr')
646
+ res = admm_solve(
647
+ A,
648
+ b,
649
+ Q,
650
+ bounds,
651
+ x0=x0,
652
+ admm_weight=solver_kwargs.pop('admm_weight', 0.01),
653
+ nmajor=solver_kwargs.pop('nmajor', 200),
654
+ linsys_solver_kwargs=solver_kwargs,
655
+ linsys_solver=linsys_solver,
656
+ )
657
+ self.c = res
658
+ self.up_to_date = True
659
+ except ValueError as e:
660
+ logger.error(f"ADMM solver failed: {e}")
661
+ self.up_to_date = False
604
662
  except ImportError:
605
663
  logger.warning(
606
664
  "Cannot import admm solver. Please install loopsolver or use lsmr or cg"
607
665
  )
608
- return False
609
- try:
610
- res = admm_solve(
611
- A,
612
- b,
613
- Q,
614
- bounds,
615
- x0=x0,
616
- admm_weight=solver_kwargs.pop('admm_weight', 0.01),
617
- nmajor=solver_kwargs.pop('nmajor', 200),
618
- linsys_solver_kwargs=solver_kwargs,
619
- )
620
- self.c = res
621
- self.up_to_date = True
622
- except ValueError as e:
623
- logger.error(f"ADMM solver failed: {e}")
624
- return False
625
- return False
666
+ self.up_to_date = False
667
+ else:
668
+ logger.error(f"Unknown solver {solver}")
669
+ self.up_to_date = False
670
+ # self._post_solve()
671
+ return self.up_to_date
626
672
 
627
673
  def update(self) -> bool:
628
674
  """
@@ -641,7 +687,8 @@ class DiscreteInterpolator(GeologicalInterpolator):
641
687
  return False
642
688
  if not self.up_to_date:
643
689
  self.setup_interpolator()
644
- return self.solve_system(self.solver)
690
+ self.up_to_date = self.solve_system(self.solver)
691
+ return self.up_to_date
645
692
 
646
693
  def evaluate_value(self, locations: np.ndarray) -> np.ndarray:
647
694
  """Evaluate the value of the interpolator at location
@@ -8,8 +8,6 @@ from ..utils import get_vectors
8
8
  from ._discrete_interpolator import DiscreteInterpolator
9
9
  from ..interpolators import InterpolatorType
10
10
 
11
- from ._operator import Operator
12
-
13
11
  from LoopStructural.utils import getLogger
14
12
 
15
13
  logger = getLogger(__name__)
@@ -26,13 +24,11 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
26
24
  """
27
25
  self.shape = "rectangular"
28
26
  DiscreteInterpolator.__init__(self, grid, data=data)
29
- # default weights for the interpolation matrix are 1 in x,y,z and
30
- # 1/
31
27
  self.set_interpolation_weights(
32
28
  {
33
- "dxy": 0.7,
34
- "dyz": 0.7,
35
- "dxz": 0.7,
29
+ "dxy": 1.0,
30
+ "dyz": 1.0,
31
+ "dxz": 1.0,
36
32
  "dxx": 1.0,
37
33
  "dyy": 1.0,
38
34
  "dzz": 1.0,
@@ -47,7 +43,6 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
47
43
  }
48
44
  )
49
45
 
50
- # grid.step_vector[2]
51
46
  self.type = InterpolatorType.FINITE_DIFFERENCE
52
47
 
53
48
  def setup_interpolator(self, **kwargs):
@@ -88,43 +83,24 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
88
83
  self.interpolation_weights["dyy"] = 0.1 * kwargs["regularisation"]
89
84
  self.interpolation_weights["dzz"] = 0.1 * kwargs["regularisation"]
90
85
  self.interpolation_weights[key] = kwargs[key]
91
- # if we want to define the operators manually
92
- if "operators" in kwargs:
93
- for o in kwargs["operators"].values():
94
- self.assemble_inner(o[0], o[1])
95
- # otherwise just use defaults
96
- if "operators" not in kwargs:
97
- operator = Operator.Dxy_mask
98
- weight = (
99
- self.interpolation_weights["dxy"] / 4
100
- ) # (4*self.support.step_vector[0]*self.support.step_vector[1])
101
- self.assemble_inner(operator, weight)
102
- operator = Operator.Dyz_mask
103
- weight = (
104
- self.interpolation_weights["dyz"] / 4
105
- ) # (4*self.support.step_vector[1]*self.support.step_vector[2])
106
- self.assemble_inner(operator, weight)
107
- operator = Operator.Dxz_mask
108
- weight = (
109
- self.interpolation_weights["dxz"] / 4
110
- ) # (4*self.support.step_vector[0]*self.support.step_vector[2])
111
- self.assemble_inner(operator, weight)
112
- operator = Operator.Dxx_mask
113
- weight = self.interpolation_weights["dxx"] / 1 # self.support.step_vector[0]**2
114
- self.assemble_inner(operator, weight)
115
- operator = Operator.Dyy_mask
116
- weight = self.interpolation_weights["dyy"] / 1 # self.support.step_vector[1]**2
117
- self.assemble_inner(operator, weight)
118
- operator = Operator.Dzz_mask
119
- weight = self.interpolation_weights["dzz"] / 1 # self.support.step_vector[2]**2
120
- self.assemble_inner(operator, weight)
86
+ # either use the default operators or the ones passed to the function
87
+ operators = kwargs.get(
88
+ "operators", self.support.get_operators(weights=self.interpolation_weights)
89
+ )
90
+ for k, o in operators.items():
91
+ self.assemble_inner(o[0], o[1], name=k)
92
+
121
93
  self.add_norm_constraints(self.interpolation_weights["npw"])
122
94
  self.add_gradient_constraints(self.interpolation_weights["gpw"])
123
95
  self.add_value_constraints(self.interpolation_weights["cpw"])
124
96
  self.add_tangent_constraints(self.interpolation_weights["tpw"])
125
97
  self.add_interface_constraints(self.interpolation_weights["ipw"])
126
98
  self.add_value_inequality_constraints()
127
- self.add_inequality_pairs_constraints()
99
+ self.add_inequality_pairs_constraints(
100
+ kwargs.get('inequality_pairs', None),
101
+ upper_bound=kwargs.get('inequality_pair_upper_bound', np.finfo(float).eps),
102
+ lower_bound=kwargs.get('inequality_pair_lower_bound', -np.inf),
103
+ )
128
104
 
129
105
  def copy(self):
130
106
  """
@@ -167,9 +143,9 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
167
143
  # a/=self.support.enp.product(self.support.step_vector)
168
144
  self.add_constraints_to_least_squares(
169
145
  a,
170
- points[inside, 3],
146
+ points[inside, self.support.dimension],
171
147
  idc[inside, :],
172
- w=w * points[inside, 4],
148
+ w=w * points[inside, self.support.dimension + 1],
173
149
  name="value",
174
150
  )
175
151
  if np.sum(inside) <= 0:
@@ -206,8 +182,13 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
206
182
  # A *= vol[:,None]
207
183
  idc = tetras[inside, :]
208
184
 
209
- for unique_id in np.unique(points[np.logical_and(~np.isnan(points[:, 3]), inside), 3]):
210
- mask = points[inside, 3] == unique_id
185
+ for unique_id in np.unique(
186
+ points[
187
+ np.logical_and(~np.isnan(points[:, self.support.dimension]), inside),
188
+ self.support.dimension,
189
+ ]
190
+ ):
191
+ mask = points[inside, self.support.dimension] == unique_id
211
192
  ij = np.array(
212
193
  np.meshgrid(
213
194
  np.arange(0, A[mask, :].shape[0]),
@@ -250,7 +231,9 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
250
231
  # calculate unit vector for orientation data
251
232
  # points[:,3:]/=np.linalg.norm(points[:,3:],axis=1)[:,None]
252
233
 
253
- node_idx, inside = self.support.position_to_cell_corners(points[:, :3])
234
+ node_idx, inside = self.support.position_to_cell_corners(
235
+ points[:, : self.support.dimension]
236
+ )
254
237
  # calculate unit vector for node gradients
255
238
  # this means we are only constraining direction of grad not the
256
239
  # magnitude
@@ -267,13 +250,22 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
267
250
  T,
268
251
  elements,
269
252
  inside_,
270
- ) = self.support.get_element_gradient_for_location(points[inside, :3])
253
+ ) = self.support.get_element_gradient_for_location(
254
+ points[inside, : self.support.dimension]
255
+ )
271
256
  # normalise constraint vector and scale element matrix by this
272
- norm = np.linalg.norm(points[:, 3:6], axis=1)
257
+ norm = np.linalg.norm(
258
+ points[:, self.support.dimension : self.support.dimension + self.support.dimension],
259
+ axis=1,
260
+ )
273
261
  points[:, 3:6] /= norm[:, None]
274
262
  T /= norm[inside, None, None]
275
263
  # calculate two orthogonal vectors to constraint (strike and dip vector)
276
- strike_vector, dip_vector = get_vectors(points[inside, 3:6])
264
+ strike_vector, dip_vector = get_vectors(
265
+ points[
266
+ inside, self.support.dimension : self.support.dimension + self.support.dimension
267
+ ]
268
+ )
277
269
  A = np.einsum("ij,ijk->ik", strike_vector.T, T)
278
270
  B = np.zeros(points[inside, :].shape[0])
279
271
  self.add_constraints_to_least_squares(A, B, idc[inside, :], w=w, name="gradient")
@@ -302,7 +294,9 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
302
294
  if points.shape[0] > 0:
303
295
  # calculate unit vector for orientation data
304
296
  # points[:,3:]/=np.linalg.norm(points[:,3:],axis=1)[:,None]
305
- node_idx, inside = self.support.position_to_cell_corners(points[:, :3])
297
+ node_idx, inside = self.support.position_to_cell_corners(
298
+ points[:, : self.support.dimension]
299
+ )
306
300
  gi = np.zeros(self.support.n_nodes)
307
301
  gi[:] = -1
308
302
  gi[self.region] = np.arange(0, self.nx)
@@ -319,31 +313,23 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
319
313
  T,
320
314
  elements,
321
315
  inside_,
322
- ) = self.support.get_element_gradient_for_location(points[inside, :3])
316
+ ) = self.support.get_element_gradient_for_location(
317
+ points[inside, : self.support.dimension]
318
+ )
323
319
  # T*=np.product(self.support.step_vector)
324
320
  # T/=self.support.step_vector[0]
321
+
325
322
  w /= 3
326
- self.add_constraints_to_least_squares(
327
- T[:, 0, :],
328
- points[inside, 3],
329
- idc[inside, :],
330
- w=w,
331
- name="norm",
332
- )
333
- self.add_constraints_to_least_squares(
334
- T[:, 1, :],
335
- points[inside, 4],
336
- idc[inside, :],
337
- w=w,
338
- name="norm",
339
- )
340
- self.add_constraints_to_least_squares(
341
- T[:, 2, :],
342
- points[inside, 5],
343
- idc[inside, :],
344
- w=w,
345
- name="norm",
346
- )
323
+ for d in range(self.support.dimension):
324
+
325
+ self.add_constraints_to_least_squares(
326
+ T[:, d, :],
327
+ points[inside, self.support.dimension + d],
328
+ idc[inside, :],
329
+ w=w,
330
+ name=f"norm_{d}",
331
+ )
332
+
347
333
  if np.sum(inside) <= 0:
348
334
  logger.warning(
349
335
  f"{np.sum(~inside)} \
@@ -356,7 +342,7 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
356
342
  points: np.ndarray,
357
343
  vectors: np.ndarray,
358
344
  w: float = 1.0,
359
- B: float = 0,
345
+ b: float = 0,
360
346
  name="gradient orthogonal",
361
347
  ):
362
348
  """
@@ -378,7 +364,9 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
378
364
  if points.shape[0] > 0:
379
365
 
380
366
  # calculate unit vector for orientation data
381
- node_idx, inside = self.support.position_to_cell_corners(points[:, :3])
367
+ node_idx, inside = self.support.position_to_cell_corners(
368
+ points[:, : self.support.dimension]
369
+ )
382
370
  # calculate unit vector for node gradients
383
371
  # this means we are only constraining direction of grad not the
384
372
  # magnitude
@@ -400,13 +388,15 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
400
388
  T,
401
389
  elements,
402
390
  inside_,
403
- ) = self.support.get_element_gradient_for_location(points[inside, :3])
391
+ ) = self.support.get_element_gradient_for_location(
392
+ points[inside, : self.support.dimension]
393
+ )
404
394
  T[norm > 0, :, :] /= norm[norm > 0, None, None]
405
395
 
406
396
  # dot product of vector and element gradient = 0
407
- A = np.einsum("ij,ijk->ik", vectors[inside, :3], T)
408
- B = np.zeros(points[inside, :].shape[0]) + B
409
- self.add_constraints_to_least_squares(A, B, idc[inside, :], w=w, name=name)
397
+ A = np.einsum("ij,ijk->ik", vectors[inside, : self.support.dimension], T)
398
+ b_ = np.zeros(points[inside, :].shape[0]) + b
399
+ self.add_constraints_to_least_squares(A, b_, idc[inside, :], w=w, name=name)
410
400
 
411
401
  if np.sum(inside) <= 0:
412
402
  logger.warning(
@@ -432,7 +422,7 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
432
422
 
433
423
  # def assemble_borders(self, operator, w):
434
424
 
435
- def assemble_inner(self, operator, w):
425
+ def assemble_inner(self, operator, w, name='regularisation'):
436
426
  """
437
427
 
438
428
  Parameters
@@ -465,6 +455,6 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
465
455
  B[inside],
466
456
  idc[inside, :],
467
457
  w=w,
468
- name="regularisation",
458
+ name=name,
469
459
  )
470
460
  return
@@ -7,6 +7,7 @@ from LoopStructural.utils.exceptions import LoopTypeError
7
7
  from ..interpolators import InterpolatorType
8
8
  import numpy as np
9
9
 
10
+ from typing import Optional
10
11
  from ..utils import getLogger
11
12
 
12
13
  logger = getLogger(__name__)
@@ -42,6 +43,8 @@ class GeologicalInterpolator(metaclass=ABCMeta):
42
43
  self.constraints = []
43
44
  self.__str = "Base Geological Interpolator"
44
45
  self.valid = True
46
+ self.dimensions = 3 # default to 3d
47
+ self.support = None
45
48
 
46
49
  @property
47
50
  def data(self):
@@ -106,9 +109,9 @@ class GeologicalInterpolator(metaclass=ABCMeta):
106
109
 
107
110
  """
108
111
  points = self.check_array(points)
109
- if points.shape[1] == 4:
112
+ if points.shape[1] == self.dimensions + 1:
110
113
  points = np.hstack([points, np.ones((points.shape[0], 1))])
111
- if points.shape[1] < 5:
114
+ if points.shape[1] < self.dimensions + 2:
112
115
  raise ValueError("Value points must at least have X,Y,Z,val,w")
113
116
  self.data["value"] = points
114
117
  self.n_i = points.shape[0]
@@ -127,9 +130,9 @@ class GeologicalInterpolator(metaclass=ABCMeta):
127
130
  -------
128
131
 
129
132
  """
130
- if points.shape[1] == 6:
133
+ if points.shape[1] == self.dimensions * 2:
131
134
  points = np.hstack([points, np.ones((points.shape[0], 1))])
132
- if points.shape[1] < 7:
135
+ if points.shape[1] < self.dimensions * 2 + 1:
133
136
  raise ValueError("Gradient constraints must at least have X,Y,Z,gx,gy,gz")
134
137
  self.n_g = points.shape[0]
135
138
  self.data["gradient"] = points
@@ -148,9 +151,9 @@ class GeologicalInterpolator(metaclass=ABCMeta):
148
151
  -------
149
152
 
150
153
  """
151
- if points.shape[1] == 6:
154
+ if points.shape[1] == self.dimensions * 2:
152
155
  points = np.hstack([points, np.ones((points.shape[0], 1))])
153
- if points.shape[1] < 7:
156
+ if points.shape[1] < self.dimensions * 2 + 1:
154
157
  raise ValueError("Nonrmal constraints must at least have X,Y,Z,nx,ny,nz")
155
158
  self.n_n = points.shape[0]
156
159
  self.data["normal"] = points
@@ -169,9 +172,9 @@ class GeologicalInterpolator(metaclass=ABCMeta):
169
172
  -------
170
173
 
171
174
  """
172
- if points.shape[1] == 6:
175
+ if points.shape[1] == self.dimensions * 2:
173
176
  points = np.hstack([points, np.ones((points.shape[0], 1))])
174
- if points.shape[1] < 7:
177
+ if points.shape[1] < self.dimensions * 2 + 1:
175
178
  raise ValueError("Tangent constraints must at least have X,Y,Z,tx,ty,tz")
176
179
  self.data["tangent"] = points
177
180
  self.up_to_date = False
@@ -181,13 +184,13 @@ class GeologicalInterpolator(metaclass=ABCMeta):
181
184
  self.up_to_date = False
182
185
 
183
186
  def set_value_inequality_constraints(self, points: np.ndarray):
184
- if points.shape[1] < 5:
187
+ if points.shape[1] < self.dimensions + 2:
185
188
  raise ValueError("Inequality constraints must at least have X,Y,Z,lower,upper")
186
189
  self.data["inequality"] = points
187
190
  self.up_to_date = False
188
191
 
189
192
  def set_inequality_pairs_constraints(self, points: np.ndarray):
190
- if points.shape[1] < 4:
193
+ if points.shape[1] < self.dimensions + 1:
191
194
  raise ValueError("Inequality pairs constraints must at least have X,Y,Z,rock_id")
192
195
 
193
196
  self.data["inequality_pairs"] = points
@@ -313,6 +316,20 @@ class GeologicalInterpolator(metaclass=ABCMeta):
313
316
  def add_interface_constraints(self, w: float = 1.0):
314
317
  pass
315
318
 
319
+ @abstractmethod
320
+ def add_value_inequality_constraints(self, w: float = 1.0):
321
+ pass
322
+
323
+ @abstractmethod
324
+ def add_inequality_pairs_constraints(
325
+ self,
326
+ w: float = 1.0,
327
+ upper_bound=np.finfo(float).eps,
328
+ lower_bound=-np.inf,
329
+ pairs: Optional[list] = None,
330
+ ):
331
+ pass
332
+
316
333
  def to_dict(self):
317
334
  return {
318
335
  "type": self.type,
@@ -181,7 +181,7 @@ class P1Interpolator(DiscreteInterpolator):
181
181
  points: np.ndarray,
182
182
  vectors: np.ndarray,
183
183
  w: float = 1.0,
184
- B: float = 0,
184
+ b: float = 0,
185
185
  name='undefined gradient orthogonal constraint',
186
186
  ):
187
187
  """
@@ -215,8 +215,8 @@ class P1Interpolator(DiscreteInterpolator):
215
215
  norm = np.linalg.norm(vectors, axis=1)
216
216
  vectors[norm > 0, :] /= norm[norm > 0, None]
217
217
  A = np.einsum("ij,ijk->ik", vectors[inside, :3], grad[inside, :, :])
218
- B = np.zeros(points[inside, :].shape[0]) + B
219
- self.add_constraints_to_least_squares(A, B, elements, w=wt, name="gradient orthogonal")
218
+ B = np.zeros(points[inside, :].shape[0]) + b
219
+ self.add_constraints_to_least_squares(A, B, elements, w=wt, name=name)
220
220
  if np.sum(inside) <= 0:
221
221
  logger.warning(
222
222
  f"{np.sum(~inside)} \