resqpy 4.14.2__py3-none-any.whl → 5.1.5__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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 +2 -1
- 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 +1349 -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.5.dist-info}/METADATA +8 -9
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
- resqpy/grid/_moved_functions.py +0 -15
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.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))
|