LoopStructural 1.6.2__py3-none-any.whl → 1.6.6__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 (69) hide show
  1. LoopStructural/datatypes/_bounding_box.py +77 -7
  2. LoopStructural/datatypes/_point.py +67 -7
  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/_builders.py +141 -141
  9. LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
  10. LoopStructural/interpolators/_discrete_interpolator.py +100 -53
  11. LoopStructural/interpolators/_finite_difference_interpolator.py +78 -88
  12. LoopStructural/interpolators/_geological_interpolator.py +27 -10
  13. LoopStructural/interpolators/_interpolator_builder.py +55 -0
  14. LoopStructural/interpolators/_interpolator_factory.py +7 -18
  15. LoopStructural/interpolators/_p1interpolator.py +3 -3
  16. LoopStructural/interpolators/_surfe_wrapper.py +42 -12
  17. LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
  18. LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
  19. LoopStructural/interpolators/supports/_3d_base_structured.py +28 -7
  20. LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
  21. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  22. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
  23. LoopStructural/interpolators/supports/__init__.py +7 -0
  24. LoopStructural/interpolators/supports/_base_support.py +7 -0
  25. LoopStructural/modelling/__init__.py +1 -3
  26. LoopStructural/modelling/core/geological_model.py +11 -12
  27. LoopStructural/modelling/features/__init__.py +1 -0
  28. LoopStructural/modelling/features/_analytical_feature.py +48 -18
  29. LoopStructural/modelling/features/_base_geological_feature.py +37 -8
  30. LoopStructural/modelling/features/_cross_product_geological_feature.py +7 -0
  31. LoopStructural/modelling/features/_geological_feature.py +50 -12
  32. LoopStructural/modelling/features/_projected_vector_feature.py +112 -0
  33. LoopStructural/modelling/features/_structural_frame.py +16 -18
  34. LoopStructural/modelling/features/_unconformity_feature.py +3 -3
  35. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  36. LoopStructural/modelling/features/builders/_folded_feature_builder.py +47 -16
  37. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  38. LoopStructural/modelling/features/builders/_structural_frame_builder.py +7 -2
  39. LoopStructural/modelling/features/fault/__init__.py +1 -1
  40. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  41. LoopStructural/modelling/features/fault/_fault_function_feature.py +3 -0
  42. LoopStructural/modelling/features/fault/_fault_segment.py +50 -53
  43. LoopStructural/modelling/features/fold/__init__.py +1 -2
  44. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  45. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  46. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  47. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  48. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  49. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  50. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  51. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  52. LoopStructural/modelling/input/process_data.py +47 -26
  53. LoopStructural/modelling/input/project_file.py +49 -23
  54. LoopStructural/modelling/intrusions/intrusion_feature.py +3 -0
  55. LoopStructural/utils/__init__.py +1 -0
  56. LoopStructural/utils/_surface.py +17 -5
  57. LoopStructural/utils/_transformation.py +98 -14
  58. LoopStructural/utils/colours.py +50 -0
  59. LoopStructural/utils/features.py +5 -0
  60. LoopStructural/utils/maths.py +51 -0
  61. LoopStructural/version.py +1 -1
  62. LoopStructural-1.6.6.dist-info/METADATA +160 -0
  63. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.6.dist-info}/RECORD +66 -59
  64. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.6.dist-info}/WHEEL +1 -1
  65. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  66. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  67. LoopStructural-1.6.2.dist-info/METADATA +0 -81
  68. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.6.dist-info}/LICENSE +0 -0
  69. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.6.dist-info}/top_level.txt +0 -0
