resqpy 4.14.2__py3-none-any.whl → 5.1.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.
- resqpy/__init__.py +1 -1
- resqpy/fault/_gcs_functions.py +10 -10
- resqpy/fault/_grid_connection_set.py +277 -113
- resqpy/grid/__init__.py +2 -3
- resqpy/grid/_defined_geometry.py +3 -3
- resqpy/grid/_extract_functions.py +8 -2
- resqpy/grid/_grid.py +95 -12
- resqpy/grid/_grid_types.py +22 -7
- resqpy/grid/_points_functions.py +1 -1
- resqpy/grid/_regular_grid.py +6 -2
- resqpy/grid_surface/__init__.py +17 -38
- resqpy/grid_surface/_blocked_well_populate.py +5 -5
- resqpy/grid_surface/_find_faces.py +1413 -253
- resqpy/lines/_polyline.py +24 -33
- resqpy/model/_catalogue.py +9 -0
- resqpy/model/_forestry.py +18 -14
- resqpy/model/_hdf5.py +11 -3
- resqpy/model/_model.py +85 -10
- resqpy/model/_xml.py +38 -13
- resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
- resqpy/olio/read_nexus_fault.py +8 -2
- resqpy/olio/relperm.py +1 -1
- resqpy/olio/transmission.py +8 -8
- resqpy/olio/triangulation.py +36 -30
- resqpy/olio/vector_utilities.py +340 -6
- resqpy/olio/volume.py +0 -20
- resqpy/olio/wellspec_keywords.py +19 -13
- resqpy/olio/write_hdf5.py +1 -1
- resqpy/olio/xml_et.py +12 -0
- resqpy/property/__init__.py +6 -4
- resqpy/property/_collection_add_part.py +4 -3
- resqpy/property/_collection_create_xml.py +4 -2
- resqpy/property/_collection_get_attributes.py +4 -0
- resqpy/property/attribute_property_set.py +311 -0
- resqpy/property/grid_property_collection.py +11 -11
- resqpy/property/property_collection.py +79 -31
- resqpy/property/property_common.py +3 -8
- resqpy/rq_import/_add_surfaces.py +34 -14
- resqpy/rq_import/_grid_from_cp.py +2 -2
- resqpy/rq_import/_import_nexus.py +75 -48
- resqpy/rq_import/_import_vdb_all_grids.py +64 -52
- resqpy/rq_import/_import_vdb_ensemble.py +12 -13
- resqpy/surface/_mesh.py +4 -0
- resqpy/surface/_surface.py +593 -118
- resqpy/surface/_tri_mesh.py +13 -10
- resqpy/surface/_tri_mesh_stencil.py +4 -4
- resqpy/surface/_triangulated_patch.py +71 -51
- resqpy/time_series/_any_time_series.py +7 -4
- resqpy/time_series/_geologic_time_series.py +1 -1
- resqpy/unstructured/_hexa_grid.py +6 -2
- resqpy/unstructured/_prism_grid.py +13 -5
- resqpy/unstructured/_pyramid_grid.py +6 -2
- resqpy/unstructured/_tetra_grid.py +6 -2
- resqpy/unstructured/_unstructured_grid.py +6 -2
- resqpy/well/_blocked_well.py +1986 -1946
- resqpy/well/_deviation_survey.py +3 -3
- resqpy/well/_md_datum.py +11 -21
- resqpy/well/_trajectory.py +10 -5
- resqpy/well/_wellbore_frame.py +10 -2
- resqpy/well/blocked_well_frame.py +3 -3
- resqpy/well/well_object_funcs.py +7 -9
- resqpy/well/well_utils.py +33 -0
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/METADATA +8 -9
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/RECORD +66 -66
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/WHEEL +1 -1
- resqpy/grid/_moved_functions.py +0 -15
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/LICENSE +0 -0
resqpy/surface/_surface.py
CHANGED
@@ -8,6 +8,7 @@ import logging
|
|
8
8
|
log = logging.getLogger(__name__)
|
9
9
|
|
10
10
|
import numpy as np
|
11
|
+
import math as maths
|
11
12
|
|
12
13
|
import resqpy.crs as rqc
|
13
14
|
import resqpy.lines as rql
|
@@ -47,7 +48,7 @@ class Surface(rqsb.BaseSurface):
|
|
47
48
|
originator = None,
|
48
49
|
extra_metadata = {}):
|
49
50
|
"""Create an empty Surface object (RESQML TriangulatedSetRepresentation).
|
50
|
-
|
51
|
+
|
51
52
|
Optionally populates from xml, point set or mesh.
|
52
53
|
|
53
54
|
arguments:
|
@@ -122,6 +123,76 @@ class Surface(rqsb.BaseSurface):
|
|
122
123
|
self.set_from_mesh_file(mesh_file, mesh_format, quad_triangles = quad_triangles)
|
123
124
|
elif tsurf_file is not None:
|
124
125
|
self.set_from_tsurf_file(tsurf_file)
|
126
|
+
self._load_normal_vector_from_extra_metadata()
|
127
|
+
|
128
|
+
@classmethod
|
129
|
+
def from_list_of_patches(cls, model, patch_list, title, crs_uuid = None, extra_metadata = None):
|
130
|
+
"""Create a Surface from a prepared list of TriangulatedPatch objects.
|
131
|
+
|
132
|
+
arguments:
|
133
|
+
- model (Model): the model to which the surface will be associated
|
134
|
+
- patch_list (list of TriangulatedPatch): the list of patches to be combined to form the surface
|
135
|
+
- title (str): the citation title for the new surface
|
136
|
+
- crs_uuid (uuid, optional): the uuid of a crs in model which the points are deemed to be in
|
137
|
+
- extra_metadata (dict of (str: str), optional): extra metadata to add to the new surface
|
138
|
+
|
139
|
+
returns:
|
140
|
+
- new Surface comprised of a patch for each entry in the patch list
|
141
|
+
|
142
|
+
notes:
|
143
|
+
- the triangulated patch objects are used directly in the surface
|
144
|
+
- the patches should not have had their hdf5 data written yet
|
145
|
+
- the patch index values will be set, with any previous values ignored
|
146
|
+
- the patches will be hijacked to the target model if their model is different
|
147
|
+
- each patch will have its points converted in situ into the surface crs
|
148
|
+
- if the crs_uuid argument is None, the crs_uuid is taken from the first patch
|
149
|
+
"""
|
150
|
+
assert len(patch_list) > 0, 'attempting to create Surface from empty patch list'
|
151
|
+
if crs_uuid is None:
|
152
|
+
crs_uuid = patch_list[0].crs_uuid
|
153
|
+
if model.uuid(uuid = crs_uuid) is None:
|
154
|
+
model.copy_uuid_from_other_model(patch_list[0].model, crs_uuid)
|
155
|
+
surf = cls(model, title = title, crs_uuid = crs_uuid, extra_metadata = extra_metadata)
|
156
|
+
surf.patch_list = patch_list
|
157
|
+
surf.crs_uuid = crs_uuid
|
158
|
+
crs = rqc.Crs(model, uuid = crs_uuid)
|
159
|
+
for i, patch in enumerate(surf.patch_list):
|
160
|
+
assert patch.points is not None, f'points missing in patch {i} when making surface {title}'
|
161
|
+
patch.index = i
|
162
|
+
patch._set_t_type()
|
163
|
+
if not bu.matching_uuids(patch.crs_uuid, crs_uuid):
|
164
|
+
p_crs = rqc.Crs(patch.model, uuid = patch.crs_uuid)
|
165
|
+
p_crs.convert_array_to(crs, patch.points)
|
166
|
+
patch.model = model
|
167
|
+
return surf
|
168
|
+
|
169
|
+
@classmethod
|
170
|
+
def from_list_of_patches_of_triangles_and_points(cls, model, t_p_list, title, crs_uuid, extra_metadata = None):
|
171
|
+
"""Create a Surface from a prepared list of pairs of (triangles, points).
|
172
|
+
|
173
|
+
arguments:
|
174
|
+
- model (Model): the model to which the surface will be associated
|
175
|
+
- t_p_list (list of (numpy int array, numpy float array)): the list of patches of triangles and points;
|
176
|
+
the int arrays have shape (N, 3) being the triangle vertex indices of points; the float array has
|
177
|
+
shape (M, 3) being the xyx values for the points, in the crs identified by crs_uuid
|
178
|
+
- title (str): the citation title for the new surface
|
179
|
+
- crs_uuid (uuid): the uuid of a crs in model which the points are deemed to be in
|
180
|
+
- extra_metadata (dict of (str: str), optional): extra metadata to add to the new surface
|
181
|
+
|
182
|
+
returns:
|
183
|
+
- new Surface comprised of a patch for each entry in the list of pairs of triangles and points data
|
184
|
+
|
185
|
+
note:
|
186
|
+
- each entry in the t_p_list will have its own patch in the resulting surface, indexed in order of list
|
187
|
+
"""
|
188
|
+
assert t_p_list, f'no triangles and points pairs in list when generating surface: {title}'
|
189
|
+
assert crs_uuid is not None
|
190
|
+
patch_list = []
|
191
|
+
for i, (t, p) in enumerate(t_p_list):
|
192
|
+
patch = rqs.TriangulatedPatch(model, patch_index = i, crs_uuid = crs_uuid)
|
193
|
+
patch.set_from_triangles_and_points(t, p)
|
194
|
+
patch_list.append(patch)
|
195
|
+
return cls.from_list_of_patches(model, patch_list, title, crs_uuid = crs_uuid, extra_metadata = extra_metadata)
|
125
196
|
|
126
197
|
@classmethod
|
127
198
|
def from_tri_mesh(cls, tri_mesh, exclude_nans = False):
|
@@ -272,69 +343,169 @@ class Surface(rqsb.BaseSurface):
|
|
272
343
|
|
273
344
|
self.model = parent_model
|
274
345
|
|
275
|
-
def
|
276
|
-
"""Returns
|
346
|
+
def number_of_patches(self):
|
347
|
+
"""Returns the number of patches present in the surface."""
|
348
|
+
|
349
|
+
self.extract_patches(self.root)
|
350
|
+
return len(self.patch_list)
|
351
|
+
|
352
|
+
def triangles_and_points(self, patch = None, copy = False):
|
353
|
+
"""Returns arrays representing one patch or a combination of all the patches in the surface.
|
354
|
+
|
355
|
+
arguments:
|
356
|
+
patch (int, optional): patch index; if None, combined arrays for all patches are returned
|
357
|
+
copy (bool, default False): if True, a copy of the arrays is returned; if False, the cached
|
358
|
+
arrays are returned
|
277
359
|
|
278
360
|
returns:
|
279
361
|
tuple (triangles, points):
|
280
362
|
triangles (int array of shape[:, 3]): integer indices into points array,
|
281
|
-
|
363
|
+
being the nodes of the corners of the triangles;
|
282
364
|
points (float array of shape[:, 3]): flat array of xyz points, indexed by triangles
|
283
365
|
|
284
366
|
:meta common:
|
285
367
|
"""
|
286
368
|
|
287
|
-
if self.triangles is not None:
|
288
|
-
return (self.triangles, self.points)
|
289
369
|
self.extract_patches(self.root)
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
370
|
+
if patch is None:
|
371
|
+
if self.triangles is None or self.points is None:
|
372
|
+
if self.triangles is None:
|
373
|
+
points_offset = 0
|
374
|
+
for triangulated_patch in self.patch_list:
|
375
|
+
(t, p) = triangulated_patch.triangles_and_points()
|
376
|
+
if points_offset == 0:
|
377
|
+
self.triangles = t
|
378
|
+
self.points = p
|
379
|
+
else:
|
380
|
+
self.triangles = np.concatenate((self.triangles, t.copy() + points_offset))
|
381
|
+
self.points = np.concatenate((self.points, p))
|
382
|
+
points_offset += p.shape[0]
|
383
|
+
if copy:
|
384
|
+
return (self.triangles.copy(), self.points.copy())
|
296
385
|
else:
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
return
|
386
|
+
return (self.triangles, self.points)
|
387
|
+
assert 0 <= patch < len(self.patch_list), \
|
388
|
+
ValueError(f'patch index {patch} out of range for surface with {len(self.patch_list)} patches')
|
389
|
+
return self.patch_list[patch].triangles_and_points(copy = copy)
|
390
|
+
|
391
|
+
def patch_index_for_triangle_index(self, triangle_index):
|
392
|
+
"""Returns the patch index for a triangle index (as applicable to triangles_and_points() triangles)."""
|
393
|
+
if triangle_index is None or triangle_index < 0:
|
394
|
+
return None
|
395
|
+
self.extract_patches(self.root)
|
396
|
+
if not self.patch_list:
|
397
|
+
return None
|
398
|
+
for i, patch in enumerate(self.patch_list):
|
399
|
+
triangle_index -= patch.triangle_count
|
400
|
+
if triangle_index < 0:
|
401
|
+
return i
|
402
|
+
return None
|
403
|
+
|
404
|
+
def patch_indices_for_triangle_indices(self, triangle_indices, lazy = True):
|
405
|
+
"""Returns array of patch indices for array of triangle indices (as applicable to triangles_and_points() triangles)."""
|
406
|
+
self.extract_patches(self.root)
|
407
|
+
if not self.patch_list:
|
408
|
+
return np.full(triangle_indices.shape, -1, dtype = np.int8)
|
409
|
+
patch_count = len(self.patch_list)
|
410
|
+
dtype = (np.int8 if patch_count < 127 else np.int32)
|
411
|
+
if lazy and patch_count == 1:
|
412
|
+
return np.zeros(triangle_indices.shape, dtype = np.int8)
|
413
|
+
patch_limits = np.zeros(patch_count, dtype = np.int32)
|
414
|
+
t_count = 0
|
415
|
+
for p_i in range(patch_count):
|
416
|
+
t_count += self.patch_list[p_i].triangle_count
|
417
|
+
patch_limits[p_i] = t_count
|
418
|
+
patches = np.empty(triangle_indices.shape, dtype = dtype)
|
419
|
+
patches[:] = np.digitize(triangle_indices, patch_limits, right = False)
|
420
|
+
if not lazy:
|
421
|
+
patches[np.logical_or(triangle_indices < 0, patches == patch_count)] = -1
|
422
|
+
return patches
|
423
|
+
|
424
|
+
def decache_triangles_and_points(self):
|
425
|
+
"""Removes the cached composite triangles and points arrays."""
|
426
|
+
self.points = None
|
427
|
+
self.triangles = None
|
428
|
+
|
429
|
+
def triangle_count(self, patch = None):
|
430
|
+
"""Return the numner of triangles in this surface, or in one patch.
|
301
431
|
|
302
|
-
|
303
|
-
|
432
|
+
arguments:
|
433
|
+
patch (int, optional): patch index; if None, a combined triangle count for all patches is returned
|
434
|
+
|
435
|
+
returns:
|
436
|
+
int being the number of trianges in the patch (if specified) or in all the patches
|
437
|
+
"""
|
304
438
|
|
305
439
|
self.extract_patches(self.root)
|
306
|
-
if
|
307
|
-
|
308
|
-
|
440
|
+
if patch is None:
|
441
|
+
if not self.patch_list:
|
442
|
+
return 0
|
443
|
+
return np.sum([tp.triangle_count for tp in self.patch_list])
|
444
|
+
assert 0 <= patch < len(self.patch_list), \
|
445
|
+
ValueError(f'patch index {patch} out of range for surface with {len(self.patch_list)} patches in triangle_count')
|
446
|
+
return self.patch_list[patch].triangle_count
|
447
|
+
|
448
|
+
def node_count(self, patch = None):
|
449
|
+
"""Return the number of nodes (points) used in this surface, or in one patch.
|
450
|
+
|
451
|
+
arguments:
|
452
|
+
patch (int, optional): patch index; if None, a combined node count for all patches is returned
|
309
453
|
|
310
|
-
|
311
|
-
|
454
|
+
returns:
|
455
|
+
int being the number of nodes in the patch (if specified) or in all the patches
|
456
|
+
|
457
|
+
note:
|
458
|
+
a multi patch surface might have more than one node colocated; this method will treat such coincident nodes
|
459
|
+
as separate nodes
|
460
|
+
"""
|
312
461
|
|
313
462
|
self.extract_patches(self.root)
|
314
|
-
if
|
315
|
-
|
316
|
-
|
463
|
+
if patch is None:
|
464
|
+
if not self.patch_list:
|
465
|
+
return 0
|
466
|
+
return np.sum([tp.node_count for tp in self.patch_list])
|
467
|
+
assert 0 <= patch < len(self.patch_list), \
|
468
|
+
ValueError(f'patch index {patch} out of range for surface with {len(self.patch_list)} patches in node_count')
|
469
|
+
return self.patch_list[patch].node_count
|
317
470
|
|
318
471
|
def change_crs(self, required_crs):
|
319
472
|
"""Changes the crs of the surface, also sets a new uuid if crs changed.
|
320
473
|
|
321
|
-
|
474
|
+
notes:
|
322
475
|
this method is usually used to change the coordinate system for a temporary resqpy object;
|
323
|
-
to add as a new part, call write_hdf5() and create_xml() methods
|
476
|
+
to add as a new part, call write_hdf5() and create_xml() methods;
|
477
|
+
patches are maintained by this method;
|
478
|
+
normal vector extra metadata item is updated if present; rotation matrix is removed
|
324
479
|
"""
|
325
480
|
|
326
481
|
old_crs = rqc.Crs(self.model, uuid = self.crs_uuid)
|
327
482
|
self.crs_uuid = required_crs.uuid
|
328
|
-
if required_crs
|
483
|
+
if bu.matching_uuids(required_crs.uuid, old_crs.uuid) or not self.patch_list:
|
329
484
|
log.debug(f'no crs change needed for {self.title}')
|
330
485
|
return
|
486
|
+
equivalent_crs = (required_crs == old_crs)
|
331
487
|
log.debug(f'crs change needed for {self.title} from {old_crs.title} to {required_crs.title}')
|
332
488
|
for patch in self.patch_list:
|
333
|
-
patch.
|
334
|
-
|
489
|
+
assert bu.matching_uuids(patch.crs_uuid, old_crs.uuid)
|
490
|
+
if not equivalent_crs:
|
491
|
+
patch.triangles_and_points()
|
492
|
+
required_crs.convert_array_from(old_crs, patch.points)
|
335
493
|
patch.crs_uuid = self.crs_uuid
|
336
494
|
self.triangles = None # clear cached arrays for surface
|
337
495
|
self.points = None
|
496
|
+
if not equivalent_crs:
|
497
|
+
if self.extra_metadata.pop('rotation matrix', None) is not None:
|
498
|
+
log.debug(f'discarding rotation matrix extra metadata during crs change of: {self.title}')
|
499
|
+
self._load_normal_vector_from_extra_metadata()
|
500
|
+
if self.normal_vector is not None:
|
501
|
+
if required_crs.z_inc_down != old_crs.z_inc_down:
|
502
|
+
self.normal_vector[2] = -self.normal_vector[2]
|
503
|
+
theta = (wam.convert(required_crs.rotation, required_crs.rotation_units, 'dega') -
|
504
|
+
wam.convert(old_crs.rotation, old_crs.rotation_units, 'dega'))
|
505
|
+
if not maths.isclose(theta, 0.0):
|
506
|
+
self.normal_vector = vec.rotate_vector(vec.rotation_matrix_3d_axial(2, theta), self.normal_vector)
|
507
|
+
self.extra_metadata['normal vector'] = str(
|
508
|
+
f'{self.normal_vector[0]},{self.normal_vector[1]},{self.normal_vector[2]}')
|
338
509
|
self.uuid = bu.new_uuid() # hope this doesn't cause problems
|
339
510
|
assert self.root is None
|
340
511
|
|
@@ -381,7 +552,7 @@ class Surface(rqsb.BaseSurface):
|
|
381
552
|
self.uuid = bu.new_uuid()
|
382
553
|
|
383
554
|
def set_to_split_surface(self, large_surface, line, delta_xyz):
|
384
|
-
"""Populate this (empty) surface with a version of a larger surface split by
|
555
|
+
"""Populate this (empty) surface with a version of a larger surface split by a straight xy line.
|
385
556
|
|
386
557
|
arguments:
|
387
558
|
large_surface (Surface): the larger surface, a copy of which is to be split
|
@@ -399,13 +570,14 @@ class Surface(rqsb.BaseSurface):
|
|
399
570
|
t, p = large_surface.triangles_and_points()
|
400
571
|
assert p.ndim == 2 and p.shape[1] == 3
|
401
572
|
pp = np.concatenate((p, line), axis = 0)
|
402
|
-
|
573
|
+
t_type = np.int32 if len(pp) <= 2_147_483_647 else np.int64
|
574
|
+
tp = np.empty(p.shape, dtype = t_type)
|
403
575
|
tp[:, 0] = len(p)
|
404
576
|
tp[:, 1] = len(p) + 1
|
405
|
-
tp[:, 2] = np.arange(len(p), dtype =
|
577
|
+
tp[:, 2] = np.arange(len(p), dtype = t_type)
|
406
578
|
cw = vec.clockwise_triangles(pp, tp)
|
407
|
-
pai =
|
408
|
-
pbi =
|
579
|
+
pai = (cw >= 0.0) # bool mask over p
|
580
|
+
pbi = (cw <= 0.0) # bool mask over p
|
409
581
|
tap = pai[t]
|
410
582
|
tbp = pbi[t]
|
411
583
|
ta = np.any(tap, axis = 1) # bool array over t
|
@@ -414,11 +586,11 @@ class Surface(rqsb.BaseSurface):
|
|
414
586
|
# here we stick the two halves together into a single patch
|
415
587
|
# todo: keep as two patches as required by RESQML business rules
|
416
588
|
p_combo = np.empty((0, 3))
|
417
|
-
t_combo = np.empty((0, 3), dtype =
|
589
|
+
t_combo = np.empty((0, 3), dtype = t_type)
|
418
590
|
for i, tab in enumerate((ta, tb)):
|
419
591
|
p_keep = np.unique(t[tab])
|
420
592
|
# note new point index for each old point that is being kept
|
421
|
-
p_map = np.full(len(p), -1, dtype =
|
593
|
+
p_map = np.full(len(p), -1, dtype = t_type)
|
422
594
|
p_map[p_keep] = np.arange(len(p_keep))
|
423
595
|
# copy those unique points into a trimmed points array
|
424
596
|
points_trimmed = p[p_keep].copy()
|
@@ -435,17 +607,24 @@ class Surface(rqsb.BaseSurface):
|
|
435
607
|
|
436
608
|
self.set_from_triangles_and_points(t_combo, p_combo)
|
437
609
|
|
438
|
-
def distinct_edges(self):
|
439
|
-
"""Returns a numpy int array of shape (N, 2) being the ordered node pairs of distinct edges of triangles.
|
610
|
+
def distinct_edges(self, patch = None):
|
611
|
+
"""Returns a numpy int array of shape (N, 2) being the ordered node pairs of distinct edges of triangles.
|
612
|
+
|
613
|
+
arguments:
|
614
|
+
patch (int, optional): patch index; if None, a combination of edges for all patches is returned
|
615
|
+
"""
|
440
616
|
|
441
|
-
triangles, _ = self.triangles_and_points()
|
617
|
+
triangles, _ = self.triangles_and_points(patch = patch)
|
442
618
|
assert triangles is not None
|
443
619
|
unique_edges, _ = triangulate.edges(triangles)
|
444
620
|
return unique_edges
|
445
621
|
|
446
|
-
def distinct_edges_and_counts(self):
|
622
|
+
def distinct_edges_and_counts(self, patch = None):
|
447
623
|
"""Returns unique edges as pairs of point indices, and a count of uses of each edge.
|
448
624
|
|
625
|
+
arguments:
|
626
|
+
patch (int, optional): patch index; if None, combined results for all patches are returned
|
627
|
+
|
449
628
|
returns:
|
450
629
|
numpy int array of shape (N, 2), numpy int array of shape (N,)
|
451
630
|
where 2D array is list-like sorted points index pairs for unique edges
|
@@ -453,14 +632,39 @@ class Surface(rqsb.BaseSurface):
|
|
453
632
|
|
454
633
|
notes:
|
455
634
|
first entry in each pair is always the lower of the two point indices;
|
456
|
-
for well formed surfaces, the count should everywhere be
|
635
|
+
for well formed surfaces, the count should everywhere be one or two;
|
457
636
|
the function does not attempt to detect coincident points
|
458
637
|
"""
|
459
638
|
|
460
|
-
triangles, _ = self.triangles_and_points()
|
639
|
+
triangles, _ = self.triangles_and_points(patch = patch)
|
461
640
|
assert triangles is not None
|
462
641
|
return triangulate.edges(triangles)
|
463
642
|
|
643
|
+
def edge_lengths(self, required_uom = None, patch = None):
|
644
|
+
"""Returns float array of shape (N, 3) being triangle edge lengths.
|
645
|
+
|
646
|
+
arguments:
|
647
|
+
required_uom (str, optional): the required length uom for the resulting edge lengths; default is crs xy units
|
648
|
+
patch (int, optional): patch index; if None, edge lengths for all patches are returned
|
649
|
+
|
650
|
+
returns:
|
651
|
+
numpy float array of shape (N, 3) where N is the number of triangles
|
652
|
+
"""
|
653
|
+
|
654
|
+
t, p = self.triangles_and_points(patch = patch)
|
655
|
+
crs = rqc.Crs(self.model, uuid = self.crs_uuid)
|
656
|
+
if required_uom is None:
|
657
|
+
required_uom = crs.xy_units
|
658
|
+
if crs.xy_units != required_uom or crs.z_units != required_uom:
|
659
|
+
p = p.copy()
|
660
|
+
wam.convert_lengths(p[:, :2], crs.xy_units, required_uom)
|
661
|
+
wam.convert_lengths(p[:, 2], crs.z_units, required_uom)
|
662
|
+
t_end = np.empty_like(t)
|
663
|
+
t_end[:, :2] = t[:, 1:]
|
664
|
+
t_end[:, 2] = t[:, 0]
|
665
|
+
edge_v = p[t_end, :] - p[t, :]
|
666
|
+
return vec.naive_lengths(edge_v)
|
667
|
+
|
464
668
|
def set_from_triangles_and_points(self, triangles, points):
|
465
669
|
"""Populate this (empty) Surface object from an array of triangle corner indices and an array of points."""
|
466
670
|
|
@@ -468,6 +672,22 @@ class Surface(rqsb.BaseSurface):
|
|
468
672
|
tri_patch.set_from_triangles_and_points(triangles, points)
|
469
673
|
self.patch_list = [tri_patch]
|
470
674
|
self.uuid = bu.new_uuid()
|
675
|
+
self.triangles = triangles.copy()
|
676
|
+
self.points = points.copy()
|
677
|
+
|
678
|
+
def set_multi_patch_from_triangles_and_points(self, triangles_and_points_list):
|
679
|
+
"""Populate this (empty) Surface object from a list of paits: array of triangle corner indices, array of points."""
|
680
|
+
|
681
|
+
self.patch_list = []
|
682
|
+
self.trianges = None
|
683
|
+
self.points = None
|
684
|
+
for patch, entry in enumerate(triangles_and_points_list):
|
685
|
+
assert len(entry) == 2, 'expecting pair of arrays (triangles, points) for each patch'
|
686
|
+
triangles, points = entry
|
687
|
+
tri_patch = rqstp.TriangulatedPatch(self.model, patch_index = patch, crs_uuid = self.crs_uuid)
|
688
|
+
tri_patch.set_from_triangles_and_points(triangles, points)
|
689
|
+
self.patch_list.append(tri_patch)
|
690
|
+
self.uuid = bu.new_uuid()
|
471
691
|
|
472
692
|
def set_from_point_set(self,
|
473
693
|
point_set,
|
@@ -480,7 +700,8 @@ class Surface(rqsb.BaseSurface):
|
|
480
700
|
flange_radial_distance = None,
|
481
701
|
flange_inner_ring = False,
|
482
702
|
saucer_parameter = None,
|
483
|
-
make_clockwise = False
|
703
|
+
make_clockwise = False,
|
704
|
+
normal_vector = None):
|
484
705
|
"""Populate this (empty) Surface object with a Delaunay triangulation of points in a PointSet object.
|
485
706
|
|
486
707
|
arguments:
|
@@ -489,9 +710,10 @@ class Surface(rqsb.BaseSurface):
|
|
489
710
|
convex; reduce to 1.0 to allow slightly more concavities; increase to 100.0 or more for very little
|
490
711
|
chance of even a slight concavity
|
491
712
|
reorient (bool, default False): if True, a copy of the points is made and reoriented to minimise the
|
492
|
-
z range (ie. z axis is approximate normal to plane of points), to enhace the triangulation
|
713
|
+
z range (ie. z axis is approximate normal to plane of points), to enhace the triangulation; if a
|
714
|
+
normal_vector is supplied, the reorientation is based on that instead of minimising z
|
493
715
|
reorient_max_dip (float, optional): if present, the reorientation of perspective off vertical is
|
494
|
-
limited to this angle in degrees
|
716
|
+
limited to this angle in degrees; ignored if normal_vector is specified
|
495
717
|
extend_with_flange (bool, default False): if True, a ring of points is added around the outside of the
|
496
718
|
points before the triangulation, effectively extending the surface with a flange
|
497
719
|
flange_point_count (int, default 11): the number of points to generate in the flange ring; ignored if
|
@@ -509,10 +731,12 @@ class Surface(rqsb.BaseSurface):
|
|
509
731
|
make_clockwise (bool, default False): if True, the returned triangles will all be clockwise when
|
510
732
|
viewed in the direction -ve to +ve z axis; if reorient is also True, the clockwise aspect is
|
511
733
|
enforced in the reoriented space
|
734
|
+
normal_vector (triple float, optional): if present and reorienting, the normal vector to use for reorientation;
|
735
|
+
if None, the reorientation is made so as to minimise the z range
|
512
736
|
|
513
737
|
returns:
|
514
738
|
if extend_with_flange is True, numpy bool array with a value per triangle indicating flange triangles;
|
515
|
-
if
|
739
|
+
if extend_with_flange is False, None
|
516
740
|
|
517
741
|
notes:
|
518
742
|
if extend_with_flange is True, then a boolean array is created for the surface, with a value per triangle,
|
@@ -520,23 +744,18 @@ class Surface(rqsb.BaseSurface):
|
|
520
744
|
suitable for adding as a property for the surface, with indexable element 'faces';
|
521
745
|
when flange extension occurs, the radius is the greater of the values determined from the radial factor
|
522
746
|
and radial distance arguments;
|
523
|
-
the saucer_parameter
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
the plane of the original points, to give a simple (and less computationally demanding) saucer shape;
|
529
|
-
+ve angles result in the shift being in the direction of the -ve z hemisphere; -ve angles result in
|
530
|
-
the shift being in the +ve z hemisphere; in either case the direction of the shift is perpendicular
|
531
|
-
to the average plane of the original points
|
747
|
+
the saucer_parameter must be between -90.0 and 90.0, and is interpreted as an angle to apply out of
|
748
|
+
the plane of the original points, to give a simple saucer shape; +ve angles result in the shift being in
|
749
|
+
the direction of the -ve z hemisphere; -ve angles result in the shift being in the +ve z hemisphere; in
|
750
|
+
either case the direction of the shift is perpendicular to the average plane of the original points;
|
751
|
+
normal_vector, if supplied, should be in the crs of the point set
|
532
752
|
"""
|
533
753
|
|
534
754
|
simple_saucer_angle = None
|
535
|
-
if saucer_parameter is not None
|
755
|
+
if saucer_parameter is not None:
|
536
756
|
assert -90.0 < saucer_parameter < 90.0, f'simple saucer angle parameter must be less than 90 degrees; too big: {saucer_parameter}'
|
537
757
|
simple_saucer_angle = saucer_parameter
|
538
758
|
saucer_parameter = None
|
539
|
-
assert saucer_parameter is None or 0.0 <= saucer_parameter < 1.0
|
540
759
|
crs = rqc.Crs(self.model, uuid = point_set.crs_uuid)
|
541
760
|
p = point_set.full_array_ref()
|
542
761
|
assert p.ndim >= 2
|
@@ -549,37 +768,44 @@ class Surface(rqsb.BaseSurface):
|
|
549
768
|
f'removing {len(p) - np.count_nonzero(row_mask)} NaN points from point set {point_set.title} prior to surface triangulation'
|
550
769
|
)
|
551
770
|
p = p[row_mask, :]
|
552
|
-
if crs.xy_units == crs.z_units
|
771
|
+
if crs.xy_units == crs.z_units:
|
553
772
|
unit_adjusted_p = p
|
554
773
|
else:
|
555
774
|
unit_adjusted_p = p.copy()
|
556
775
|
wam.convert_lengths(unit_adjusted_p[:, 2], crs.z_units, crs.xy_units)
|
557
|
-
|
776
|
+
# note: normal vector should already be for a crs with common xy & z units
|
777
|
+
# reorient the points to the fault normal vector
|
778
|
+
if normal_vector is None:
|
558
779
|
p_xy, self.normal_vector, reorient_matrix = triangulate.reorient(unit_adjusted_p,
|
559
780
|
max_dip = reorient_max_dip)
|
560
781
|
else:
|
561
|
-
|
782
|
+
assert len(normal_vector) == 3
|
783
|
+
self.normal_vector = np.array(normal_vector, dtype = np.float64)
|
784
|
+
if self.normal_vector[2] < 0.0:
|
785
|
+
self.normal_vector = -self.normal_vector
|
786
|
+
incl = vec.inclination(normal_vector)
|
787
|
+
if maths.isclose(incl, 0.0):
|
788
|
+
reorient_matrix = vec.no_rotation_matrix()
|
789
|
+
p_xy = unit_adjusted_p
|
790
|
+
else:
|
791
|
+
azi = vec.azimuth(normal_vector)
|
792
|
+
reorient_matrix = vec.tilt_3d_matrix(azi, incl)
|
793
|
+
p_xy = vec.rotate_array(reorient_matrix, unit_adjusted_p)
|
562
794
|
if extend_with_flange:
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
radial_distance = flange_radial_distance,
|
571
|
-
inner_ring = flange_inner_ring,
|
572
|
-
saucer_angle = simple_saucer_angle)
|
573
|
-
p_xy_e = np.concatenate((p_xy, flange_points), axis = 0)
|
795
|
+
flange_points, radius = triangulate.surrounding_xy_ring(p_xy,
|
796
|
+
count = flange_point_count,
|
797
|
+
radial_factor = flange_radial_factor,
|
798
|
+
radial_distance = flange_radial_distance,
|
799
|
+
inner_ring = flange_inner_ring,
|
800
|
+
saucer_angle = 0.0)
|
801
|
+
flange_points_reverse_oriented = vec.rotate_array(reorient_matrix.T, flange_points)
|
574
802
|
if reorient:
|
575
|
-
|
576
|
-
flange_points_reverse_oriented = vec.rotate_array(reorient_matrix.T, flange_points)
|
577
|
-
p_e = np.concatenate((unit_adjusted_p, flange_points_reverse_oriented), axis = 0)
|
803
|
+
p_xy_e = np.concatenate((p_xy, flange_points), axis = 0)
|
578
804
|
else:
|
579
|
-
|
805
|
+
p_xy_e = np.concatenate((unit_adjusted_p, flange_points_reverse_oriented), axis = 0)
|
806
|
+
|
580
807
|
else:
|
581
|
-
p_xy_e =
|
582
|
-
p_e = unit_adjusted_p
|
808
|
+
p_xy_e = unit_adjusted_p
|
583
809
|
flange_array = None
|
584
810
|
log.debug('number of points going into dt: ' + str(len(p_xy_e)))
|
585
811
|
success = False
|
@@ -594,19 +820,214 @@ class Surface(rqsb.BaseSurface):
|
|
594
820
|
t = triangulate.dt(p_xy_e[:, :2], container_size_factor = convexity_parameter * 1.1)
|
595
821
|
log.debug('number of triangles: ' + str(len(t)))
|
596
822
|
if make_clockwise:
|
597
|
-
triangulate.make_all_clockwise_xy(t,
|
823
|
+
triangulate.make_all_clockwise_xy(t, p_xy_e) # modifies t in situ
|
598
824
|
if extend_with_flange:
|
599
825
|
flange_array = np.zeros(len(t), dtype = bool)
|
600
826
|
flange_array[:] = np.where(np.any(t >= len(p), axis = 1), True, False)
|
601
|
-
if
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
827
|
+
if simple_saucer_angle is not None:
|
828
|
+
assert abs(simple_saucer_angle) < 90.0
|
829
|
+
z_shift = radius * maths.tan(vec.radians_from_degrees(simple_saucer_angle))
|
830
|
+
flange_points[:, 2] -= z_shift
|
831
|
+
flange_points_reverse_oriented = vec.rotate_array(reorient_matrix.T, flange_points)
|
832
|
+
if crs.xy_units != crs.z_units:
|
833
|
+
wam.convert_lengths(flange_points_reverse_oriented[:, 2], crs.xy_units, crs.z_units)
|
834
|
+
p_e = np.concatenate((p, flange_points_reverse_oriented))
|
835
|
+
else:
|
836
|
+
p_e = p
|
606
837
|
self.crs_uuid = point_set.crs_uuid
|
607
838
|
self.set_from_triangles_and_points(t, p_e)
|
608
839
|
return flange_array
|
609
840
|
|
841
|
+
def extend_surface_with_flange(self,
|
842
|
+
convexity_parameter = 5.0,
|
843
|
+
reorient = False,
|
844
|
+
reorient_max_dip = None,
|
845
|
+
flange_point_count = 11,
|
846
|
+
flange_radial_factor = 10.0,
|
847
|
+
flange_radial_distance = None,
|
848
|
+
flange_inner_ring = False,
|
849
|
+
saucer_parameter = None,
|
850
|
+
make_clockwise = False,
|
851
|
+
retriangulate = False,
|
852
|
+
normal_vector = None):
|
853
|
+
"""Returns a new Surface object where the original surface has been extended with a flange with a Delaunay triangulation of points in a PointSet object.
|
854
|
+
|
855
|
+
arguments:
|
856
|
+
convexity_parameter (float, default 5.0): controls how likely the resulting triangulation is to be
|
857
|
+
convex; reduce to 1.0 to allow slightly more concavities; increase to 100.0 or more for very little
|
858
|
+
chance of even a slight concavity
|
859
|
+
reorient (bool, default False): if True, a copy of the points is made and reoriented to minimise the
|
860
|
+
z range (ie. z axis is approximate normal to plane of points), to enhace the triangulation; if
|
861
|
+
normal_vector is supplied that is used to determine the reorientation instead of minimising z
|
862
|
+
reorient_max_dip (float, optional): if present, the reorientation of perspective off vertical is
|
863
|
+
limited to this angle in degrees; ignored if normal_vector is specified
|
864
|
+
flange_point_count (int, default 11): the number of points to generate in the flange ring; ignored if
|
865
|
+
retriangulate is False
|
866
|
+
flange_radial_factor (float, default 10.0): distance of flange points from centre of points, as a
|
867
|
+
factor of the maximum radial distance of the points themselves; ignored if extend_with_flange is False
|
868
|
+
flange_radial_distance (float, optional): if present, the minimum absolute distance of flange points from
|
869
|
+
centre of points; units are those of the crs
|
870
|
+
flange_inner_ring (bool, default False): if True, an inner ring of points, with double flange point counr,
|
871
|
+
is created at a radius just outside that of the furthest flung original point; this improves
|
872
|
+
triangulation of the extended point set when the original has a non-convex hull. Ignored if retriangulate
|
873
|
+
is False
|
874
|
+
saucer_parameter (float, optional): if present, and extend_with_flange is True, then a parameter
|
875
|
+
controlling the shift of flange points in a perpendicular direction away from the fault plane;
|
876
|
+
see notes for how this parameter is interpreted
|
877
|
+
make_clockwise (bool, default False): if True, the returned triangles will all be clockwise when
|
878
|
+
viewed in the direction -ve to +ve z axis; if reorient is also True, the clockwise aspect is
|
879
|
+
enforced in the reoriented space
|
880
|
+
retriangulate (bool, default False): if True, the surface will be generated with a retriangulation of
|
881
|
+
the existing points. If False, the surface will be generated by adding flange points and triangles directly
|
882
|
+
from the original surface edges, and will no retriangulate the input surface. If False the surface must not
|
883
|
+
contain tears
|
884
|
+
normal_vector (triple float, optional): if present and reorienting, the normal vector to use for reorientation;
|
885
|
+
if None, the reorientation is made so as to minimise the z range
|
886
|
+
|
887
|
+
returns:
|
888
|
+
a new surface, and a boolean array of length N, where N is the number of triangles on the surface. This boolean
|
889
|
+
array is False on original triangle points, and True for extended flange triangles
|
890
|
+
|
891
|
+
notes:
|
892
|
+
a boolean array is created for the surface, with a value per triangle, set to False (zero) for non-flange
|
893
|
+
triangles and True (one) for flange triangles; this array is suitable for adding as a property for the
|
894
|
+
surface, with indexable element 'faces';
|
895
|
+
when flange extension occurs, the radius is the greater of the values determined from the radial factor
|
896
|
+
and radial distance arguments;
|
897
|
+
the saucer_parameter is interpreted in one of two ways: (1) +ve fractoinal values between zero and one
|
898
|
+
are the fractional distance from the centre of the points to its rim at which to sample the surface for
|
899
|
+
extrapolation and thereby modify the recumbent z of flange points; 0 will usually give shallower and
|
900
|
+
smoother saucer; larger values (must be less than one) will lead to stronger and more erratic saucer
|
901
|
+
shape in flange; (2) other values between -90.0 and 90.0 are interpreted as an angle to apply out of
|
902
|
+
the plane of the original points, to give a simple (and less computationally demanding) saucer shape;
|
903
|
+
+ve angles result in the shift being in the direction of the -ve z hemisphere; -ve angles result in
|
904
|
+
the shift being in the +ve z hemisphere; in either case the direction of the shift is perpendicular
|
905
|
+
to the average plane of the original points;
|
906
|
+
normal_vector, if supplied, should be in the crs of this surface
|
907
|
+
"""
|
908
|
+
prev_t, prev_p = self.triangles_and_points()
|
909
|
+
point_set = rqs.PointSet(self.model, crs_uuid = self.crs_uuid, title = self.title, points_array = prev_p)
|
910
|
+
if retriangulate:
|
911
|
+
out_surf = Surface(self.model, crs_uuid = self.crs_uuid, title = self.title)
|
912
|
+
return out_surf, out_surf.set_from_point_set(point_set, convexity_parameter, reorient, reorient_max_dip,
|
913
|
+
True, flange_point_count, flange_radial_factor,
|
914
|
+
flange_radial_distance, flange_inner_ring, saucer_parameter,
|
915
|
+
make_clockwise, normal_vector)
|
916
|
+
else:
|
917
|
+
simple_saucer_angle = None
|
918
|
+
if saucer_parameter is not None and (saucer_parameter > 1.0 or saucer_parameter < 0.0):
|
919
|
+
assert -90.0 < saucer_parameter < 90.0, f'simple saucer angle parameter must be less than 90 degrees; too big: {saucer_parameter}'
|
920
|
+
simple_saucer_angle = saucer_parameter
|
921
|
+
saucer_parameter = None
|
922
|
+
assert saucer_parameter is None or 0.0 <= saucer_parameter < 1.0
|
923
|
+
crs = rqc.Crs(self.model, uuid = point_set.crs_uuid)
|
924
|
+
assert prev_p.ndim >= 2
|
925
|
+
assert prev_p.shape[-1] == 3
|
926
|
+
p = prev_p.reshape((-1, 3))
|
927
|
+
if crs.xy_units == crs.z_units or not reorient:
|
928
|
+
unit_adjusted_p = p
|
929
|
+
else:
|
930
|
+
unit_adjusted_p = p.copy()
|
931
|
+
wam.convert_lengths(unit_adjusted_p[:, 2], crs.z_units, crs.xy_units)
|
932
|
+
if reorient:
|
933
|
+
p_xy, normal, reorient_matrix = triangulate.reorient(unit_adjusted_p, max_dip = reorient_max_dip)
|
934
|
+
else:
|
935
|
+
p_xy = unit_adjusted_p
|
936
|
+
normal = self.normal()
|
937
|
+
reorient_matrix = None
|
938
|
+
|
939
|
+
centre_point = np.nanmean(p_xy.reshape((-1, 3)), axis = 0) # work out the radius for the flange points
|
940
|
+
p_radius_v = np.nanmax(np.abs(p.reshape((-1, 3)) - np.expand_dims(centre_point, axis = 0)), axis = 0)[:2]
|
941
|
+
p_radius = maths.sqrt(np.sum(p_radius_v * p_radius_v))
|
942
|
+
radius = p_radius * flange_radial_factor
|
943
|
+
if flange_radial_distance is not None and flange_radial_distance > radius:
|
944
|
+
radius = flange_radial_distance
|
945
|
+
|
946
|
+
de, dc = self.distinct_edges_and_counts() # find the distinct edges and counts
|
947
|
+
unique_edge = de[dc == 1] # find hull edges (edges on only a single triangle)
|
948
|
+
hull_points = p_xy[unique_edge] # find points defining the hull edges
|
949
|
+
hull_centres = np.mean(hull_points, axis = 1) # find the centre of each edge
|
950
|
+
|
951
|
+
flange_points = np.empty(
|
952
|
+
shape = (hull_centres.shape), dtype = float
|
953
|
+
) # loop over all the hull centres, generating a flange point and finding the azimuth from the centre to the hull centre point
|
954
|
+
az = np.empty(shape = len(hull_centres), dtype = float)
|
955
|
+
for i, c in enumerate(hull_centres):
|
956
|
+
v = [centre_point[0] - c[0], centre_point[1] - c[1], centre_point[2] - c[2]]
|
957
|
+
uv = -vec.unit_vector(v)
|
958
|
+
az[i] = vec.azimuth(uv)
|
959
|
+
flange_point = centre_point + radius * uv
|
960
|
+
if simple_saucer_angle is not None:
|
961
|
+
z_shift = radius * maths.tan(vec.radians_from_degrees(simple_saucer_angle))
|
962
|
+
if reorient:
|
963
|
+
flange_point[2] -= z_shift
|
964
|
+
else:
|
965
|
+
flange_point -= (-vec.unit_vector(normal) * z_shift)
|
966
|
+
flange_points[i] = flange_point
|
967
|
+
|
968
|
+
sort_az_ind = np.argsort(np.array(az)) # sort by azimuth, to run through the hull points
|
969
|
+
new_points = np.empty(shape = (len(flange_points), 3), dtype = float)
|
970
|
+
new_triangles = np.empty(shape = (len(flange_points) * 2, 3), dtype = int)
|
971
|
+
point_offset = len(p_xy) # the indices of the new triangles will begin after this
|
972
|
+
for i, ind in enumerate(sort_az_ind): # loop over each point in azimuth order
|
973
|
+
new_points[i] = flange_points[ind]
|
974
|
+
this_hull_edge = unique_edge[ind]
|
975
|
+
|
976
|
+
def az_for_point(c):
|
977
|
+
v = [centre_point[0] - c[0], centre_point[1] - c[1], centre_point[2] - c[2]]
|
978
|
+
uv = -vec.unit_vector(v)
|
979
|
+
return vec.azimuth(uv)
|
980
|
+
|
981
|
+
this_edge_az_sort = np.array(
|
982
|
+
[az_for_point(p_xy[this_hull_edge[0]]),
|
983
|
+
az_for_point(p_xy[this_hull_edge[1]])])
|
984
|
+
if np.min(this_edge_az_sort) < az[ind] < np.max(this_edge_az_sort):
|
985
|
+
first, second = np.argsort(this_edge_az_sort)
|
986
|
+
else:
|
987
|
+
second, first = np.argsort(this_edge_az_sort)
|
988
|
+
if i != len(sort_az_ind) - 1:
|
989
|
+
new_triangles[2 * i] = np.array(
|
990
|
+
[this_hull_edge[first], this_hull_edge[second],
|
991
|
+
i + point_offset]) # add a triangle between the two hull points and the flange point
|
992
|
+
new_triangles[(2 * i) + 1] = np.array(
|
993
|
+
[this_hull_edge[second], i + point_offset,
|
994
|
+
i + point_offset + 1]) # for all but the last point, hookup triangle to the next flange point
|
995
|
+
else:
|
996
|
+
new_triangles[2 * i] = np.array(
|
997
|
+
[this_hull_edge[first], this_hull_edge[second],
|
998
|
+
i + point_offset]) # add a triangle between the two hull points and the first flange point
|
999
|
+
new_triangles[(2 * i) + 1] = np.array(
|
1000
|
+
[this_hull_edge[second], point_offset,
|
1001
|
+
i + point_offset]) # add in the final triangle between the first and last flange points
|
1002
|
+
|
1003
|
+
all_points = np.concatenate((p_xy, new_points)) # concatenate triangle and points arrays
|
1004
|
+
all_triangles = np.concatenate((prev_t, new_triangles))
|
1005
|
+
|
1006
|
+
flange_array = np.zeros(shape = all_triangles.shape[0], dtype = bool)
|
1007
|
+
flange_array[
|
1008
|
+
len(prev_t):] = True # make a flange bool array, where all new triangles are flange and therefore True
|
1009
|
+
|
1010
|
+
assert len(all_points) == (
|
1011
|
+
point_offset + len(flange_points)), "New point count should be old point count + flange point count"
|
1012
|
+
assert len(all_triangles) == (
|
1013
|
+
len(prev_t) +
|
1014
|
+
2 * len(flange_points)), "New triangle count should be old triangle count + 2 x #flange points"
|
1015
|
+
|
1016
|
+
if saucer_parameter is not None:
|
1017
|
+
_adjust_flange_z(self.model, crs.uuid, all_points, len(all_points), all_triangles, flange_array,
|
1018
|
+
saucer_parameter) # adjust the flange points if in saucer mode
|
1019
|
+
if reorient:
|
1020
|
+
all_points = vec.rotate_array(reorient_matrix.T, all_points)
|
1021
|
+
if crs.xy_units != crs.z_units and reorient:
|
1022
|
+
wam.convert_lengths(all_points[:, 2], crs.xy_units, crs.z_units)
|
1023
|
+
|
1024
|
+
if make_clockwise:
|
1025
|
+
triangulate.make_all_clockwise_xy(all_triangles, all_points) # modifies t in situ
|
1026
|
+
|
1027
|
+
out_surf = Surface(self.model, crs_uuid = self.crs_uuid, title = self.title)
|
1028
|
+
out_surf.set_from_triangles_and_points(all_triangles, all_points) # create the new surface
|
1029
|
+
return out_surf, flange_array
|
1030
|
+
|
610
1031
|
def make_all_clockwise_xy(self, reorient = False):
|
611
1032
|
"""Reorders cached triangles data such that all triangles are clockwise when viewed from -ve z axis.
|
612
1033
|
|
@@ -629,7 +1050,7 @@ class Surface(rqsb.BaseSurface):
|
|
629
1050
|
|
630
1051
|
_, p = self.triangles_and_points()
|
631
1052
|
crs = rqc.Crs(self.model, uuid = self.crs_uuid)
|
632
|
-
if crs.xy_units ==
|
1053
|
+
if crs.xy_units == crs.z_units:
|
633
1054
|
return p
|
634
1055
|
unit_adjusted_p = p.copy()
|
635
1056
|
wam.convert_lengths(unit_adjusted_p[:, 2], crs.z_units, crs.xy_units)
|
@@ -641,9 +1062,10 @@ class Surface(rqsb.BaseSurface):
|
|
641
1062
|
notes:
|
642
1063
|
the result becomes more meaningless the less planar the surface is;
|
643
1064
|
even for a parfectly planar surface, the result is approximate;
|
644
|
-
true normal vector is found when xy & z units differ
|
1065
|
+
true normal vector is found when xy & z units differ, ie. for consistent units
|
645
1066
|
"""
|
646
1067
|
|
1068
|
+
self._load_normal_vector_from_extra_metadata()
|
647
1069
|
if self.normal_vector is None:
|
648
1070
|
p = self.unit_adjusted_points()
|
649
1071
|
_, self.normal_vector, _ = triangulate.reorient(p)
|
@@ -746,7 +1168,7 @@ class Surface(rqsb.BaseSurface):
|
|
746
1168
|
|
747
1169
|
def set_to_multi_cell_faces_from_corner_points(self, cp, quad_triangles = True):
|
748
1170
|
"""Populates this (empty) surface to represent faces of a set of cells.
|
749
|
-
|
1171
|
+
|
750
1172
|
From corner points of shape (N, 2, 2, 2, 3).
|
751
1173
|
"""
|
752
1174
|
assert cp.size % 24 == 0
|
@@ -782,7 +1204,7 @@ class Surface(rqsb.BaseSurface):
|
|
782
1204
|
|
783
1205
|
def set_to_horizontal_plane(self, depth, box_xyz, border = 0.0, quad_triangles = False):
|
784
1206
|
"""Populate this (empty) surface with a patch of two triangles.
|
785
|
-
|
1207
|
+
|
786
1208
|
Triangles define a flat, horizontal plane at a given depth.
|
787
1209
|
|
788
1210
|
arguments:
|
@@ -850,7 +1272,7 @@ class Surface(rqsb.BaseSurface):
|
|
850
1272
|
v_index = None
|
851
1273
|
for line in lines:
|
852
1274
|
if "VRTX" in line:
|
853
|
-
words = line.rstrip().split(
|
1275
|
+
words = line.rstrip().split()
|
854
1276
|
v_i = int(words[1])
|
855
1277
|
if v_index is None:
|
856
1278
|
v_index = v_i
|
@@ -860,10 +1282,11 @@ class Surface(rqsb.BaseSurface):
|
|
860
1282
|
v_index = v_i
|
861
1283
|
vertices.append(words[2:5])
|
862
1284
|
elif "TRGL" in line:
|
863
|
-
triangles.append(line.rstrip().split(
|
1285
|
+
triangles.append(line.rstrip().split()[1:4])
|
864
1286
|
assert len(vertices) >= 3, 'vertices missing'
|
865
1287
|
assert len(triangles) > 0, 'triangles missing'
|
866
|
-
|
1288
|
+
t_type = np.int32 if len(vertices) <= 2_147_483_647 else np.int64
|
1289
|
+
triangles = np.array(triangles, dtype = t_type) - index_offset
|
867
1290
|
vertices = np.array(vertices, dtype = float)
|
868
1291
|
assert np.all(triangles >= 0) and np.all(triangles < len(vertices)), 'triangle vertex indices out of range'
|
869
1292
|
self.set_from_triangles_and_points(triangles = triangles, points = vertices)
|
@@ -885,7 +1308,7 @@ class Surface(rqsb.BaseSurface):
|
|
885
1308
|
|
886
1309
|
def vertical_rescale_points(self, ref_depth = None, scaling_factor = 1.0):
|
887
1310
|
"""Modify the z values of points by rescaling.
|
888
|
-
|
1311
|
+
|
889
1312
|
Stretches the distance from reference depth by scaling factor.
|
890
1313
|
"""
|
891
1314
|
if scaling_factor == 1.0:
|
@@ -901,10 +1324,10 @@ class Surface(rqsb.BaseSurface):
|
|
901
1324
|
for patch in self.patch_list:
|
902
1325
|
patch.vertical_rescale_points(ref_depth, scaling_factor)
|
903
1326
|
|
904
|
-
def line_intersection(self, line_p, line_v, line_segment = False):
|
1327
|
+
def line_intersection(self, line_p, line_v, line_segment = False, patch = None):
|
905
1328
|
"""Returns x,y,z of an intersection point of straight line with the surface, or None if no intersection found."""
|
906
1329
|
|
907
|
-
t, p = self.triangles_and_points()
|
1330
|
+
t, p = self.triangles_and_points(patch = patch)
|
908
1331
|
tp = p[t]
|
909
1332
|
intersects = meet.line_triangles_intersects(line_p, line_v, tp, line_segment = line_segment)
|
910
1333
|
indices = meet.intersects_indices(intersects)
|
@@ -912,21 +1335,22 @@ class Surface(rqsb.BaseSurface):
|
|
912
1335
|
return None
|
913
1336
|
return intersects[indices[0]]
|
914
1337
|
|
915
|
-
def sample_z_at_xy_points(self, points, multiple_handling = 'any'):
|
1338
|
+
def sample_z_at_xy_points(self, points, multiple_handling = 'any', patch = None):
|
916
1339
|
"""Returns interpolated z values for an array of xy values.
|
917
1340
|
|
918
1341
|
arguments:
|
919
1342
|
points (numpy float array of shape (..., 2 or 3)): xy points to sample surface at (z values ignored)
|
920
1343
|
multiple_handling (str, default 'any'): one of 'any', 'minimum', 'maximum', 'exception'
|
1344
|
+
patch (int, optional): patch index; if None, results are for the full surface
|
921
1345
|
|
922
1346
|
returns:
|
923
1347
|
numpy float array of shape points.shape[:-1] being z values interpolated from the surface z values
|
924
1348
|
|
925
1349
|
notes:
|
926
1350
|
points must be in the same crs as the surface;
|
927
|
-
NaN will be set for any points that do not intersect with the surface in the xy projection;
|
928
|
-
multiple_handling argument controls behaviour when one sample point intersects
|
929
|
-
|
1351
|
+
NaN will be set for any points that do not intersect with the patch or surface in the xy projection;
|
1352
|
+
multiple_handling argument controls behaviour when one sample point intersects more than once:
|
1353
|
+
'any' a random one of the intersection z values is returned; 'minimum' or 'maximum': the
|
930
1354
|
numerical min or max of the z values is returned; 'exception': a ValueError is raised
|
931
1355
|
"""
|
932
1356
|
|
@@ -938,10 +1362,10 @@ class Surface(rqsb.BaseSurface):
|
|
938
1362
|
else:
|
939
1363
|
sample_xy = np.zeros((points.size // 2, 3), dtype = float)
|
940
1364
|
sample_xy[:, :2] = points.reshape((-1, 2))
|
941
|
-
t, p = self.triangles_and_points()
|
1365
|
+
t, p = self.triangles_and_points(patch = patch)
|
942
1366
|
p_list = vec.points_in_triangles_njit(sample_xy, p[t], 1)
|
943
1367
|
vertical = np.array((0.0, 0.0, 1.0), dtype = float)
|
944
|
-
z = np.full(sample_xy.shape[0], np.
|
1368
|
+
z = np.full(sample_xy.shape[0], np.nan, dtype = float)
|
945
1369
|
for triangle_index, p_index, _ in p_list:
|
946
1370
|
# todo: replace following with cheaper trilinear interpolation, given vertical intersection line
|
947
1371
|
xyz = meet.line_triangle_intersect_numba(sample_xy[p_index], vertical, p[t[triangle_index]], t_tol = 0.05)
|
@@ -957,17 +1381,18 @@ class Surface(rqsb.BaseSurface):
|
|
957
1381
|
raise ValueError(f'multiple {self.title} surface intersections at xy: {sample_xy[p_index]}')
|
958
1382
|
return z.reshape(points.shape[:-1])
|
959
1383
|
|
960
|
-
def normal_vectors(self, add_as_property: bool = False) -> np.ndarray:
|
961
|
-
"""Returns the normal vectors for each triangle in the surface.
|
962
|
-
|
1384
|
+
def normal_vectors(self, add_as_property: bool = False, patch = None) -> np.ndarray:
|
1385
|
+
"""Returns the normal vectors for each triangle in the patch or surface.
|
1386
|
+
|
963
1387
|
arguments:
|
964
|
-
add_as_property (bool): if True, face_surface_normal_vectors_array is added as a property to the model
|
1388
|
+
add_as_property (bool): if True, face_surface_normal_vectors_array is added as a property to the model
|
1389
|
+
patch (int, optional): patch index; if None, normal vectors for triangles in all patches are returned
|
965
1390
|
|
966
1391
|
returns:
|
967
|
-
normal_vectors_array (np.ndarray): the normal vectors corresponding to each triangle in the surface
|
1392
|
+
normal_vectors_array (np.ndarray): the normal vectors corresponding to each triangle in the surface
|
968
1393
|
"""
|
969
1394
|
crs = rqc.Crs(self.model, uuid = self.crs_uuid)
|
970
|
-
triangles, points = self.triangles_and_points()
|
1395
|
+
triangles, points = self.triangles_and_points(patch = patch)
|
971
1396
|
if crs.xy_units != crs.z_units:
|
972
1397
|
points = points.copy()
|
973
1398
|
wam.convert_lengths(points[:, 2], crs.z_units, crs.xy_units)
|
@@ -1027,13 +1452,13 @@ class Surface(rqsb.BaseSurface):
|
|
1027
1452
|
return ep
|
1028
1453
|
|
1029
1454
|
def resampled_surface(self, title = None):
|
1030
|
-
"""Creates a new
|
1031
|
-
|
1455
|
+
"""Creates a new surface which is a refined version of this surface; each triangle is divided equally into 4 new triangles.
|
1456
|
+
|
1032
1457
|
arguments:
|
1033
|
-
title (str):
|
1034
|
-
|
1458
|
+
title (str): title for the output triangulated set, if None the title will be inherited from the input surface
|
1459
|
+
|
1035
1460
|
returns:
|
1036
|
-
resqpy.surface.Surface object, with extra_metadata ('resampled from surface':
|
1461
|
+
resqpy.surface.Surface object, with extra_metadata ('resampled from surface': uuid), where uuid is for the original surface uuid
|
1037
1462
|
"""
|
1038
1463
|
rt, rp = self.triangles_and_points()
|
1039
1464
|
edge1 = np.mean(rp[rt[:]][:, ::2, :], axis = 1)
|
@@ -1050,8 +1475,9 @@ class Surface(rqsb.BaseSurface):
|
|
1050
1475
|
|
1051
1476
|
# TODO: implement alternate solution using edge functions in olio triangulation to optimise
|
1052
1477
|
points_unique, inverse = np.unique(allpoints, axis = 0, return_inverse = True)
|
1053
|
-
|
1054
|
-
|
1478
|
+
t_type = np.int32 if len(allpoints) <= 2_147_483_647 else np.int64
|
1479
|
+
tris = np.array(tris, dtype = t_type)
|
1480
|
+
tris_unique = np.empty(shape = tris.shape, dtype = t_type)
|
1055
1481
|
tris_unique[:, 0] = inverse[tris[:, 0]]
|
1056
1482
|
tris_unique[:, 1] = inverse[tris[:, 1]]
|
1057
1483
|
tris_unique[:, 2] = inverse[tris[:, 2]]
|
@@ -1066,6 +1492,37 @@ class Surface(rqsb.BaseSurface):
|
|
1066
1492
|
|
1067
1493
|
return resampled
|
1068
1494
|
|
1495
|
+
def resample_surface_unique_edges(self):
|
1496
|
+
"""Returns a new surface, with the same model, title and crs as the original, but with additional refined points along tears and edges.
|
1497
|
+
|
1498
|
+
Each edge forming a tear or outer edge in the surface will have 3 additional points added, with 2 additional points
|
1499
|
+
on each edge of the original triangle. The output surface is re-triangulated using these new points (tears will be filled)
|
1500
|
+
|
1501
|
+
returns:
|
1502
|
+
new Surface object with extra_metadata ('unique edges resampled from surface': uuid), where uuid is for the original surface uuid
|
1503
|
+
|
1504
|
+
note:
|
1505
|
+
this method involves a tr-triangulation
|
1506
|
+
"""
|
1507
|
+
_, op = self.triangles_and_points()
|
1508
|
+
ref = self.resampled_surface() # resample the original surface
|
1509
|
+
rt, rp = ref.triangles_and_points()
|
1510
|
+
de, dc = ref.distinct_edges_and_counts() # find the distinct edges and counts for the resampled surface
|
1511
|
+
de_edge = de[dc == 1] # find edges that only appear once - tears or surface edges
|
1512
|
+
edge_tri_index = np.sum(np.isin(rt, de_edge), axis = 1) == 2
|
1513
|
+
edge_tris = rp[rt[edge_tri_index]]
|
1514
|
+
mid = np.mean(rp[de_edge], axis = 1) # get the midpoint of each surface edge
|
1515
|
+
edge_ref_points = np.unique(np.concatenate([op, edge_tris.reshape(-1, 3), mid]), axis = 0) # combine all points
|
1516
|
+
|
1517
|
+
points = rqs.PointSet(self.model, points_array = edge_ref_points, title = self.title,
|
1518
|
+
crs_uuid = self.crs_uuid) # generate a pointset from these points
|
1519
|
+
|
1520
|
+
output = Surface(self.model, point_set = points,
|
1521
|
+
extra_metadata = {'resampled from surface': str(self.uuid)
|
1522
|
+
}) # return a surface with generated from these points
|
1523
|
+
|
1524
|
+
return output
|
1525
|
+
|
1069
1526
|
def write_hdf5(self, file_name = None, mode = 'a'):
|
1070
1527
|
"""Create or append to an hdf5 file, writing datasets for the triangulated patches after caching arrays.
|
1071
1528
|
|
@@ -1115,7 +1572,13 @@ class Surface(rqsb.BaseSurface):
|
|
1115
1572
|
if not self.title:
|
1116
1573
|
self.title = 'surface'
|
1117
1574
|
|
1118
|
-
|
1575
|
+
em = None
|
1576
|
+
if self.normal_vector is not None and (self.extra_metadata is None or
|
1577
|
+
'normal vector' not in self.extra_metadata):
|
1578
|
+
assert len(self.normal_vector) == 3
|
1579
|
+
em = {'normal vector': f'{self.normal_vector[0]},{self.normal_vector[1]},{self.normal_vector[2]}'}
|
1580
|
+
|
1581
|
+
tri_rep = super().create_xml(add_as_part = False, title = title, originator = originator, extra_metadata = em)
|
1119
1582
|
|
1120
1583
|
# todo: if crs_uuid is None, attempt to set to surface patch crs uuid (within patch loop, below)
|
1121
1584
|
if crs_uuid is not None:
|
@@ -1221,6 +1684,16 @@ class Surface(rqsb.BaseSurface):
|
|
1221
1684
|
|
1222
1685
|
return tri_rep
|
1223
1686
|
|
1687
|
+
def _load_normal_vector_from_extra_metadata(self):
|
1688
|
+
if self.normal_vector is None and self.extra_metadata is not None:
|
1689
|
+
nv_str = self.extra_metadata.get('normal vector')
|
1690
|
+
if nv_str is not None:
|
1691
|
+
nv_words = nv_str.split(',')
|
1692
|
+
assert len(nv_words) == 3, f'failed to convert normal vector string into triplet: {nv_str}'
|
1693
|
+
self.normal_vector = np.empty(3, dtype = float)
|
1694
|
+
for i in range(3):
|
1695
|
+
self.normal_vector[i] = float(nv_words[i])
|
1696
|
+
|
1224
1697
|
|
1225
1698
|
def distill_triangle_points(t, p):
|
1226
1699
|
"""Returns a (triangles, points) pair with points distilled as only those used from p."""
|
@@ -1229,7 +1702,8 @@ def distill_triangle_points(t, p):
|
|
1229
1702
|
# find unique points used by triangles
|
1230
1703
|
p_keep = np.unique(t)
|
1231
1704
|
# note new point index for each old point that is being kept
|
1232
|
-
|
1705
|
+
t_type = np.int32 if len(p) <= 2_147_483_647 else np.int64
|
1706
|
+
p_map = np.full(len(p), -1, dtype = t_type)
|
1233
1707
|
p_map[p_keep] = np.arange(len(p_keep))
|
1234
1708
|
# copy those unique points into a trimmed points array
|
1235
1709
|
points_distilled = p[p_keep]
|
@@ -1256,8 +1730,9 @@ def nan_removed_triangles_and_points(t, p):
|
|
1256
1730
|
expanded_mask[:] = np.expand_dims(np.logical_not(t_nan_mask), axis = -1)
|
1257
1731
|
t_filtered = t[expanded_mask].reshape((-1, 3))
|
1258
1732
|
# modified the filtered t values to adjust for the compression of filtered p
|
1259
|
-
|
1260
|
-
p_map
|
1733
|
+
t_type = np.int32 if len(p) <= 2_147_483_647 else np.int64
|
1734
|
+
p_map = np.full(len(p), -1, dtype = t_type)
|
1735
|
+
p_map[p_non_nan_mask] = np.arange(len(p_filtered), dtype = t_type)
|
1261
1736
|
t_filtered = p_map[t_filtered]
|
1262
1737
|
assert t_filtered.ndim == 2 and t_filtered.shape[1] == 3
|
1263
1738
|
assert not np.any(t_filtered < 0) and not np.any(t_filtered >= len(p_filtered))
|