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.
- LoopStructural/__init__.py +52 -0
- LoopStructural/datasets/__init__.py +23 -0
- LoopStructural/datasets/_base.py +301 -0
- LoopStructural/datasets/_example_models.py +10 -0
- LoopStructural/datasets/data/claudius.csv +21049 -0
- LoopStructural/datasets/data/claudiusbb.txt +2 -0
- LoopStructural/datasets/data/duplex.csv +126 -0
- LoopStructural/datasets/data/duplexbb.txt +2 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.cpg +1 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.prj +1 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
- LoopStructural/datasets/data/geological_map_data/bbox.csv +2 -0
- LoopStructural/datasets/data/geological_map_data/contacts.csv +657 -0
- LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +7 -0
- LoopStructural/datasets/data/geological_map_data/fault_edges.txt +2 -0
- LoopStructural/datasets/data/geological_map_data/fault_locations.csv +79 -0
- LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +19 -0
- LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +13 -0
- LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +207 -0
- LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +13 -0
- LoopStructural/datasets/data/intrusion.csv +1017 -0
- LoopStructural/datasets/data/intrusionbb.txt +2 -0
- LoopStructural/datasets/data/onefoldbb.txt +2 -0
- LoopStructural/datasets/data/onefolddata.csv +2226 -0
- LoopStructural/datasets/data/refolded_bb.txt +2 -0
- LoopStructural/datasets/data/refolded_fold.csv +205 -0
- LoopStructural/datasets/data/tabular_intrusion.csv +23 -0
- LoopStructural/datatypes/__init__.py +4 -0
- LoopStructural/datatypes/_bounding_box.py +422 -0
- LoopStructural/datatypes/_point.py +166 -0
- LoopStructural/datatypes/_structured_grid.py +94 -0
- LoopStructural/datatypes/_surface.py +184 -0
- LoopStructural/export/exporters.py +554 -0
- LoopStructural/export/file_formats.py +15 -0
- LoopStructural/export/geoh5.py +100 -0
- LoopStructural/export/gocad.py +126 -0
- LoopStructural/export/omf_wrapper.py +88 -0
- LoopStructural/interpolators/__init__.py +105 -0
- LoopStructural/interpolators/_api.py +143 -0
- LoopStructural/interpolators/_builders.py +149 -0
- LoopStructural/interpolators/_cython/__init__.py +0 -0
- LoopStructural/interpolators/_discrete_fold_interpolator.py +183 -0
- LoopStructural/interpolators/_discrete_interpolator.py +692 -0
- LoopStructural/interpolators/_finite_difference_interpolator.py +470 -0
- LoopStructural/interpolators/_geological_interpolator.py +380 -0
- LoopStructural/interpolators/_interpolator_factory.py +89 -0
- LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
- LoopStructural/interpolators/_operator.py +38 -0
- LoopStructural/interpolators/_p1interpolator.py +228 -0
- LoopStructural/interpolators/_p2interpolator.py +277 -0
- LoopStructural/interpolators/_surfe_wrapper.py +174 -0
- LoopStructural/interpolators/supports/_2d_base_unstructured.py +340 -0
- LoopStructural/interpolators/supports/_2d_p1_unstructured.py +68 -0
- LoopStructural/interpolators/supports/_2d_p2_unstructured.py +288 -0
- LoopStructural/interpolators/supports/_2d_structured_grid.py +462 -0
- LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
- LoopStructural/interpolators/supports/_3d_base_structured.py +467 -0
- LoopStructural/interpolators/supports/_3d_p2_tetra.py +331 -0
- LoopStructural/interpolators/supports/_3d_structured_grid.py +470 -0
- LoopStructural/interpolators/supports/_3d_structured_tetra.py +746 -0
- LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +637 -0
- LoopStructural/interpolators/supports/__init__.py +55 -0
- LoopStructural/interpolators/supports/_aabb.py +77 -0
- LoopStructural/interpolators/supports/_base_support.py +114 -0
- LoopStructural/interpolators/supports/_face_table.py +70 -0
- LoopStructural/interpolators/supports/_support_factory.py +32 -0
- LoopStructural/modelling/__init__.py +29 -0
- LoopStructural/modelling/core/__init__.py +0 -0
- LoopStructural/modelling/core/geological_model.py +1867 -0
- LoopStructural/modelling/features/__init__.py +32 -0
- LoopStructural/modelling/features/_analytical_feature.py +79 -0
- LoopStructural/modelling/features/_base_geological_feature.py +364 -0
- LoopStructural/modelling/features/_cross_product_geological_feature.py +100 -0
- LoopStructural/modelling/features/_geological_feature.py +288 -0
- LoopStructural/modelling/features/_lambda_geological_feature.py +93 -0
- LoopStructural/modelling/features/_region.py +18 -0
- LoopStructural/modelling/features/_structural_frame.py +186 -0
- LoopStructural/modelling/features/_unconformity_feature.py +83 -0
- LoopStructural/modelling/features/builders/__init__.py +5 -0
- LoopStructural/modelling/features/builders/_base_builder.py +111 -0
- LoopStructural/modelling/features/builders/_fault_builder.py +590 -0
- LoopStructural/modelling/features/builders/_folded_feature_builder.py +129 -0
- LoopStructural/modelling/features/builders/_geological_feature_builder.py +543 -0
- LoopStructural/modelling/features/builders/_structural_frame_builder.py +237 -0
- LoopStructural/modelling/features/fault/__init__.py +3 -0
- LoopStructural/modelling/features/fault/_fault_function.py +444 -0
- LoopStructural/modelling/features/fault/_fault_function_feature.py +82 -0
- LoopStructural/modelling/features/fault/_fault_segment.py +505 -0
- LoopStructural/modelling/features/fold/__init__.py +9 -0
- LoopStructural/modelling/features/fold/_fold.py +167 -0
- LoopStructural/modelling/features/fold/_fold_rotation_angle.py +149 -0
- LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +67 -0
- LoopStructural/modelling/features/fold/_foldframe.py +194 -0
- LoopStructural/modelling/features/fold/_svariogram.py +188 -0
- LoopStructural/modelling/input/__init__.py +2 -0
- LoopStructural/modelling/input/fault_network.py +80 -0
- LoopStructural/modelling/input/map2loop_processor.py +165 -0
- LoopStructural/modelling/input/process_data.py +650 -0
- LoopStructural/modelling/input/project_file.py +84 -0
- LoopStructural/modelling/intrusions/__init__.py +25 -0
- LoopStructural/modelling/intrusions/geom_conceptual_models.py +142 -0
- LoopStructural/modelling/intrusions/geometric_scaling_functions.py +123 -0
- LoopStructural/modelling/intrusions/intrusion_builder.py +672 -0
- LoopStructural/modelling/intrusions/intrusion_feature.py +410 -0
- LoopStructural/modelling/intrusions/intrusion_frame_builder.py +971 -0
- LoopStructural/modelling/intrusions/intrusion_support_functions.py +460 -0
- LoopStructural/utils/__init__.py +38 -0
- LoopStructural/utils/_surface.py +143 -0
- LoopStructural/utils/_transformation.py +76 -0
- LoopStructural/utils/config.py +18 -0
- LoopStructural/utils/dtm_creator.py +17 -0
- LoopStructural/utils/exceptions.py +31 -0
- LoopStructural/utils/helper.py +292 -0
- LoopStructural/utils/json_encoder.py +18 -0
- LoopStructural/utils/linalg.py +8 -0
- LoopStructural/utils/logging.py +79 -0
- LoopStructural/utils/maths.py +245 -0
- LoopStructural/utils/regions.py +103 -0
- LoopStructural/utils/typing.py +7 -0
- LoopStructural/utils/utils.py +68 -0
- LoopStructural/version.py +1 -0
- LoopStructural/visualisation/__init__.py +11 -0
- LoopStructural-1.6.1.dist-info/LICENSE +21 -0
- LoopStructural-1.6.1.dist-info/METADATA +81 -0
- LoopStructural-1.6.1.dist-info/RECORD +129 -0
- LoopStructural-1.6.1.dist-info/WHEEL +5 -0
- 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
|
+
}
|