LoopStructural 1.6.1__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 (129) hide show
  1. LoopStructural/__init__.py +52 -0
  2. LoopStructural/datasets/__init__.py +23 -0
  3. LoopStructural/datasets/_base.py +301 -0
  4. LoopStructural/datasets/_example_models.py +10 -0
  5. LoopStructural/datasets/data/claudius.csv +21049 -0
  6. LoopStructural/datasets/data/claudiusbb.txt +2 -0
  7. LoopStructural/datasets/data/duplex.csv +126 -0
  8. LoopStructural/datasets/data/duplexbb.txt +2 -0
  9. LoopStructural/datasets/data/fault_trace/fault_trace.cpg +1 -0
  10. LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
  11. LoopStructural/datasets/data/fault_trace/fault_trace.prj +1 -0
  12. LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
  13. LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
  14. LoopStructural/datasets/data/geological_map_data/bbox.csv +2 -0
  15. LoopStructural/datasets/data/geological_map_data/contacts.csv +657 -0
  16. LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +7 -0
  17. LoopStructural/datasets/data/geological_map_data/fault_edges.txt +2 -0
  18. LoopStructural/datasets/data/geological_map_data/fault_locations.csv +79 -0
  19. LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +19 -0
  20. LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +13 -0
  21. LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +207 -0
  22. LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +13 -0
  23. LoopStructural/datasets/data/intrusion.csv +1017 -0
  24. LoopStructural/datasets/data/intrusionbb.txt +2 -0
  25. LoopStructural/datasets/data/onefoldbb.txt +2 -0
  26. LoopStructural/datasets/data/onefolddata.csv +2226 -0
  27. LoopStructural/datasets/data/refolded_bb.txt +2 -0
  28. LoopStructural/datasets/data/refolded_fold.csv +205 -0
  29. LoopStructural/datasets/data/tabular_intrusion.csv +23 -0
  30. LoopStructural/datatypes/__init__.py +4 -0
  31. LoopStructural/datatypes/_bounding_box.py +422 -0
  32. LoopStructural/datatypes/_point.py +166 -0
  33. LoopStructural/datatypes/_structured_grid.py +94 -0
  34. LoopStructural/datatypes/_surface.py +184 -0
  35. LoopStructural/export/exporters.py +554 -0
  36. LoopStructural/export/file_formats.py +15 -0
  37. LoopStructural/export/geoh5.py +100 -0
  38. LoopStructural/export/gocad.py +126 -0
  39. LoopStructural/export/omf_wrapper.py +88 -0
  40. LoopStructural/interpolators/__init__.py +105 -0
  41. LoopStructural/interpolators/_api.py +143 -0
  42. LoopStructural/interpolators/_builders.py +149 -0
  43. LoopStructural/interpolators/_cython/__init__.py +0 -0
  44. LoopStructural/interpolators/_discrete_fold_interpolator.py +183 -0
  45. LoopStructural/interpolators/_discrete_interpolator.py +692 -0
  46. LoopStructural/interpolators/_finite_difference_interpolator.py +470 -0
  47. LoopStructural/interpolators/_geological_interpolator.py +380 -0
  48. LoopStructural/interpolators/_interpolator_factory.py +89 -0
  49. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  50. LoopStructural/interpolators/_operator.py +38 -0
  51. LoopStructural/interpolators/_p1interpolator.py +228 -0
  52. LoopStructural/interpolators/_p2interpolator.py +277 -0
  53. LoopStructural/interpolators/_surfe_wrapper.py +174 -0
  54. LoopStructural/interpolators/supports/_2d_base_unstructured.py +340 -0
  55. LoopStructural/interpolators/supports/_2d_p1_unstructured.py +68 -0
  56. LoopStructural/interpolators/supports/_2d_p2_unstructured.py +288 -0
  57. LoopStructural/interpolators/supports/_2d_structured_grid.py +462 -0
  58. LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
  59. LoopStructural/interpolators/supports/_3d_base_structured.py +467 -0
  60. LoopStructural/interpolators/supports/_3d_p2_tetra.py +331 -0
  61. LoopStructural/interpolators/supports/_3d_structured_grid.py +470 -0
  62. LoopStructural/interpolators/supports/_3d_structured_tetra.py +746 -0
  63. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +637 -0
  64. LoopStructural/interpolators/supports/__init__.py +55 -0
  65. LoopStructural/interpolators/supports/_aabb.py +77 -0
  66. LoopStructural/interpolators/supports/_base_support.py +114 -0
  67. LoopStructural/interpolators/supports/_face_table.py +70 -0
  68. LoopStructural/interpolators/supports/_support_factory.py +32 -0
  69. LoopStructural/modelling/__init__.py +29 -0
  70. LoopStructural/modelling/core/__init__.py +0 -0
  71. LoopStructural/modelling/core/geological_model.py +1867 -0
  72. LoopStructural/modelling/features/__init__.py +32 -0
  73. LoopStructural/modelling/features/_analytical_feature.py +79 -0
  74. LoopStructural/modelling/features/_base_geological_feature.py +364 -0
  75. LoopStructural/modelling/features/_cross_product_geological_feature.py +100 -0
  76. LoopStructural/modelling/features/_geological_feature.py +288 -0
  77. LoopStructural/modelling/features/_lambda_geological_feature.py +93 -0
  78. LoopStructural/modelling/features/_region.py +18 -0
  79. LoopStructural/modelling/features/_structural_frame.py +186 -0
  80. LoopStructural/modelling/features/_unconformity_feature.py +83 -0
  81. LoopStructural/modelling/features/builders/__init__.py +5 -0
  82. LoopStructural/modelling/features/builders/_base_builder.py +111 -0
  83. LoopStructural/modelling/features/builders/_fault_builder.py +590 -0
  84. LoopStructural/modelling/features/builders/_folded_feature_builder.py +129 -0
  85. LoopStructural/modelling/features/builders/_geological_feature_builder.py +543 -0
  86. LoopStructural/modelling/features/builders/_structural_frame_builder.py +237 -0
  87. LoopStructural/modelling/features/fault/__init__.py +3 -0
  88. LoopStructural/modelling/features/fault/_fault_function.py +444 -0
  89. LoopStructural/modelling/features/fault/_fault_function_feature.py +82 -0
  90. LoopStructural/modelling/features/fault/_fault_segment.py +505 -0
  91. LoopStructural/modelling/features/fold/__init__.py +9 -0
  92. LoopStructural/modelling/features/fold/_fold.py +167 -0
  93. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +149 -0
  94. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +67 -0
  95. LoopStructural/modelling/features/fold/_foldframe.py +194 -0
  96. LoopStructural/modelling/features/fold/_svariogram.py +188 -0
  97. LoopStructural/modelling/input/__init__.py +2 -0
  98. LoopStructural/modelling/input/fault_network.py +80 -0
  99. LoopStructural/modelling/input/map2loop_processor.py +165 -0
  100. LoopStructural/modelling/input/process_data.py +650 -0
  101. LoopStructural/modelling/input/project_file.py +84 -0
  102. LoopStructural/modelling/intrusions/__init__.py +25 -0
  103. LoopStructural/modelling/intrusions/geom_conceptual_models.py +142 -0
  104. LoopStructural/modelling/intrusions/geometric_scaling_functions.py +123 -0
  105. LoopStructural/modelling/intrusions/intrusion_builder.py +672 -0
  106. LoopStructural/modelling/intrusions/intrusion_feature.py +410 -0
  107. LoopStructural/modelling/intrusions/intrusion_frame_builder.py +971 -0
  108. LoopStructural/modelling/intrusions/intrusion_support_functions.py +460 -0
  109. LoopStructural/utils/__init__.py +38 -0
  110. LoopStructural/utils/_surface.py +143 -0
  111. LoopStructural/utils/_transformation.py +76 -0
  112. LoopStructural/utils/config.py +18 -0
  113. LoopStructural/utils/dtm_creator.py +17 -0
  114. LoopStructural/utils/exceptions.py +31 -0
  115. LoopStructural/utils/helper.py +292 -0
  116. LoopStructural/utils/json_encoder.py +18 -0
  117. LoopStructural/utils/linalg.py +8 -0
  118. LoopStructural/utils/logging.py +79 -0
  119. LoopStructural/utils/maths.py +245 -0
  120. LoopStructural/utils/regions.py +103 -0
  121. LoopStructural/utils/typing.py +7 -0
  122. LoopStructural/utils/utils.py +68 -0
  123. LoopStructural/version.py +1 -0
  124. LoopStructural/visualisation/__init__.py +11 -0
  125. LoopStructural-1.6.1.dist-info/LICENSE +21 -0
  126. LoopStructural-1.6.1.dist-info/METADATA +81 -0
  127. LoopStructural-1.6.1.dist-info/RECORD +129 -0
  128. LoopStructural-1.6.1.dist-info/WHEEL +5 -0
  129. LoopStructural-1.6.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,692 @@