@@ -1,149 +1,149 @@
1
- from LoopStructural.utils.exceptions import LoopException
2
- import numpy as np
3
- from typing import Optional
4
- from LoopStructural.interpolators import (
5
- P1Interpolator,
6
- P2Interpolator,
7
- FiniteDifferenceInterpolator,
8
- GeologicalInterpolator,
9
- DiscreteFoldInterpolator,
10
- StructuredGrid,
11
- TetMesh,
12
- )
13
- from LoopStructural.datatypes import BoundingBox
14
- from LoopStructural.utils.logging import getLogger
1
+ # from LoopStructural.utils.exceptions import LoopException
2
+ # import numpy as np
3
+ # from typing import Optional
4
+ # from LoopStructural.interpolators import (
5
+ # P1Interpolator,
6
+ # P2Interpolator,
7
+ # FiniteDifferenceInterpolator,
8
+ # GeologicalInterpolator,
9
+ # DiscreteFoldInterpolator,
10
+ # StructuredGrid,
11
+ # TetMesh,
12
+ # )
13
+ # from LoopStructural.datatypes import BoundingBox
14
+ # from LoopStructural.utils.logging import getLogger
15
15
 
16
- logger = getLogger(__name__)
16
+ # logger = getLogger(__name__)
17
17
 
18
18
 
19
- def get_interpolator(
20
- bounding_box: BoundingBox,
21
- interpolatortype: str,
22
- nelements: int,
23
- element_volume: Optional[float] = None,
24
- buffer: float = 0.2,
25
- dimensions: int = 3,
26
- support=None,
27
- ) -> GeologicalInterpolator:
28
- # add a buffer to the interpolation domain, this is necessary for
29
- # faults but also generally a good
30
- # idea to avoid boundary problems
31
- # buffer = bb[1, :]
32
- origin = bounding_box.with_buffer(buffer).origin
33
- maximum = bounding_box.with_buffer(buffer).maximum
34
- box_vol = np.prod(maximum - origin)
35
- if interpolatortype == "PLI":
36
- if support is None:
37
- if element_volume is None:
38
- # nelements /= 5
39
- element_volume = box_vol / nelements
40
- # calculate the step vector of a regular cube
41
- step_vector = np.zeros(3)
42
- step_vector[:] = element_volume ** (1.0 / 3.0)
43
- # step_vector /= np.array([1,1,2])
44
- # number of steps is the length of the box / step vector
45
- nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
46
- if np.any(np.less(nsteps, 3)):
47
- axis_labels = ["x", "y", "z"]
48
- for i in range(3):
49
- if nsteps[i] < 3:
50
- nsteps[i] = 3
51
- logger.error(
52
- f"Number of steps in direction {axis_labels[i]} is too small, try increasing nelements"
53
- )
54
- logger.error("Cannot create interpolator: number of steps is too small")
55
- raise ValueError("Number of steps too small cannot create interpolator")
19
+ # def get_interpolator(
20
+ # bounding_box: BoundingBox,
21
+ # interpolatortype: str,
22
+ # nelements: int,
23
+ # element_volume: Optional[float] = None,
24
+ # buffer: float = 0.2,
25
+ # dimensions: int = 3,
26
+ # support=None,
27
+ # ) -> GeologicalInterpolator:
28
+ # # add a buffer to the interpolation domain, this is necessary for
29
+ # # faults but also generally a good
30
+ # # idea to avoid boundary problems
31
+ # # buffer = bb[1, :]
32
+ # origin = bounding_box.with_buffer(buffer).origin
33
+ # maximum = bounding_box.with_buffer(buffer).maximum
34
+ # box_vol = np.prod(maximum - origin)
35
+ # if interpolatortype == "PLI":
36
+ # if support is None:
37
+ # if element_volume is None:
38
+ # # nelements /= 5
39
+ # element_volume = box_vol / nelements
40
+ # # calculate the step vector of a regular cube
41
+ # step_vector = np.zeros(3)
42
+ # step_vector[:] = element_volume ** (1.0 / 3.0)
43
+ # # step_vector /= np.array([1,1,2])
44
+ # # number of steps is the length of the box / step vector
45
+ # nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
46
+ # if np.any(np.less(nsteps, 3)):
47
+ # axis_labels = ["x", "y", "z"]
48
+ # for i in range(3):
49
+ # if nsteps[i] < 3:
50
+ # nsteps[i] = 3
51
+ # logger.error(
52
+ # f"Number of steps in direction {axis_labels[i]} is too small, try increasing nelements"
53
+ # )
54
+ # logger.error("Cannot create interpolator: number of steps is too small")
55
+ # raise ValueError("Number of steps too small cannot create interpolator")
56
56
 
