resqpy 4.18.11__py3-none-any.whl → 5.1.0__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/grid/__init__.py +2 -3
- resqpy/grid/_grid.py +1 -7
- resqpy/grid_surface/_find_faces.py +98 -29
- resqpy/lines/_polyline.py +24 -33
- resqpy/model/_model.py +9 -9
- resqpy/multi_processing/wrappers/grid_surface_mp.py +91 -44
- resqpy/olio/triangulation.py +19 -17
- resqpy/olio/volume.py +0 -20
- resqpy/property/__init__.py +3 -2
- resqpy/property/_collection_get_attributes.py +2 -0
- resqpy/rq_import/_grid_from_cp.py +2 -2
- resqpy/surface/_surface.py +377 -53
- resqpy/surface/_tri_mesh.py +3 -1
- resqpy/time_series/_any_time_series.py +5 -4
- resqpy/well/_blocked_well.py +1916 -1910
- resqpy/well/_md_datum.py +11 -21
- resqpy/well/_wellbore_frame.py +10 -2
- resqpy/well/well_utils.py +33 -0
- {resqpy-4.18.11.dist-info → resqpy-5.1.0.dist-info}/METADATA +8 -8
- {resqpy-4.18.11.dist-info → resqpy-5.1.0.dist-info}/RECORD +23 -24
- {resqpy-4.18.11.dist-info → resqpy-5.1.0.dist-info}/WHEEL +1 -1
- resqpy/grid/_moved_functions.py +0 -15
- {resqpy-4.18.11.dist-info → resqpy-5.1.0.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:
|
@@ -124,6 +125,75 @@ class Surface(rqsb.BaseSurface):
|
|
124
125
|
self.set_from_tsurf_file(tsurf_file)
|
125
126
|
self._load_normal_vector_from_extra_metadata()
|
126
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)
|
196
|
+
|
127
197
|
@classmethod
|
128
198
|
def from_tri_mesh(cls, tri_mesh, exclude_nans = False):
|
129
199
|
"""Create a Surface from a TriMesh.
|
@@ -318,6 +388,39 @@ class Surface(rqsb.BaseSurface):
|
|
318
388
|
ValueError(f'patch index {patch} out of range for surface with {len(self.patch_list)} patches')
|
319
389
|
return self.patch_list[patch].triangles_and_points(copy = copy)
|
320
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
|
+
|
321
424
|
def decache_triangles_and_points(self):
|
322
425
|
"""Removes the cached composite triangles and points arrays."""
|
323
426
|
self.points = None
|
@@ -368,9 +471,11 @@ class Surface(rqsb.BaseSurface):
|
|
368
471
|
def change_crs(self, required_crs):
|
369
472
|
"""Changes the crs of the surface, also sets a new uuid if crs changed.
|
370
473
|
|
371
|
-
|
474
|
+
notes:
|
372
475
|
this method is usually used to change the coordinate system for a temporary resqpy object;
|
373
|
-
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
|
374
479
|
"""
|
375
480
|
|
376
481
|
old_crs = rqc.Crs(self.model, uuid = self.crs_uuid)
|
@@ -385,6 +490,18 @@ class Surface(rqsb.BaseSurface):
|
|
385
490
|
patch.crs_uuid = self.crs_uuid
|
386
491
|
self.triangles = None # clear cached arrays for surface
|
387
492
|
self.points = None
|
493
|
+
if self.extra_metadata.pop('rotation matrix', None) is not None:
|
494
|
+
log.warning(f'discarding rotation matrix extra metadata during crs change of: {self.title}')
|
495
|
+
self._load_normal_vector_from_extra_metadata()
|
496
|
+
if self.normal_vector is not None:
|
497
|
+
if required_crs.z_inc_down != old_crs.z_inc_down:
|
498
|
+
self.normal_vector[2] = -self.normal_vector[2]
|
499
|
+
theta = (wam.convert(required_crs.rotation, required_crs.rotation_units, 'dega') -
|
500
|
+
wam.convert(old_crs.rotation, old_crs.rotation_units, 'dega'))
|
501
|
+
if not maths.isclose(theta, 0.0):
|
502
|
+
self.normal_vector = vec.rotate_vector(vec.rotation_matrix_3d_axial(2, theta), self.normal_vector)
|
503
|
+
self.extra_metadata['normal vector'] = str(
|
504
|
+
f'{self.normal_vector[0]},{self.normal_vector[1]},{self.normal_vector[2]}')
|
388
505
|
self.uuid = bu.new_uuid() # hope this doesn't cause problems
|
389
506
|
assert self.root is None
|
390
507
|
|
@@ -579,7 +696,8 @@ class Surface(rqsb.BaseSurface):
|
|
579
696
|
flange_radial_distance = None,
|
580
697
|
flange_inner_ring = False,
|
581
698
|
saucer_parameter = None,
|
582
|
-
make_clockwise = False
|
699
|
+
make_clockwise = False,
|
700
|
+
normal_vector = None):
|
583
701
|
"""Populate this (empty) Surface object with a Delaunay triangulation of points in a PointSet object.
|
584
702
|
|
585
703
|
arguments:
|
@@ -588,9 +706,10 @@ class Surface(rqsb.BaseSurface):
|
|
588
706
|
convex; reduce to 1.0 to allow slightly more concavities; increase to 100.0 or more for very little
|
589
707
|
chance of even a slight concavity
|
590
708
|
reorient (bool, default False): if True, a copy of the points is made and reoriented to minimise the
|
591
|
-
z range (ie. z axis is approximate normal to plane of points), to enhace the triangulation
|
709
|
+
z range (ie. z axis is approximate normal to plane of points), to enhace the triangulation; if a
|
710
|
+
normal_vector is supplied, the reorientation is based on that instead of minimising z
|
592
711
|
reorient_max_dip (float, optional): if present, the reorientation of perspective off vertical is
|
593
|
-
limited to this angle in degrees
|
712
|
+
limited to this angle in degrees; ignored if normal_vector is specified
|
594
713
|
extend_with_flange (bool, default False): if True, a ring of points is added around the outside of the
|
595
714
|
points before the triangulation, effectively extending the surface with a flange
|
596
715
|
flange_point_count (int, default 11): the number of points to generate in the flange ring; ignored if
|
@@ -608,10 +727,12 @@ class Surface(rqsb.BaseSurface):
|
|
608
727
|
make_clockwise (bool, default False): if True, the returned triangles will all be clockwise when
|
609
728
|
viewed in the direction -ve to +ve z axis; if reorient is also True, the clockwise aspect is
|
610
729
|
enforced in the reoriented space
|
730
|
+
normal_vector (triple float, optional): if present and reorienting, the normal vector to use for reorientation;
|
731
|
+
if None, the reorientation is made so as to minimise the z range
|
611
732
|
|
612
733
|
returns:
|
613
734
|
if extend_with_flange is True, numpy bool array with a value per triangle indicating flange triangles;
|
614
|
-
if
|
735
|
+
if extend_with_flange is False, None
|
615
736
|
|
616
737
|
notes:
|
617
738
|
if extend_with_flange is True, then a boolean array is created for the surface, with a value per triangle,
|
@@ -619,23 +740,18 @@ class Surface(rqsb.BaseSurface):
|
|
619
740
|
suitable for adding as a property for the surface, with indexable element 'faces';
|
620
741
|
when flange extension occurs, the radius is the greater of the values determined from the radial factor
|
621
742
|
and radial distance arguments;
|
622
|
-
the saucer_parameter
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
the plane of the original points, to give a simple (and less computationally demanding) saucer shape;
|
628
|
-
+ve angles result in the shift being in the direction of the -ve z hemisphere; -ve angles result in
|
629
|
-
the shift being in the +ve z hemisphere; in either case the direction of the shift is perpendicular
|
630
|
-
to the average plane of the original points
|
743
|
+
the saucer_parameter must be between -90.0 and 90.0, and is interpreted as an angle to apply out of
|
744
|
+
the plane of the original points, to give a simple saucer shape; +ve angles result in the shift being in
|
745
|
+
the direction of the -ve z hemisphere; -ve angles result in the shift being in the +ve z hemisphere; in
|
746
|
+
either case the direction of the shift is perpendicular to the average plane of the original points;
|
747
|
+
normal_vector, if supplied, should be in the crs of the point set
|
631
748
|
"""
|
632
749
|
|
633
750
|
simple_saucer_angle = None
|
634
|
-
if saucer_parameter is not None
|
751
|
+
if saucer_parameter is not None:
|
635
752
|
assert -90.0 < saucer_parameter < 90.0, f'simple saucer angle parameter must be less than 90 degrees; too big: {saucer_parameter}'
|
636
753
|
simple_saucer_angle = saucer_parameter
|
637
754
|
saucer_parameter = None
|
638
|
-
assert saucer_parameter is None or 0.0 <= saucer_parameter < 1.0
|
639
755
|
crs = rqc.Crs(self.model, uuid = point_set.crs_uuid)
|
640
756
|
p = point_set.full_array_ref()
|
641
757
|
assert p.ndim >= 2
|
@@ -648,37 +764,44 @@ class Surface(rqsb.BaseSurface):
|
|
648
764
|
f'removing {len(p) - np.count_nonzero(row_mask)} NaN points from point set {point_set.title} prior to surface triangulation'
|
649
765
|
)
|
650
766
|
p = p[row_mask, :]
|
651
|
-
if crs.xy_units == crs.z_units
|
767
|
+
if crs.xy_units == crs.z_units:
|
652
768
|
unit_adjusted_p = p
|
653
769
|
else:
|
654
770
|
unit_adjusted_p = p.copy()
|
655
771
|
wam.convert_lengths(unit_adjusted_p[:, 2], crs.z_units, crs.xy_units)
|
656
|
-
|
772
|
+
# note: normal vector should already be for a crs with common xy & z units
|
773
|
+
# reorient the points to the fault normal vector
|
774
|
+
if normal_vector is None:
|
657
775
|
p_xy, self.normal_vector, reorient_matrix = triangulate.reorient(unit_adjusted_p,
|
658
776
|
max_dip = reorient_max_dip)
|
659
777
|
else:
|
660
|
-
|
778
|
+
assert len(normal_vector) == 3
|
779
|
+
self.normal_vector = np.array(normal_vector, dtype = np.float64)
|
780
|
+
if self.normal_vector[2] < 0.0:
|
781
|
+
self.normal_vector = -self.normal_vector
|
782
|
+
incl = vec.inclination(normal_vector)
|
783
|
+
if maths.isclose(incl, 0.0):
|
784
|
+
reorient_matrix = vec.no_rotation_matrix()
|
785
|
+
p_xy = unit_adjusted_p
|
786
|
+
else:
|
787
|
+
azi = vec.azimuth(normal_vector)
|
788
|
+
reorient_matrix = vec.tilt_3d_matrix(azi, incl)
|
789
|
+
p_xy = vec.rotate_array(reorient_matrix, unit_adjusted_p)
|
661
790
|
if extend_with_flange:
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
radial_distance = flange_radial_distance,
|
670
|
-
inner_ring = flange_inner_ring,
|
671
|
-
saucer_angle = simple_saucer_angle)
|
672
|
-
p_xy_e = np.concatenate((p_xy, flange_points), axis = 0)
|
791
|
+
flange_points, radius = triangulate.surrounding_xy_ring(p_xy,
|
792
|
+
count = flange_point_count,
|
793
|
+
radial_factor = flange_radial_factor,
|
794
|
+
radial_distance = flange_radial_distance,
|
795
|
+
inner_ring = flange_inner_ring,
|
796
|
+
saucer_angle = 0.0)
|
797
|
+
flange_points_reverse_oriented = vec.rotate_array(reorient_matrix.T, flange_points)
|
673
798
|
if reorient:
|
674
|
-
|
675
|
-
flange_points_reverse_oriented = vec.rotate_array(reorient_matrix.T, flange_points)
|
676
|
-
p_e = np.concatenate((unit_adjusted_p, flange_points_reverse_oriented), axis = 0)
|
799
|
+
p_xy_e = np.concatenate((p_xy, flange_points), axis = 0)
|
677
800
|
else:
|
678
|
-
|
801
|
+
p_xy_e = np.concatenate((unit_adjusted_p, flange_points_reverse_oriented), axis = 0)
|
802
|
+
|
679
803
|
else:
|
680
|
-
p_xy_e =
|
681
|
-
p_e = unit_adjusted_p
|
804
|
+
p_xy_e = unit_adjusted_p
|
682
805
|
flange_array = None
|
683
806
|
log.debug('number of points going into dt: ' + str(len(p_xy_e)))
|
684
807
|
success = False
|
@@ -693,19 +816,214 @@ class Surface(rqsb.BaseSurface):
|
|
693
816
|
t = triangulate.dt(p_xy_e[:, :2], container_size_factor = convexity_parameter * 1.1)
|
694
817
|
log.debug('number of triangles: ' + str(len(t)))
|
695
818
|
if make_clockwise:
|
696
|
-
triangulate.make_all_clockwise_xy(t,
|
819
|
+
triangulate.make_all_clockwise_xy(t, p_xy_e) # modifies t in situ
|
697
820
|
if extend_with_flange:
|
698
821
|
flange_array = np.zeros(len(t), dtype = bool)
|
699
822
|
flange_array[:] = np.where(np.any(t >= len(p), axis = 1), True, False)
|
700
|
-
if
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
823
|
+
if simple_saucer_angle is not None:
|
824
|
+
assert abs(simple_saucer_angle) < 90.0
|
825
|
+
z_shift = radius * maths.tan(vec.radians_from_degrees(simple_saucer_angle))
|
826
|
+
flange_points[:, 2] -= z_shift
|
827
|
+
flange_points_reverse_oriented = vec.rotate_array(reorient_matrix.T, flange_points)
|
828
|
+
if crs.xy_units != crs.z_units:
|
829
|
+
wam.convert_lengths(flange_points_reverse_oriented[:, 2], crs.xy_units, crs.z_units)
|
830
|
+
p_e = np.concatenate((p, flange_points_reverse_oriented))
|
831
|
+
else:
|
832
|
+
p_e = p
|
705
833
|
self.crs_uuid = point_set.crs_uuid
|
706
834
|
self.set_from_triangles_and_points(t, p_e)
|
707
835
|
return flange_array
|
708
836
|
|
837
|
+
def extend_surface_with_flange(self,
|
838
|
+
convexity_parameter = 5.0,
|
839
|
+
reorient = False,
|
840
|
+
reorient_max_dip = None,
|
841
|
+
flange_point_count = 11,
|
842
|
+
flange_radial_factor = 10.0,
|
843
|
+
flange_radial_distance = None,
|
844
|
+
flange_inner_ring = False,
|
845
|
+
saucer_parameter = None,
|
846
|
+
make_clockwise = False,
|
847
|
+
retriangulate = False,
|
848
|
+
normal_vector = None):
|
849
|
+
"""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.
|
850
|
+
|
851
|
+
arguments:
|
852
|
+
convexity_parameter (float, default 5.0): controls how likely the resulting triangulation is to be
|
853
|
+
convex; reduce to 1.0 to allow slightly more concavities; increase to 100.0 or more for very little
|
854
|
+
chance of even a slight concavity
|
855
|
+
reorient (bool, default False): if True, a copy of the points is made and reoriented to minimise the
|
856
|
+
z range (ie. z axis is approximate normal to plane of points), to enhace the triangulation; if
|
857
|
+
normal_vector is supplied that is used to determine the reorientation instead of minimising z
|
858
|
+
reorient_max_dip (float, optional): if present, the reorientation of perspective off vertical is
|
859
|
+
limited to this angle in degrees; ignored if normal_vector is specified
|
860
|
+
flange_point_count (int, default 11): the number of points to generate in the flange ring; ignored if
|
861
|
+
retriangulate is False
|
862
|
+
flange_radial_factor (float, default 10.0): distance of flange points from centre of points, as a
|
863
|
+
factor of the maximum radial distance of the points themselves; ignored if extend_with_flange is False
|
864
|
+
flange_radial_distance (float, optional): if present, the minimum absolute distance of flange points from
|
865
|
+
centre of points; units are those of the crs
|
866
|
+
flange_inner_ring (bool, default False): if True, an inner ring of points, with double flange point counr,
|
867
|
+
is created at a radius just outside that of the furthest flung original point; this improves
|
868
|
+
triangulation of the extended point set when the original has a non-convex hull. Ignored if retriangulate
|
869
|
+
is False
|
870
|
+
saucer_parameter (float, optional): if present, and extend_with_flange is True, then a parameter
|
871
|
+
controlling the shift of flange points in a perpendicular direction away from the fault plane;
|
872
|
+
see notes for how this parameter is interpreted
|
873
|
+
make_clockwise (bool, default False): if True, the returned triangles will all be clockwise when
|
874
|
+
viewed in the direction -ve to +ve z axis; if reorient is also True, the clockwise aspect is
|
875
|
+
enforced in the reoriented space
|
876
|
+
retriangulate (bool, default False): if True, the surface will be generated with a retriangulation of
|
877
|
+
the existing points. If False, the surface will be generated by adding flange points and triangles directly
|
878
|
+
from the original surface edges, and will no retriangulate the input surface. If False the surface must not
|
879
|
+
contain tears
|
880
|
+
normal_vector (triple float, optional): if present and reorienting, the normal vector to use for reorientation;
|
881
|
+
if None, the reorientation is made so as to minimise the z range
|
882
|
+
|
883
|
+
returns:
|
884
|
+
a new surface, and a boolean array of length N, where N is the number of triangles on the surface. This boolean
|
885
|
+
array is False on original triangle points, and True for extended flange triangles
|
886
|
+
|
887
|
+
notes:
|
888
|
+
a boolean array is created for the surface, with a value per triangle, set to False (zero) for non-flange
|
889
|
+
triangles and True (one) for flange triangles; this array is suitable for adding as a property for the
|
890
|
+
surface, with indexable element 'faces';
|
891
|
+
when flange extension occurs, the radius is the greater of the values determined from the radial factor
|
892
|
+
and radial distance arguments;
|
893
|
+
the saucer_parameter is interpreted in one of two ways: (1) +ve fractoinal values between zero and one
|
894
|
+
are the fractional distance from the centre of the points to its rim at which to sample the surface for
|
895
|
+
extrapolation and thereby modify the recumbent z of flange points; 0 will usually give shallower and
|
896
|
+
smoother saucer; larger values (must be less than one) will lead to stronger and more erratic saucer
|
897
|
+
shape in flange; (2) other values between -90.0 and 90.0 are interpreted as an angle to apply out of
|
898
|
+
the plane of the original points, to give a simple (and less computationally demanding) saucer shape;
|
899
|
+
+ve angles result in the shift being in the direction of the -ve z hemisphere; -ve angles result in
|
900
|
+
the shift being in the +ve z hemisphere; in either case the direction of the shift is perpendicular
|
901
|
+
to the average plane of the original points;
|
902
|
+
normal_vector, if supplied, should be in the crs of this surface
|
903
|
+
"""
|
904
|
+
prev_t, prev_p = self.triangles_and_points()
|
905
|
+
point_set = rqs.PointSet(self.model, crs_uuid = self.crs_uuid, title = self.title, points_array = prev_p)
|
906
|
+
if retriangulate:
|
907
|
+
out_surf = Surface(self.model, crs_uuid = self.crs_uuid, title = self.title)
|
908
|
+
return out_surf, out_surf.set_from_point_set(point_set, convexity_parameter, reorient, reorient_max_dip,
|
909
|
+
True, flange_point_count, flange_radial_factor,
|
910
|
+
flange_radial_distance, flange_inner_ring, saucer_parameter,
|
911
|
+
make_clockwise, normal_vector)
|
912
|
+
else:
|
913
|
+
simple_saucer_angle = None
|
914
|
+
if saucer_parameter is not None and (saucer_parameter > 1.0 or saucer_parameter < 0.0):
|
915
|
+
assert -90.0 < saucer_parameter < 90.0, f'simple saucer angle parameter must be less than 90 degrees; too big: {saucer_parameter}'
|
916
|
+
simple_saucer_angle = saucer_parameter
|
917
|
+
saucer_parameter = None
|
918
|
+
assert saucer_parameter is None or 0.0 <= saucer_parameter < 1.0
|
919
|
+
crs = rqc.Crs(self.model, uuid = point_set.crs_uuid)
|
920
|
+
assert prev_p.ndim >= 2
|
921
|
+
assert prev_p.shape[-1] == 3
|
922
|
+
p = prev_p.reshape((-1, 3))
|
923
|
+
if crs.xy_units == crs.z_units or not reorient:
|
924
|
+
unit_adjusted_p = p
|
925
|
+
else:
|
926
|
+
unit_adjusted_p = p.copy()
|
927
|
+
wam.convert_lengths(unit_adjusted_p[:, 2], crs.z_units, crs.xy_units)
|
928
|
+
if reorient:
|
929
|
+
p_xy, normal, reorient_matrix = triangulate.reorient(unit_adjusted_p, max_dip = reorient_max_dip)
|
930
|
+
else:
|
931
|
+
p_xy = unit_adjusted_p
|
932
|
+
normal = self.normal()
|
933
|
+
reorient_matrix = None
|
934
|
+
|
935
|
+
centre_point = np.nanmean(p_xy.reshape((-1, 3)), axis = 0) # work out the radius for the flange points
|
936
|
+
p_radius_v = np.nanmax(np.abs(p.reshape((-1, 3)) - np.expand_dims(centre_point, axis = 0)), axis = 0)[:2]
|
937
|
+
p_radius = maths.sqrt(np.sum(p_radius_v * p_radius_v))
|
938
|
+
radius = p_radius * flange_radial_factor
|
939
|
+
if flange_radial_distance is not None and flange_radial_distance > radius:
|
940
|
+
radius = flange_radial_distance
|
941
|
+
|
942
|
+
de, dc = self.distinct_edges_and_counts() # find the distinct edges and counts
|
943
|
+
unique_edge = de[dc == 1] # find hull edges (edges on only a single triangle)
|
944
|
+
hull_points = p_xy[unique_edge] # find points defining the hull edges
|
945
|
+
hull_centres = np.mean(hull_points, axis = 1) # find the centre of each edge
|
946
|
+
|
947
|
+
flange_points = np.empty(
|
948
|
+
shape = (hull_centres.shape), dtype = float
|
949
|
+
) # loop over all the hull centres, generating a flange point and finding the azimuth from the centre to the hull centre point
|
950
|
+
az = np.empty(shape = len(hull_centres), dtype = float)
|
951
|
+
for i, c in enumerate(hull_centres):
|
952
|
+
v = [centre_point[0] - c[0], centre_point[1] - c[1], centre_point[2] - c[2]]
|
953
|
+
uv = -vec.unit_vector(v)
|
954
|
+
az[i] = vec.azimuth(uv)
|
955
|
+
flange_point = centre_point + radius * uv
|
956
|
+
if simple_saucer_angle is not None:
|
957
|
+
z_shift = radius * maths.tan(vec.radians_from_degrees(simple_saucer_angle))
|
958
|
+
if reorient:
|
959
|
+
flange_point[2] -= z_shift
|
960
|
+
else:
|
961
|
+
flange_point -= (-vec.unit_vector(normal) * z_shift)
|
962
|
+
flange_points[i] = flange_point
|
963
|
+
|
964
|
+
sort_az_ind = np.argsort(np.array(az)) # sort by azimuth, to run through the hull points
|
965
|
+
new_points = np.empty(shape = (len(flange_points), 3), dtype = float)
|
966
|
+
new_triangles = np.empty(shape = (len(flange_points) * 2, 3), dtype = int)
|
967
|
+
point_offset = len(p_xy) # the indices of the new triangles will begin after this
|
968
|
+
for i, ind in enumerate(sort_az_ind): # loop over each point in azimuth order
|
969
|
+
new_points[i] = flange_points[ind]
|
970
|
+
this_hull_edge = unique_edge[ind]
|
971
|
+
|
972
|
+
def az_for_point(c):
|
973
|
+
v = [centre_point[0] - c[0], centre_point[1] - c[1], centre_point[2] - c[2]]
|
974
|
+
uv = -vec.unit_vector(v)
|
975
|
+
return vec.azimuth(uv)
|
976
|
+
|
977
|
+
this_edge_az_sort = np.array(
|
978
|
+
[az_for_point(p_xy[this_hull_edge[0]]),
|
979
|
+
az_for_point(p_xy[this_hull_edge[1]])])
|
980
|
+
if np.min(this_edge_az_sort) < az[ind] < np.max(this_edge_az_sort):
|
981
|
+
first, second = np.argsort(this_edge_az_sort)
|
982
|
+
else:
|
983
|
+
second, first = np.argsort(this_edge_az_sort)
|
984
|
+
if i != len(sort_az_ind) - 1:
|
985
|
+
new_triangles[2 * i] = np.array(
|
986
|
+
[this_hull_edge[first], this_hull_edge[second],
|
987
|
+
i + point_offset]) # add a triangle between the two hull points and the flange point
|
988
|
+
new_triangles[(2 * i) + 1] = np.array(
|
989
|
+
[this_hull_edge[second], i + point_offset,
|
990
|
+
i + point_offset + 1]) # for all but the last point, hookup triangle to the next flange point
|
991
|
+
else:
|
992
|
+
new_triangles[2 * i] = np.array(
|
993
|
+
[this_hull_edge[first], this_hull_edge[second],
|
994
|
+
i + point_offset]) # add a triangle between the two hull points and the first flange point
|
995
|
+
new_triangles[(2 * i) + 1] = np.array(
|
996
|
+
[this_hull_edge[second], point_offset,
|
997
|
+
i + point_offset]) # add in the final triangle between the first and last flange points
|
998
|
+
|
999
|
+
all_points = np.concatenate((p_xy, new_points)) # concatenate triangle and points arrays
|
1000
|
+
all_triangles = np.concatenate((prev_t, new_triangles))
|
1001
|
+
|
1002
|
+
flange_array = np.zeros(shape = all_triangles.shape[0], dtype = bool)
|
1003
|
+
flange_array[
|
1004
|
+
len(prev_t):] = True # make a flange bool array, where all new triangles are flange and therefore True
|
1005
|
+
|
1006
|
+
assert len(all_points) == (
|
1007
|
+
point_offset + len(flange_points)), "New point count should be old point count + flange point count"
|
1008
|
+
assert len(all_triangles) == (
|
1009
|
+
len(prev_t) +
|
1010
|
+
2 * len(flange_points)), "New triangle count should be old triangle count + 2 x #flange points"
|
1011
|
+
|
1012
|
+
if saucer_parameter is not None:
|
1013
|
+
_adjust_flange_z(self.model, crs.uuid, all_points, len(all_points), all_triangles, flange_array,
|
1014
|
+
saucer_parameter) # adjust the flange points if in saucer mode
|
1015
|
+
if reorient:
|
1016
|
+
all_points = vec.rotate_array(reorient_matrix.T, all_points)
|
1017
|
+
if crs.xy_units != crs.z_units and reorient:
|
1018
|
+
wam.convert_lengths(all_points[:, 2], crs.xy_units, crs.z_units)
|
1019
|
+
|
1020
|
+
if make_clockwise:
|
1021
|
+
triangulate.make_all_clockwise_xy(all_triangles, all_points) # modifies t in situ
|
1022
|
+
|
1023
|
+
out_surf = Surface(self.model, crs_uuid = self.crs_uuid, title = self.title)
|
1024
|
+
out_surf.set_from_triangles_and_points(all_triangles, all_points) # create the new surface
|
1025
|
+
return out_surf, flange_array
|
1026
|
+
|
709
1027
|
def make_all_clockwise_xy(self, reorient = False):
|
710
1028
|
"""Reorders cached triangles data such that all triangles are clockwise when viewed from -ve z axis.
|
711
1029
|
|
@@ -740,9 +1058,10 @@ class Surface(rqsb.BaseSurface):
|
|
740
1058
|
notes:
|
741
1059
|
the result becomes more meaningless the less planar the surface is;
|
742
1060
|
even for a parfectly planar surface, the result is approximate;
|
743
|
-
true normal vector is found when xy & z units differ
|
1061
|
+
true normal vector is found when xy & z units differ, ie. for consistent units
|
744
1062
|
"""
|
745
1063
|
|
1064
|
+
self._load_normal_vector_from_extra_metadata()
|
746
1065
|
if self.normal_vector is None:
|
747
1066
|
p = self.unit_adjusted_points()
|
748
1067
|
_, self.normal_vector, _ = triangulate.reorient(p)
|
@@ -845,7 +1164,7 @@ class Surface(rqsb.BaseSurface):
|
|
845
1164
|
|
846
1165
|
def set_to_multi_cell_faces_from_corner_points(self, cp, quad_triangles = True):
|
847
1166
|
"""Populates this (empty) surface to represent faces of a set of cells.
|
848
|
-
|
1167
|
+
|
849
1168
|
From corner points of shape (N, 2, 2, 2, 3).
|
850
1169
|
"""
|
851
1170
|
assert cp.size % 24 == 0
|
@@ -881,7 +1200,7 @@ class Surface(rqsb.BaseSurface):
|
|
881
1200
|
|
882
1201
|
def set_to_horizontal_plane(self, depth, box_xyz, border = 0.0, quad_triangles = False):
|
883
1202
|
"""Populate this (empty) surface with a patch of two triangles.
|
884
|
-
|
1203
|
+
|
885
1204
|
Triangles define a flat, horizontal plane at a given depth.
|
886
1205
|
|
887
1206
|
arguments:
|
@@ -985,7 +1304,7 @@ class Surface(rqsb.BaseSurface):
|
|
985
1304
|
|
986
1305
|
def vertical_rescale_points(self, ref_depth = None, scaling_factor = 1.0):
|
987
1306
|
"""Modify the z values of points by rescaling.
|
988
|
-
|
1307
|
+
|
989
1308
|
Stretches the distance from reference depth by scaling factor.
|
990
1309
|
"""
|
991
1310
|
if scaling_factor == 1.0:
|
@@ -1170,12 +1489,16 @@ class Surface(rqsb.BaseSurface):
|
|
1170
1489
|
return resampled
|
1171
1490
|
|
1172
1491
|
def resample_surface_unique_edges(self):
|
1173
|
-
"""Returns a new surface, with the same model, title and crs as the original
|
1174
|
-
|
1175
|
-
Each edge forming a tear or outer edge in the surface will have 3 additional points added, with 2 additional points
|
1492
|
+
"""Returns a new surface, with the same model, title and crs as the original, but with additional refined points along tears and edges.
|
1493
|
+
|
1494
|
+
Each edge forming a tear or outer edge in the surface will have 3 additional points added, with 2 additional points
|
1495
|
+
on each edge of the original triangle. The output surface is re-triangulated using these new points (tears will be filled)
|
1176
1496
|
|
1177
1497
|
returns:
|
1178
|
-
|
1498
|
+
new Surface object with extra_metadata ('unique edges resampled from surface': uuid), where uuid is for the original surface uuid
|
1499
|
+
|
1500
|
+
note:
|
1501
|
+
this method involves a tr-triangulation
|
1179
1502
|
"""
|
1180
1503
|
_, op = self.triangles_and_points()
|
1181
1504
|
ref = self.resampled_surface() # resample the original surface
|
@@ -1246,7 +1569,8 @@ class Surface(rqsb.BaseSurface):
|
|
1246
1569
|
self.title = 'surface'
|
1247
1570
|
|
1248
1571
|
em = None
|
1249
|
-
if self.normal_vector is not None
|
1572
|
+
if self.normal_vector is not None and (self.extra_metadata is None or
|
1573
|
+
'normal vector' not in self.extra_metadata):
|
1250
1574
|
assert len(self.normal_vector) == 3
|
1251
1575
|
em = {'normal vector': f'{self.normal_vector[0]},{self.normal_vector[1]},{self.normal_vector[2]}'}
|
1252
1576
|
|
resqpy/surface/_tri_mesh.py
CHANGED
@@ -219,6 +219,7 @@ class TriMesh(rqs.Mesh):
|
|
219
219
|
base edge towards point 2; f1 is component towards point 1; f0 is component towards point 0;
|
220
220
|
the trilinear coordinates sum to one and can be used as weights to interpolate z values at points
|
221
221
|
"""
|
222
|
+
assert xy_array.ndim > 1 and 2 <= xy_array.shape[-1] <= 3
|
222
223
|
x = xy_array[..., 0].copy()
|
223
224
|
y = xy_array[..., 1].copy()
|
224
225
|
if self.origin is not None:
|
@@ -236,7 +237,7 @@ class TriMesh(rqs.Mesh):
|
|
236
237
|
mask = np.logical_or(mask, np.logical_or(i < 0, i >= self.ni - 1))
|
237
238
|
fx = ip - i.astype(float)
|
238
239
|
i *= 2
|
239
|
-
am =
|
240
|
+
am = (fx > 1.0 - fy).astype(bool)
|
240
241
|
i[am] += 1
|
241
242
|
fx[am] -= 1.0 - fy[am]
|
242
243
|
fy[am] = 1.0 - fy[am]
|
@@ -275,6 +276,7 @@ class TriMesh(rqs.Mesh):
|
|
275
276
|
of three vertices of triangles containing xy points; float triplets contain corresponding weights (summing to
|
276
277
|
one per triangle) which can be used to interpolate z values at points xy_array
|
277
278
|
"""
|
279
|
+
assert xy_array.ndim > 1 and 2 <= xy_array.shape[-1] <= 3
|
278
280
|
tji, tc = self.tji_tc_for_xy_array(xy_array)
|
279
281
|
ji = self.tri_nodes_for_tji_array(tji)
|
280
282
|
return (ji, tc)
|
@@ -34,14 +34,15 @@ class AnyTimeSeries(BaseResqpy):
|
|
34
34
|
dt_text = rqet.find_tag_text(child, 'DateTime')
|
35
35
|
assert dt_text, 'missing DateTime field in xml for time series'
|
36
36
|
year_offset = rqet.find_tag_int(child, 'YearOffset')
|
37
|
-
if year_offset is not None:
|
38
|
-
assert self.timeframe == 'geologic'
|
37
|
+
if self.timeframe == 'geologic' and year_offset is not None:
|
39
38
|
if year_offset > 0:
|
40
39
|
log.warning(f'positive year offset in xml indicates future geological time: {year_offset}')
|
41
40
|
self.timestamps.append(year_offset) # todo: trim and check timestamp
|
42
|
-
|
43
|
-
|
41
|
+
elif self.timeframe == 'human' and not year_offset:
|
42
|
+
# year_offset can be 0 for "human" time frames, indicating None.
|
44
43
|
self.timestamps.append(dt_text) # todo: trim and check timestamp
|
44
|
+
else:
|
45
|
+
raise AssertionError(f'Invalid combination of timeframe {self.timeframe} and year_offset {year_offset}')
|
45
46
|
self.timestamps.sort()
|
46
47
|
|
47
48
|
def is_equivalent(self, other_ts):
|