1
+ """
2
+ Discrete interpolator base for least squares
3
+ """
4
+
5
+ from abc import abstractmethod
6
+ from typing import Callable, Optional, Union
7
+ import logging
8
+
9
+ from time import time
10
+ import numpy as np
11
+ from scipy import sparse # import sparse.coo_matrix, sparse.bmat, sparse.eye
12
+ from ..interpolators import InterpolatorType
13
+
14
+ from ..interpolators import GeologicalInterpolator
15
+ from ..utils import getLogger
16
+
17
+ logger = getLogger(__name__)
18
+
19
+
20
+ class DiscreteInterpolator(GeologicalInterpolator):
21
+ """ """
22
+
23
+ def __init__(self, support, data={}, c=None, up_to_date=False):
24
+ """
25
+ Base class for a discrete interpolator e.g. piecewise linear or finite difference which is
26
+ any interpolator that solves the system using least squares approximation
27
+
28
+ Parameters
29
+ ----------
30
+ support
31
+ A discrete mesh with, nodes, elements, etc
32
+ """
33
+ GeologicalInterpolator.__init__(self, data=data, up_to_date=up_to_date)
34
+ self.B = []
35
+ self.support = support
36
+ self.c = (
37
+ np.array(c)
38
+ if c is not None and np.array(c).shape[0] == self.support.n_nodes
39
+ else np.zeros(self.support.n_nodes)
40
+ )
41
+ self.region_function = lambda xyz: np.ones(xyz.shape[0], dtype=bool)
42
+
43
+ self.shape = "rectangular"
44
+ if self.shape == "square":
45
+ self.B = np.zeros(self.nx)
46
+ self.c_ = 0
47
+
48
+ self.solver = "cg"
49
+
50
+ self.eq_const_C = []
51
+ self.eq_const_row = []
52
+ self.eq_const_col = []
53
+ self.eq_const_d = []
54
+
55
+ self.equal_constraints = {}
56
+ self.eq_const_c = 0
57
+ self.ineq_constraints = {}
58
+ self.ineq_const_c = 0
59
+
60
+ self.non_linear_constraints = []
61
+ self.constraints = {}
62
+ self.interpolation_weights = {}
63
+ logger.info("Creating discrete interpolator with {} degrees of freedom".format(self.nx))
64
+ self.type = InterpolatorType.BASE_DISCRETE
65
+ self.c = np.zeros(self.support.n_nodes)
66
+
67
+ @property
68
+ def nx(self) -> int:
69
+ """Number of degrees of freedom for the interpolator
70
+
71
+ Returns
72
+ -------
73
+ int
74
+ number of degrees of freedom, positve
75
+ """
76
+ return len(self.support.nodes[self.region])
77
+
78
+ @property
79
+ def region(self) -> np.ndarray:
80
+ """The active region of the interpolator. A boolean
81
+ mask for all elements that are interpolated
82
+
83
+ Returns
84
+ -------
85
+ np.ndarray
86
+
87
+ """
88
+
89
+ return self.region_function(self.support.nodes).astype(bool)
90
+
91
+ @property
92
+ def region_map(self):
93
+ region_map = np.zeros(self.support.n_nodes).astype(int)
94
+ region_map[self.region] = np.array(range(0, len(region_map[self.region])))
95
+ return region_map
96
+
97
+ def set_region(self, region=None):
98
+ """
99
+ Set the region of the support the interpolator is working on
100
+
101
+ Parameters
102
+ ----------
103
+ region - function(position)
104
+ return true when in region, false when out
105
+
106
+ Returns
107
+ -------
108
+
109
+ """
110
+ # evaluate the region function on the support to determine
111
+ # which nodes are inside update region map and degrees of freedom
112
+ # self.region_function = region
113
+ logger.info(
114
+ "Cannot use region at the moment. Interpolation now uses region and has {} degrees of freedom".format(
115
+ self.nx
116
+ )
117
+ )
118
+
119
+ def set_interpolation_weights(self, weights):
120
+ """
121
+ Set the interpolation weights dictionary
122
+
123
+ Parameters
124
+ ----------
125
+ weights - dictionary
126
+ Entry of new weights to assign to self.interpolation_weights
127
+
128
+ Returns
129
+ -------
130
+
131
+ """
132
+ for key in weights:
133
+ self.up_to_date = False
134
+ self.interpolation_weights[key] = weights[key]
135
+
136
+ def reset(self):
137
+ """
138
+ Reset the interpolation constraints
139
+
140
+ """
141
+ self.constraints = {}
142
+ self.c_ = 0
143
+ logger.debug("Resetting interpolation constraints")
144
+
145
+ def add_constraints_to_least_squares(self, A, B, idc, w=1.0, name="undefined"):
146
+ """
147
+ Adds constraints to the least squares system. Automatically works
148
+ out the row
149
+ index given the shape of the input arrays
150
+
151
+ Parameters
152
+ ----------
153
+ A : numpy array / list
154
+ RxC numpy array of constraints where C is number of columns,R rows
155
+ B : numpy array /list
156
+ B values array length R
157
+ idc : numpy array/list
158
+ RxC column index
159
+
160
+ Returns
161
+ -------
162
+ list of constraint ids
163
+
164
+ """
165
+ A = np.array(A)
166
+ B = np.array(B)
167
+ idc = np.array(idc)
168
+ n_rows = A.shape[0]
169
+ # logger.debug('Adding constraints to interpolator: {} {} {}'.format(A.shape[0]))
170
+ # print(A.shape,B.shape,idc.shape)
171
+ if A.shape != idc.shape:
172
+ logger.error(f"Cannot add constraints: A and indexes have different shape : {name}")
173
+ return
174
+
175
+ if len(A.shape) > 2:
176
+ n_rows = A.shape[0] * A.shape[1]
177
+ if isinstance(w, np.ndarray):
178
+ w = np.tile(w, (A.shape[1]))
179
+ A = A.reshape((A.shape[0] * A.shape[1], A.shape[2]))
180
+ idc = idc.reshape((idc.shape[0] * idc.shape[1], idc.shape[2]))
181
+ B = B.reshape((A.shape[0]))
182
+ # w = w.reshape((A.shape[0]))
183
+ # normalise by rows of A
184
+ length = np.linalg.norm(A, axis=1) # .getcol(0).norm()
185
+ B[length > 0] /= length[length > 0]
186
+ # going to assume if any are nan they are all nan
187
+ mask = np.any(np.isnan(A), axis=1)
188
+ A[mask, :] = 0
189
+ A[length > 0, :] /= length[length > 0, None]
190
+ if isinstance(w, (float, int)):
191
+ w = np.ones(A.shape[0]) * w
192
+ if not isinstance(w, np.ndarray):
193
+ raise BaseException("w must be a numpy array")
194
+
195
+ 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
+ raise BaseException("Weight array does not match number of constraints")
200
+ if np.any(np.isnan(idc)) or np.any(np.isnan(A)) or np.any(np.isnan(B)):
201
+ logger.warning("Constraints contain nan not adding constraints: {}".format(name))
202
+ # return
203
+ rows = np.arange(0, n_rows).astype(int)
204
+ base_name = name
205
+ while name in self.constraints:
206
+ count = 0
207
+ if "_" in name:
208
+ count = int(name.split("_")[1]) + 1
209
+ name = base_name + "_{}".format(count)
210
+
211
+ rows = np.tile(rows, (A.shape[-1], 1)).T
212
+ self.constraints[name] = {
213
+ 'matrix': sparse.coo_matrix(
214
+ (A.flatten(), (rows.flatten(), idc.flatten())), shape=(n_rows, self.nx)
215
+ ).tocsc(),
216
+ 'b': B.flatten(),
217
+ 'w': w,
218
+ }
219
+
220
+ @abstractmethod
221
+ def add_gradient_orthogonal_constraints(
222
+ self, points: np.ndarray, vectors: np.ndarray, w: float = 1.0
223
+ ):
224
+ pass
225
+
226
+ def calculate_residual_for_constraints(self):
227
+ """Calculates Ax-B for all constraints added to the interpolator
228
+ This could be a proxy to identify which constraints are controlling the model
229
+
230
+ Returns
231
+ -------
232
+ np.ndarray
233
+ vector of Ax-B
234
+ """
235
+ residuals = {}
236
+ for constraint_name, constraint in self.constraints:
237
+ residuals[constraint_name] = (
238
+ np.einsum("ij,ij->i", constraint["A"], self.c[constraint["idc"].astype(int)])
239
+ - constraint["B"].flatten()
240
+ )
241
+ return residuals
242
+
243
+ def add_inequality_constraints_to_matrix(
244
+ self, A: np.ndarray, bounds: np.ndarray, idc: np.ndarray, name: str = "undefined"
245
+ ):
246
+ """Adds constraints for a matrix where the linear function
247
+ l < Ax > u constrains the objective function
248
+
249
+
250
+ Parameters
251
+ ----------
252
+ A : numpy array
253
+ matrix of coefficients
254
+ bounds : numpy array
255
+ nx3 lower, upper, 1
256
+ idc : numpy array
257
+ index of constraints in the matrix
258
+ Returns
259
+ -------
260
+
261
+ """
262
+ # map from mesh node index to region node index
263
+ gi = np.zeros(self.support.n_nodes, dtype=int)
264
+ gi[:] = -1
265
+ gi[self.region] = np.arange(0, self.nx, dtype=int)
266
+ idc = gi[idc]
267
+ rows = np.arange(0, idc.shape[0])
268
+ rows = np.tile(rows, (A.shape[-1], 1)).T
269
+
270
+ self.ineq_constraints[name] = {
271
+ 'matrix': sparse.coo_matrix(
272
+ (A.flatten(), (rows.flatten(), idc.flatten())), shape=(rows.shape[0], self.nx)
273
+ ).tocsc(),
274
+ "bounds": bounds,
275
+ }
276
+
277
+ def add_value_inequality_constraints(self, w: float = 1.0):
278
+ points = self.get_inequality_value_constraints()
279
+ # check that we have added some points
280
+ if points.shape[0] > 0:
281
+ vertices, a, element, inside = self.support.get_element_for_location(points)
282
+ rows = np.arange(0, points[inside, :].shape[0], dtype=int)
283
+ rows = np.tile(rows, (a.shape[-1], 1)).T
284
+ a = a[inside]
285
+ cols = self.support.elements[element[inside]]
286
+ self.add_inequality_constraints_to_matrix(a, points[:, 3:5], cols, 'inequality_value')
287
+
288
+ def add_inequality_pairs_constraints(
289
+ self, w: float = 1.0, upper_bound=np.finfo(float).eps, lower_bound=-np.inf
290
+ ):
291
+
292
+ points = self.get_inequality_pairs_constraints()
293
+ if points.shape[0] > 0:
294
+
295
+ # assemble a list of pairs in the model
296
+ # this will make pairs even across stratigraphic boundaries
297
+ # 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())
308
+ for pair in pairs:
309
+ upper_points = points[points[:, self.support.dimension] == pair[0]]
310
+ lower_points = points[points[:, self.support.dimension] == pair[1]]
311
+
312
+ upper_interpolation = self.support.get_element_for_location(upper_points)
313
+ lower_interpolation = self.support.get_element_for_location(lower_points)
314
+ ij = np.array(
315
+ [
316
+ *np.meshgrid(
317
+ np.arange(0, int(upper_interpolation[3].sum()), dtype=int),
318
+ np.arange(0, int(lower_interpolation[3].sum()), dtype=int),
319
+ )
320
+ ],
321
+ dtype=int,
322
+ )
323
+
324
+ ij = ij.reshape(2, -1).T
325
+ rows = np.arange(0, ij.shape[0], dtype=int)
326
+ rows = np.tile(rows, (upper_interpolation[1].shape[-1], 1)).T
327
+ rows = np.hstack([rows, rows])
328
+ a = upper_interpolation[1][upper_interpolation[3]][ij[:, 0]] # np.ones(ij.shape[0])
329
+ a = np.hstack([a, -lower_interpolation[1][lower_interpolation[3]][ij[:, 1]]])
330
+ cols = np.hstack(
331
+ [
332
+ self.support.elements[
333
+ upper_interpolation[2][upper_interpolation[3]][ij[:, 0]]
334
+ ],
335
+ self.support.elements[
336
+ lower_interpolation[2][lower_interpolation[3]][ij[:, 1]]
337
+ ],
338
+ ]
339
+ )
340
+
341
+ bounds = np.zeros((ij.shape[0], 2))
342
+ bounds[:, 0] = lower_bound
343
+ bounds[:, 1] = upper_bound
344
+
345
+ self.add_inequality_constraints_to_matrix(
346
+ a, bounds, cols, f'inequality_pairs_{pair[0]}_{pair[1]}'
347
+ )
348
+
349
+ def add_inequality_feature(
350
+ self,
351
+ feature: Callable[[np.ndarray], np.ndarray],
352
+ lower: bool = True,
353
+ mask: Optional[np.ndarray] = None,
354
+ ):
355
+ """Add an inequality constraint to the interpolator using an existing feature.
356
+ This will make the interpolator greater than or less than the exising feature.
357
+ Evaluate the feature at the interpolation nodes.
358
+ Can provide a boolean mask to restrict to only some parts
359
+
360
+ Parameters
361
+ ----------
362
+ feature : BaseFeature
363
+ the feature that will be used to constraint the interpolator
364
+ lower : bool, optional
365
+ lower or upper constraint, by default True
366
+ mask : np.ndarray, optional
367
+ restrict the nodes to evaluate on, by default None
368
+ """
369
+ # add inequality value for the nodes of the mesh
370
+ # flag lower determines whether the feature is a lower bound or upper bound
371
+ # mask is just a boolean array determining which nodes to apply it to
372
+
373
+ value = feature(self.support.nodes)
374
+ if mask is None:
375
+ mask = np.ones(value.shape[0], dtype=bool)
376
+ l = np.zeros(value.shape[0]) - np.inf
377
+ u = np.zeros(value.shape[0]) + np.inf
378
+ mask = np.logical_and(mask, ~np.isnan(value))
379
+ if lower:
380
+ l[mask] = value[mask]
381
+ if not lower:
382
+ u[mask] = value[mask]
383
+
384
+ self.add_inequality_constraints_to_matrix(
385
+ np.ones((value.shape[0], 1)),
386
+ l,
387
+ u,
388
+ np.arange(0, self.nx, dtype=int),
389
+ )
390
+
391
+ def add_equality_constraints(self, node_idx, values, name="undefined"):
392
+ """
393
+ Adds hard constraints to the least squares system. For now this just
394
+ sets
395
+ the node values to be fixed using a lagrangian.
396
+
397
+ Parameters
398
+ ----------
399
+ node_idx : numpy array/list
400
+ int array of node indexes
401
+ values : numpy array/list
402
+ array of node values
403
+
404
+ Returns
405
+ -------
406
+
407
+ """
408
+ # map from mesh node index to region node index
409
+ gi = np.zeros(self.support.n_nodes)
410
+ gi[:] = -1
411
+ gi[self.region] = np.arange(0, self.nx)
412
+ idc = gi[node_idx]
413
+ outside = ~(idc == -1)
414
+
415
+ self.equal_constraints[name] = {
416
+ "A": np.ones(idc[outside].shape[0]),
417
+ "B": values[outside],
418
+ "col": idc[outside],
419
+ # "w": w,
420
+ "row": np.arange(self.eq_const_c, self.eq_const_c + idc[outside].shape[0]),
421
+ }
422
+ self.eq_const_c += idc[outside].shape[0]
423
+
424
+ def add_tangent_constraints(self, w=1.0):
425
+ """Adds the constraints :math:`f(X)\cdotT=0`
426
+
427
+ Parameters
428
+ ----------
429
+ w : double
430
+
431
+
432
+ Returns
433
+ -------
434
+
435
+ """
436
+ points = self.get_tangent_constraints()
437
+ if points.shape[0] > 1:
438
+ self.add_gradient_orthogonal_constraints(points[:, :3], points[:, 3:6], w)
439
+
440
+ def build_matrix(self):
441
+ """
442
+ Assemble constraints into interpolation matrix. Adds equaltiy
443
+ constraints
444
+ using lagrange modifiers if necessary
445
+
446
+ Parameters
447
+ ----------
448
+ damp: bool
449
+ Flag whether damping should be added to the diagonal of the matrix
450
+ Returns
451
+ -------
452
+ Interpolation matrix and B
453
+ """
454
+
455
+ mats = []
456
+ bs = []
457
+ for c in self.constraints.values():
458
+ if len(c["w"]) == 0:
459
+ continue
460
+ mats.append(c['matrix'].multiply(c['w'][:, None]))
461
+ bs.append(c['b'] * c['w'])
462
+ A = sparse.vstack(mats)
463
+ logger.info(f"Interpolation matrix is {A.shape[0]} x {A.shape[1]}")
464
+
465
+ B = np.hstack(bs)
466
+ return A, B
467
+
468
+ def add_equality_block(self, A, B):
469
+ if len(self.equal_constraints) > 0:
470
+ ATA = A.T.dot(A)
471
+ ATB = A.T.dot(B)
472
+ logger.info(f"Equality block is {self.eq_const_c} x {self.nx}")
473
+ # solving constrained least squares using
474
+ # | ATA CT | |c| = b
475
+ # | C 0 | |y| d
476
+ # where A is the interpoaltion matrix
477
+ # C is the equality constraint matrix
478
+ # b is the interpolation constraints to be honoured
479
+ # in a least squares sense
480
+ # and d are the equality constraints
481
+ # c are the node values and y are the
482
+ # lagrange multipliers#
483
+ a = []
484
+ rows = []
485
+ cols = []
486
+ b = []
487
+ for c in self.equal_constraints.values():
488
+ b.extend((c["B"]).tolist())
489
+ aa = c["A"].flatten()
490
+ mask = aa == 0
491
+ a.extend(aa[~mask].tolist())
492
+ rows.extend(c["row"].flatten()[~mask].tolist())
493
+ cols.extend(c["col"].flatten()[~mask].tolist())
494
+
495
+ C = sparse.coo_matrix(
496
+ (np.array(a), (np.array(rows), cols)),
497
+ shape=(self.eq_const_c, self.nx),
498
+ dtype=float,
499
+ ).tocsr()
500
+
501
+ d = np.array(b)
502
+ ATA = sparse.bmat([[ATA, C.T], [C, None]])
503
+ ATB = np.hstack([ATB, d])
504
+
505
+ return ATA, ATB
506
+
507
+ def build_inequality_matrix(self):
508
+ mats = []
509
+ bounds = []
510
+ for c in self.ineq_constraints.values():
511
+ mats.append(c['matrix'])
512
+ bounds.append(c['bounds'])
513
+ if len(mats) == 0:
514
+ return None, None
515
+ Q = sparse.vstack(mats)
516
+ bounds = np.vstack(bounds)
517
+ return Q, bounds
518
+
519
+ def solve_system(
520
+ self,
521
+ solver: Optional[Union[Callable[[sparse.csr_matrix, np.ndarray], np.ndarray], str]] = None,
522
+ solver_kwargs: dict = {},
523
+ ) -> bool:
524
+ """
525
+ Main entry point to run the solver and update the node value
526
+ attribute for the
527
+ discreteinterpolator class
528
+
529
+ Parameters
530
+ ----------
531
+ solver : string/callable
532
+ solver 'cg' conjugate gradient, 'lsmr' or callable function
533
+ solver_kwargs
534
+ kwargs for solver check scipy documentation for more information
535
+
536
+ Returns
537
+ -------
538
+ bool
539
+ True if the interpolation is run
540
+
541
+ """
542
+ starttime = time()
543
+ self.c = np.zeros(self.support.n_nodes)
544
+ self.c[:] = np.nan
545
+ A, b = self.build_matrix()
546
+ Q, bounds = self.build_inequality_matrix()
547
+ if callable(solver):
548
+ logger.warning('Using custom solver')
549
+ self.c = solver(A.tocsr(), b)
550
+ self.up_to_date = True
551
+
552
+ return True
553
+ ## solve with lsmr
554
+ if isinstance(solver, str):
555
+ if solver not in ['cg', 'lsmr', 'admm']:
556
+ logger.warning(
557
+ f'Unknown solver {solver} using cg. \n Available solvers are cg and lsmr or a custom solver as a callable function'
558
+ )
559
+ solver = 'cg'
560
+ if solver == 'cg':
561
+ 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)
565
+ if res[1] > 0:
566
+ logger.warning(
567
+ f'CG reached iteration limit ({res[1]})and did not converge, check input data. Setting solution to last iteration'
568
+ )
569
+ self.c = res[0]
570
+ self.up_to_date = True
571
+ return True
572
+ elif solver == 'lsmr':
573
+ logger.info("Solving using lsmr")
574
+ res = sparse.linalg.lsmr(A, b, **solver_kwargs)
575
+ if res[1] == 1 or res[1] == 4 or res[1] == 2 or res[1] == 5:
576
+ self.c = res[0]
577
+ elif res[1] == 0:
578
+ logger.warning("Solution to least squares problem is all zeros, check input data")
579
+ elif res[1] == 3 or res[1] == 6:
580
+ logger.warning("COND(A) seems to be greater than CONLIM, check input data")
581
+ # self.c = res[0]
582
+ elif res[1] == 7:
583
+ logger.warning(
584
+ "LSMR reached iteration limit and did not converge, check input data. Setting solution to last iteration"
585
+ )
586
+ self.c = res[0]
587
+ self.up_to_date = True
588
+ logger.info("Interpolation took %f seconds" % (time() - starttime))
589
+ return True
590
+ elif solver == 'admm':
591
+ logger.info("Solving using admm")
592
+
593
+ if 'x0' in solver_kwargs:
594
+ x0 = solver_kwargs['x0'](self.support)
595
+ else:
596
+ x0 = np.zeros(A.shape[1])
597
+ solver_kwargs.pop('x0', None)
598
+ if Q is None:
599
+ logger.warning("No inequality constraints, using lsmr")
600
+ return self.solve_system('lsmr', solver_kwargs)
601
+
602
+ try:
603
+ from loopsolver import admm_solve
604
+ except ImportError:
605
+ logger.warning(
606
+ "Cannot import admm solver. Please install loopsolver or use lsmr or cg"
607
+ )
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
626
+
627
+ def update(self) -> bool:
628
+ """
629
+ Check if the solver is up to date, if not rerun interpolation using
630
+ the previously used solver. If the interpolation has not been run
631
+ before it will
632
+ return False
633
+
634
+ Returns
635
+ -------
636
+ bool
637
+
638
+ """
639
+ if self.solver is None:
640
+ logging.debug("Cannot rerun interpolator")
641
+ return False
642
+ if not self.up_to_date:
643
+ self.setup_interpolator()
644
+ return self.solve_system(self.solver)
645
+
646
+ def evaluate_value(self, locations: np.ndarray) -> np.ndarray:
647
+ """Evaluate the value of the interpolator at location
648
+
649
+ Parameters
650
+ ----------
651
+ evaluation_points : np.ndarray
652
+ location to evaluate the interpolator
653
+
654
+ Returns
655
+ -------
656
+ np.ndarray
657
+ value of the interpolator
658
+ """
659
+ self.update()
660
+ evaluation_points = np.array(locations)
661
+ evaluated = np.zeros(evaluation_points.shape[0])
662
+ mask = np.any(evaluation_points == np.nan, axis=1)
663
+
664
+ if evaluation_points[~mask, :].shape[0] > 0:
665
+ evaluated[~mask] = self.support.evaluate_value(evaluation_points[~mask], self.c)
666
+ return evaluated
667
+
668
+ def evaluate_gradient(self, locations: np.ndarray) -> np.ndarray:
669
+ """
670
+ Evaluate the gradient of the scalar field at the evaluation points
671
+ Parameters
672
+ ----------
673
+ evaluation_points : np.array
674
+ xyz locations to evaluate the gradient
675
+
676
+ Returns
677
+ -------
678
+
679
+ """
680
+ self.update()
681
+ if locations.shape[0] > 0:
682
+ return self.support.evaluate_gradient(locations, self.c)
683
+ return np.zeros((0, 3))
684
+
685
+ def to_dict(self):
686
+ return {
687
+ "type": self.type.name,
688
+ "support": self.support.to_dict(),
689
+ "c": self.c,
690
+ **super().to_dict(),
691
+ # 'region_function':self.region_function,
692
+ }