57
- support = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector)
58
- logger.info(
59
- "Creating regular tetrahedron mesh with %i elements \n"
60
- "for modelling using PLI" % (support.ntetra)
61
- )
57
+ # support = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector)
58
+ # logger.info(
59
+ # "Creating regular tetrahedron mesh with %i elements \n"
60
+ # "for modelling using PLI" % (support.ntetra)
61
+ # )
62
62
 
63
- return P1Interpolator(support)
64
- if interpolatortype == "P2":
65
- if support is not None:
66
- logger.info(
67
- "Creating regular tetrahedron mesh with %i elements \n"
68
- "for modelling using P2" % (support.ntetra)
69
- )
70
- return P2Interpolator(support)
71
- else:
72
- raise ValueError("Cannot create P2 interpolator without support, try using PLI")
63
+ # return P1Interpolator(support)
64
+ # if interpolatortype == "P2":
65
+ # if support is not None:
66
+ # logger.info(
67
+ # "Creating regular tetrahedron mesh with %i elements \n"
68
+ # "for modelling using P2" % (support.ntetra)
69
+ # )
70
+ # return P2Interpolator(support)
71
+ # else:
72
+ # raise ValueError("Cannot create P2 interpolator without support, try using PLI")
73
73
 
74
- if interpolatortype == "FDI":
75
- # find the volume of one element
76
- if element_volume is None:
77
- element_volume = box_vol / nelements
78
- # calculate the step vector of a regular cube
79
- step_vector = np.zeros(3)
80
- step_vector[:] = element_volume ** (1.0 / 3.0)
81
- # number of steps is the length of the box / step vector
82
- nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
83
- if np.any(np.less(nsteps, 3)):
84
- logger.error("Cannot create interpolator: number of steps is too small")
85
- axis_labels = ["x", "y", "z"]
86
- for i in range(3):
87
- if nsteps[i] < 3:
88
- nsteps[i] = 3
89
- # logger.error(
90
- # f"Number of steps in direction {axis_labels[i]} is too small, try increasing nelements"
91
- # )
92
- # raise ValueError("Number of steps too small cannot create interpolator")
93
- # create a structured grid using the origin and number of steps
74
+ # if interpolatortype == "FDI":
75
+ # # find the volume of one element
76
+ # if element_volume is None:
77
+ # element_volume = box_vol / nelements
78
+ # # calculate the step vector of a regular cube
79
+ # step_vector = np.zeros(3)
80
+ # step_vector[:] = element_volume ** (1.0 / 3.0)
81
+ # # number of steps is the length of the box / step vector
82
+ # nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
83
+ # if np.any(np.less(nsteps, 3)):
84
+ # logger.error("Cannot create interpolator: number of steps is too small")
85
+ # axis_labels = ["x", "y", "z"]
86
+ # for i in range(3):
87
+ # if nsteps[i] < 3:
88
+ # nsteps[i] = 3
89
+ # # logger.error(
90
+ # # f"Number of steps in direction {axis_labels[i]} is too small, try increasing nelements"
91
+ # # )
92
+ # # raise ValueError("Number of steps too small cannot create interpolator")
93
+ # # create a structured grid using the origin and number of steps
94
94
 
95
- grid = StructuredGrid(origin=origin, nsteps=nsteps, step_vector=step_vector)
96
- logger.info(
97
- f"Creating regular grid with {grid.n_elements} elements \n" "for modelling using FDI"
98
- )
99
- return FiniteDifferenceInterpolator(grid)
100
- if interpolatortype == "DFI":
101
- if element_volume is None:
102
- nelements /= 5
103
- element_volume = box_vol / nelements
104
- # calculate the step vector of a regular cube
105
- step_vector = np.zeros(3)
106
- step_vector[:] = element_volume ** (1.0 / 3.0)
107
- # number of steps is the length of the box / step vector
108
- nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
109
- # create a structured grid using the origin and number of steps
95
+ # grid = StructuredGrid(origin=origin, nsteps=nsteps, step_vector=step_vector)
96
+ # logger.info(
97
+ # f"Creating regular grid with {grid.n_elements} elements \n" "for modelling using FDI"
98
+ # )
99
+ # return FiniteDifferenceInterpolator(grid)
100
+ # if interpolatortype == "DFI":
101
+ # if element_volume is None:
102
+ # nelements /= 5
103
+ # element_volume = box_vol / nelements
104
+ # # calculate the step vector of a regular cube
105
+ # step_vector = np.zeros(3)
106
+ # step_vector[:] = element_volume ** (1.0 / 3.0)
107
+ # # number of steps is the length of the box / step vector
108
+ # nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
109
+ # # create a structured grid using the origin and number of steps
110
110
 
111
- mesh = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector)
112
- logger.info(
113
- f"Creating regular tetrahedron mesh with {mesh.ntetra} elements \n"
114
- "for modelling using DFI"
115
- )
116
- return DiscreteFoldInterpolator(mesh, None)
117
- raise LoopException("No interpolator")
118
- # fi interpolatortype == "DFI" and dfi is True:
119
- # if element_volume is None:
120
- # nelements /= 5
121
- # element_volume = box_vol / nelements
122
- # # calculate the step vector of a regular cube
123
- # step_vector = np.zeros(3)
124
- # step_vector[:] = element_volume ** (1.0 / 3.0)
125
- # # number of steps is the length of the box / step vector
126
- # nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
127
- # # create a structured grid using the origin and number of steps
128
- # if "meshbuilder" in kwargs:
129
- # mesh = kwargs["meshbuilder"].build(bb, nelements)
130
- # else:
131
- # mesh = kwargs.get(
132
- # "mesh",
133
- # TetMesh(origin=bb[0, :], nsteps=nsteps, step_vector=step_vector),
134
- # )
135
- # logger.info(
136
- # f"Creating regular tetrahedron mesh with {mesh.ntetra} elements \n"
137
- # "for modelling using DFI"
138
- # )
139
- # return DFI(mesh, kwargs["fold"])
140
- # if interpolatortype == "Surfe" or interpolatortype == "surfe":
141
- # # move import of surfe to where we actually try and use it
142
- # if not surfe:
143
- # logger.warning("Cannot import Surfe, try another interpolator")
144
- # raise ImportError("Cannot import surfepy, try pip install surfe")
145
- # method = kwargs.get("method", "single_surface")
146
- # logger.info("Using surfe interpolator")
147
- # return SurfeRBFInterpolator(method)
148
- # logger.warning("No interpolator")
149
- # raise InterpolatorError("Could not create interpolator")
111
+ # mesh = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector)
112
+ # logger.info(
113
+ # f"Creating regular tetrahedron mesh with {mesh.ntetra} elements \n"
114
+ # "for modelling using DFI"
115
+ # )
116
+ # return DiscreteFoldInterpolator(mesh, None)
117
+ # raise LoopException("No interpolator")
118
+ # # fi interpolatortype == "DFI" and dfi is True:
119
+ # # if element_volume is None:
120
+ # # nelements /= 5
121
+ # # element_volume = box_vol / nelements
122
+ # # # calculate the step vector of a regular cube
123
+ # # step_vector = np.zeros(3)
124
+ # # step_vector[:] = element_volume ** (1.0 / 3.0)
125
+ # # # number of steps is the length of the box / step vector
126
+ # # nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
127
+ # # # create a structured grid using the origin and number of steps
128
+ # # if "meshbuilder" in kwargs:
129
+ # # mesh = kwargs["meshbuilder"].build(bb, nelements)
130
+ # # else:
131
+ # # mesh = kwargs.get(
132
+ # # "mesh",
133
+ # # TetMesh(origin=bb[0, :], nsteps=nsteps, step_vector=step_vector),
134
+ # # )
135
+ # # logger.info(
136
+ # # f"Creating regular tetrahedron mesh with {mesh.ntetra} elements \n"
137
+ # # "for modelling using DFI"
138
+ # # )
139
+ # # return DFI(mesh, kwargs["fold"])
140
+ # # if interpolatortype == "Surfe" or interpolatortype == "surfe":
141
+ # # # move import of surfe to where we actually try and use it
142
+ # # if not surfe:
143
+ # # logger.warning("Cannot import Surfe, try another interpolator")
144
+ # # raise ImportError("Cannot import surfepy, try pip install surfe")
145
+ # # method = kwargs.get("method", "single_surface")
146
+ # # logger.info("Using surfe interpolator")
147
+ # # return SurfeRBFInterpolator(method)
148
+ # # logger.warning("No interpolator")
149
+ # # raise InterpolatorError("Could not create interpolator")
@@ -2,7 +2,7 @@
2
2
  Piecewise linear interpolator using folds
3
3
  """
4
4
 
5
- from typing import Optional
5
+ from typing import Optional, Callable
6
6
 
7
7
  import numpy as np
8
8
 
@@ -64,6 +64,7 @@ class DiscreteFoldInterpolator(PiecewiseLinearInterpolator):
64
64
  fold_normalisation=1.0,
65
65
  fold_norm=1.0,
66
66
  step=2,
67
+ mask_fn: Optional[Callable] = None,
67
68
  ):
68
69
  """
69
70
 
@@ -104,6 +105,10 @@ class DiscreteFoldInterpolator(PiecewiseLinearInterpolator):
104
105
  # calculate element volume for weighting
105
106
  vecs = nodes[:, 1:, :] - nodes[:, 0, None, :]
106
107
  vol = np.abs(np.linalg.det(vecs)) / 6
108
+ weight = np.ones(self.support.n_elements, dtype=float)
109
+ if mask_fn is not None:
110
+ weight[mask_fn(self.support.barycentre)] = 0
111
+ weight = weight[::step]
107
112
  if fold_orientation is not None:
108
113
  """
109
114
  dot product between vector in deformed ori plane = 0
@@ -120,7 +125,7 @@ class DiscreteFoldInterpolator(PiecewiseLinearInterpolator):
120
125
  B = np.zeros(A.shape[0])
121
126
  idc = self.support.get_elements()[element_idx[::step], :]
122
127
  self.add_constraints_to_least_squares(
123
- A, B, idc, w=fold_orientation, name="fold orientation"
128
+ A, B, idc, w=weight * fold_orientation, name="fold orientation"
124
129
  )
125
130
 
126
131
  if fold_axis_w is not None:
@@ -139,7 +144,9 @@ class DiscreteFoldInterpolator(PiecewiseLinearInterpolator):
139
144
  B = np.zeros(A.shape[0]).tolist()
140
145
  idc = self.support.get_elements()[element_idx[::step], :]
141
146
 
142
- self.add_constraints_to_least_squares(A, B, idc, w=fold_axis_w, name="fold axis")
147
+ self.add_constraints_to_least_squares(
148
+ A, B, idc, w=weight * fold_axis_w, name="fold axis"
149
+ )
143
150
 
144
151
  if fold_normalisation is not None:
145
152
  """
@@ -160,7 +167,7 @@ class DiscreteFoldInterpolator(PiecewiseLinearInterpolator):
160
167
  idc = self.support.get_elements()[element_idx[::step], :]
161
168
 
162
169
  self.add_constraints_to_least_squares(
163
- A, B, idc, w=fold_normalisation, name="fold normalisation"
170
+ A, B, idc, w=weight * fold_normalisation, name="fold normalisation"
164
171
  )
165
172
 
166
173
  if fold_regularisation is not None:
@@ -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