resqpy 4.14.2__py3-none-any.whl → 5.1.6__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 +8 -2
- resqpy/grid/_grid.py +95 -12
- resqpy/grid/_grid_types.py +22 -7
- resqpy/grid/_points_functions.py +1 -1
- resqpy/grid/_regular_grid.py +6 -2
- resqpy/grid_surface/__init__.py +17 -38
- resqpy/grid_surface/_blocked_well_populate.py +5 -5
- resqpy/grid_surface/_find_faces.py +1413 -253
- resqpy/lines/_polyline.py +24 -33
- resqpy/model/_catalogue.py +9 -0
- resqpy/model/_forestry.py +18 -14
- resqpy/model/_hdf5.py +11 -3
- resqpy/model/_model.py +85 -10
- resqpy/model/_xml.py +38 -13
- resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
- resqpy/olio/read_nexus_fault.py +8 -2
- resqpy/olio/relperm.py +1 -1
- resqpy/olio/transmission.py +8 -8
- resqpy/olio/triangulation.py +36 -30
- resqpy/olio/vector_utilities.py +340 -6
- resqpy/olio/volume.py +0 -20
- resqpy/olio/wellspec_keywords.py +19 -13
- resqpy/olio/write_hdf5.py +1 -1
- resqpy/olio/xml_et.py +12 -0
- resqpy/property/__init__.py +6 -4
- resqpy/property/_collection_add_part.py +4 -3
- resqpy/property/_collection_create_xml.py +4 -2
- resqpy/property/_collection_get_attributes.py +4 -0
- resqpy/property/attribute_property_set.py +311 -0
- resqpy/property/grid_property_collection.py +11 -11
- resqpy/property/property_collection.py +79 -31
- resqpy/property/property_common.py +3 -8
- resqpy/rq_import/_add_surfaces.py +34 -14
- resqpy/rq_import/_grid_from_cp.py +2 -2
- resqpy/rq_import/_import_nexus.py +75 -48
- resqpy/rq_import/_import_vdb_all_grids.py +64 -52
- resqpy/rq_import/_import_vdb_ensemble.py +12 -13
- resqpy/surface/_mesh.py +4 -0
- resqpy/surface/_surface.py +593 -118
- resqpy/surface/_tri_mesh.py +13 -10
- resqpy/surface/_tri_mesh_stencil.py +4 -4
- resqpy/surface/_triangulated_patch.py +71 -51
- resqpy/time_series/_any_time_series.py +7 -4
- resqpy/time_series/_geologic_time_series.py +1 -1
- resqpy/unstructured/_hexa_grid.py +6 -2
- resqpy/unstructured/_prism_grid.py +13 -5
- resqpy/unstructured/_pyramid_grid.py +6 -2
- resqpy/unstructured/_tetra_grid.py +6 -2
- resqpy/unstructured/_unstructured_grid.py +6 -2
- resqpy/well/_blocked_well.py +1986 -1946
- resqpy/well/_deviation_survey.py +3 -3
- resqpy/well/_md_datum.py +11 -21
- resqpy/well/_trajectory.py +10 -5
- resqpy/well/_wellbore_frame.py +10 -2
- resqpy/well/blocked_well_frame.py +3 -3
- resqpy/well/well_object_funcs.py +7 -9
- resqpy/well/well_utils.py +33 -0
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/METADATA +8 -9
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/RECORD +66 -66
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/WHEEL +1 -1
- resqpy/grid/_moved_functions.py +0 -15
- {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/LICENSE +0 -0
resqpy/well/_blocked_well.py
CHANGED
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
|
|
10
10
|
import math as maths
|
11
11
|
import numpy as np
|
12
12
|
import pandas as pd
|
13
|
+
import warnings
|
13
14
|
from functools import partial
|
14
15
|
|
15
16
|
import resqpy.crs as crs
|
@@ -136,12 +137,13 @@ class BlockedWell(BaseResqpy):
|
|
136
137
|
self.wellbore_interpretation = None #: associated wellbore interpretation object
|
137
138
|
self.wellbore_feature = None #: associated wellbore feature object
|
138
139
|
self.well_name = None #: name of well to import from ascii file formats
|
140
|
+
self.cell_index_dtype = np.int32 #: set to int64 if any grid has more than 2^31 - 1 cells, otherwise int32
|
139
141
|
|
140
142
|
self.cell_interval_map = None # maps from cell index to interval (ie. node) index; populated on demand
|
141
143
|
|
142
144
|
self.property_collection = None
|
143
145
|
#: all logs associated with the blockedwellbore; an instance of :class:`resqpy.property.WellIntervalPropertyCollection`
|
144
|
-
self.
|
146
|
+
self._logs = None # use of .logs is deprecated
|
145
147
|
self.cellind_null = -1
|
146
148
|
self.gridind_null = -1
|
147
149
|
self.facepair_null = -1
|
@@ -150,11 +152,11 @@ class BlockedWell(BaseResqpy):
|
|
150
152
|
# this is the default as indicated on page 139 (but not p. 180) of the RESQML Usage Gude v2.0.1
|
151
153
|
# also assumes K is generally increasing downwards
|
152
154
|
# see DevOps backlog item 269001 discussion for more information
|
153
|
-
# self.face_index_map = np.array([[0, 1], [4, 2], [5, 3]], dtype =
|
154
|
-
self.face_index_map = np.array([[0, 1], [2, 4], [5, 3]], dtype =
|
155
|
+
# self.face_index_map = np.array([[0, 1], [4, 2], [5, 3]], dtype = np.int8)
|
156
|
+
self.face_index_map = np.array([[0, 1], [2, 4], [5, 3]], dtype = np.int8) # order: top, base, J-, I+, J+, I-
|
155
157
|
# and the inverse, maps from 0..5 to (axis, p01)
|
156
|
-
# self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 1], [2, 1], [1, 0], [2, 0]], dtype =
|
157
|
-
self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 0], [2, 1], [1, 1], [2, 0]], dtype =
|
158
|
+
# self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 1], [2, 1], [1, 0], [2, 0]], dtype = np.int8)
|
159
|
+
self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 0], [2, 1], [1, 1], [2, 0]], dtype = np.int8)
|
158
160
|
# note: the rework_face_pairs() method, below, overwrites the face indices based on I, J cell indices
|
159
161
|
|
160
162
|
super().__init__(model = parent_model,
|
@@ -190,160 +192,17 @@ class BlockedWell(BaseResqpy):
|
|
190
192
|
self.title = well_name
|
191
193
|
# else an empty object is returned
|
192
194
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
grid_final = grid
|
201
|
-
return grid_final
|
202
|
-
|
203
|
-
def __check_cellio_init_okay(self, cellio_file, well_name, grid):
|
204
|
-
"""Checks if BlockedWell object initialization from a cellio file is okay."""
|
205
|
-
|
206
|
-
okay = self.import_from_rms_cellio(cellio_file, well_name, grid)
|
207
|
-
if not okay:
|
208
|
-
self.node_count = 0
|
209
|
-
|
210
|
-
def _load_from_xml(self):
|
211
|
-
"""Loads the blocked wellbore object from an xml node (and associated hdf5 data)."""
|
212
|
-
|
213
|
-
node = self.root
|
214
|
-
assert node is not None
|
215
|
-
|
216
|
-
self.__find_trajectory_uuid(node = node)
|
217
|
-
|
218
|
-
self.node_count = rqet.find_tag_int(node, 'NodeCount')
|
219
|
-
assert self.node_count is not None and self.node_count >= 2, 'problem with blocked well node count'
|
220
|
-
|
221
|
-
mds_node = rqet.find_tag(node, 'NodeMd')
|
222
|
-
assert mds_node is not None, 'blocked well node measured depths hdf5 reference not found in xml'
|
223
|
-
rqwu.load_hdf5_array(self, mds_node, 'node_mds')
|
224
|
-
|
225
|
-
# Statement below has no effect, is this a bug?
|
226
|
-
self.node_mds is not None and self.node_mds.ndim == 1 and self.node_mds.size == self.node_count
|
227
|
-
|
228
|
-
self.cell_count = rqet.find_tag_int(node, 'CellCount')
|
229
|
-
assert self.cell_count is not None and self.cell_count > 0
|
230
|
-
|
231
|
-
# todo: remove this if block once RMS export issue resolved
|
232
|
-
if self.cell_count == self.node_count:
|
233
|
-
extended_mds = np.empty((self.node_mds.size + 1,))
|
234
|
-
extended_mds[:-1] = self.node_mds
|
235
|
-
extended_mds[-1] = self.node_mds[-1] + 1.0
|
236
|
-
self.node_mds = extended_mds
|
237
|
-
self.node_count += 1
|
238
|
-
|
239
|
-
assert self.cell_count < self.node_count
|
240
|
-
|
241
|
-
self.__find_ci_node_and_load_hdf5_array(node = node)
|
242
|
-
|
243
|
-
self.__find_fi_node_and_load_hdf5_array(node)
|
244
|
-
|
245
|
-
unique_grid_indices = self.__find_gi_node_and_load_hdf5_array(node = node)
|
246
|
-
|
247
|
-
self.__find_grid_node(node = node, unique_grid_indices = unique_grid_indices)
|
248
|
-
|
249
|
-
interp_uuid = rqet.find_nested_tags_text(node, ['RepresentedInterpretation', 'UUID'])
|
250
|
-
if interp_uuid is None:
|
251
|
-
self.wellbore_interpretation = None
|
252
|
-
else:
|
253
|
-
self.wellbore_interpretation = rqo.WellboreInterpretation(self.model, uuid = interp_uuid)
|
254
|
-
|
255
|
-
# Create blocked well log collection of all log data
|
256
|
-
self.logs = rqp.WellIntervalPropertyCollection(frame = self)
|
257
|
-
|
258
|
-
# Set up matches between cell_indices and grid_indices
|
259
|
-
self.cell_grid_link = self.map_cell_and_grid_indices()
|
260
|
-
|
261
|
-
def __find_trajectory_uuid(self, node):
|
262
|
-
"""Find and verify the uuid of the trajectory associated with the BlockedWell object."""
|
263
|
-
|
264
|
-
trajectory_uuid = bu.uuid_from_string(rqet.find_nested_tags_text(node, ['Trajectory', 'UUID']))
|
265
|
-
assert trajectory_uuid is not None, 'blocked well trajectory reference not found in xml'
|
266
|
-
if self.trajectory is None:
|
267
|
-
self.trajectory = rqw.Trajectory(self.model, uuid = trajectory_uuid)
|
268
|
-
else:
|
269
|
-
assert bu.matching_uuids(self.trajectory.uuid, trajectory_uuid), 'blocked well trajectory uuid mismatch'
|
270
|
-
|
271
|
-
def __find_ci_node_and_load_hdf5_array(self, node):
|
272
|
-
"""Find the BlockedWell object's cell indices hdf5 reference node and load the array."""
|
273
|
-
|
274
|
-
ci_node = rqet.find_tag(node, 'CellIndices')
|
275
|
-
assert ci_node is not None, 'blocked well cell indices hdf5 reference not found in xml'
|
276
|
-
rqwu.load_hdf5_array(self, ci_node, 'cell_indices', dtype = int)
|
277
|
-
assert (self.cell_indices is not None and self.cell_indices.ndim == 1 and
|
278
|
-
self.cell_indices.size == self.cell_count), 'mismatch in number of cell indices for blocked well'
|
279
|
-
self.cellind_null = rqet.find_tag_int(ci_node, 'NullValue')
|
280
|
-
if self.cellind_null is None:
|
281
|
-
self.cellind_null = -1 # if no Null found assume -1 default
|
282
|
-
|
283
|
-
def __find_fi_node_and_load_hdf5_array(self, node):
|
284
|
-
"""Find the BlockedWell object's face indices hdf5 reference node and load the array."""
|
285
|
-
|
286
|
-
fi_node = rqet.find_tag(node, 'LocalFacePairPerCellIndices')
|
287
|
-
assert fi_node is not None, 'blocked well face indices hdf5 reference not found in xml'
|
288
|
-
rqwu.load_hdf5_array(self, fi_node, 'raw_face_indices', dtype = 'int')
|
289
|
-
assert self.raw_face_indices is not None, 'failed to load face indices for blocked well'
|
290
|
-
assert self.raw_face_indices.size == 2 * self.cell_count, 'mismatch in number of cell faces for blocked well'
|
291
|
-
if self.raw_face_indices.ndim > 1:
|
292
|
-
self.raw_face_indices = self.raw_face_indices.reshape((self.raw_face_indices.size,))
|
293
|
-
mask = np.where(self.raw_face_indices == -1)
|
294
|
-
self.raw_face_indices[mask] = 0
|
295
|
-
self.face_pair_indices = self.face_index_inverse_map[self.raw_face_indices]
|
296
|
-
self.face_pair_indices[mask] = (-1, -1)
|
297
|
-
self.face_pair_indices = self.face_pair_indices.reshape((-1, 2, 2))
|
298
|
-
del self.raw_face_indices
|
299
|
-
self.facepair_null = rqet.find_tag_int(fi_node, 'NullValue')
|
300
|
-
if self.facepair_null is None:
|
301
|
-
self.facepair_null = -1
|
302
|
-
|
303
|
-
def __find_gi_node_and_load_hdf5_array(self, node):
|
304
|
-
"""Find the BlockedWell object's grid indices hdf5 reference node and load the array."""
|
305
|
-
|
306
|
-
gi_node = rqet.find_tag(node, 'GridIndices')
|
307
|
-
assert gi_node is not None, 'blocked well grid indices hdf5 reference not found in xml'
|
308
|
-
rqwu.load_hdf5_array(self, gi_node, 'grid_indices', dtype = 'int')
|
309
|
-
# assert self.grid_indices is not None and self.grid_indices.ndim == 1 and self.grid_indices.size == self.node_count - 1
|
310
|
-
# temporary code to handle blocked wells with incorrectly shaped grid indices wrt. nodes
|
311
|
-
assert self.grid_indices is not None and self.grid_indices.ndim == 1
|
312
|
-
if self.grid_indices.size != self.node_count - 1:
|
313
|
-
if self.grid_indices.size == self.cell_count and self.node_count == 2 * self.cell_count:
|
314
|
-
log.warning(f'handling node duplication or missing unblocked intervals in blocked well: {self.title}')
|
315
|
-
|
316
|
-
expanded_grid_indices = np.full(self.node_count - 1, -1, dtype = int)
|
317
|
-
expanded_grid_indices[::2] = self.grid_indices
|
318
|
-
self.grid_indices = expanded_grid_indices
|
319
|
-
else:
|
320
|
-
raise ValueError(
|
321
|
-
f'incorrect grid indices size with respect to node count in blocked well: {self.title}')
|
322
|
-
# end of temporary code
|
323
|
-
unique_grid_indices = np.unique(self.grid_indices) # sorted list of unique values
|
324
|
-
self.gridind_null = rqet.find_tag_int(gi_node, 'NullValue')
|
325
|
-
if self.gridind_null is None:
|
326
|
-
self.gridind_null = -1 # if no Null found assume -1 default
|
327
|
-
return unique_grid_indices
|
328
|
-
|
329
|
-
def __find_grid_node(self, node, unique_grid_indices):
|
330
|
-
"""Find the BlockedWell object's grid reference node(s)."""
|
331
|
-
grid_node_list = rqet.list_of_tag(node, 'Grid')
|
332
|
-
assert len(grid_node_list) > 0, 'blocked well grid reference(s) not found in xml'
|
333
|
-
assert unique_grid_indices[0] >= -1 and unique_grid_indices[-1] < len(
|
334
|
-
grid_node_list), 'blocked well grid index out of range'
|
335
|
-
assert np.count_nonzero(
|
336
|
-
self.grid_indices >= 0) == self.cell_count, 'mismatch in number of blocked well intervals'
|
337
|
-
self.grid_list = []
|
338
|
-
for grid_ref_node in grid_node_list:
|
339
|
-
grid_node = self.model.referenced_node(grid_ref_node)
|
340
|
-
assert grid_node is not None, 'grid referenced in blocked well xml is not present in model'
|
341
|
-
grid_uuid = rqet.uuid_for_part_root(grid_node)
|
342
|
-
grid_obj = self.model.grid(uuid = grid_uuid, find_properties = False)
|
343
|
-
self.grid_list.append(grid_obj)
|
195
|
+
@property
|
196
|
+
def logs(self):
|
197
|
+
"""Returns blocked well property collection of all well log properties."""
|
198
|
+
warnings.warn('DEPRECATED: use of BlockedWell.logs attribute is deprecated, use property collection instead')
|
199
|
+
if self._logs is None:
|
200
|
+
self._logs = rqp.WellIntervalPropertyCollection(frame = self)
|
201
|
+
return self._logs
|
344
202
|
|
345
203
|
def extract_property_collection(self, refresh = False):
|
346
204
|
"""Returns a property collection for the blocked well."""
|
205
|
+
|
347
206
|
if self.property_collection is None or refresh:
|
348
207
|
self.property_collection = rqp.PropertyCollection(support = self)
|
349
208
|
return self.property_collection
|
@@ -407,6 +266,7 @@ class BlockedWell(BaseResqpy):
|
|
407
266
|
|
408
267
|
def interval_for_cell(self, cell_index):
|
409
268
|
"""Returns the interval index for a given cell index (identical if there are no unblocked intervals)."""
|
269
|
+
|
410
270
|
assert 0 <= cell_index < self.cell_count
|
411
271
|
if self.node_count == self.cell_count + 1:
|
412
272
|
return cell_index
|
@@ -424,20 +284,10 @@ class BlockedWell(BaseResqpy):
|
|
424
284
|
(float, float) being the entry and exit measured depths for the cell, along the trajectory;
|
425
285
|
uom is held in trajectory object
|
426
286
|
"""
|
287
|
+
|
427
288
|
interval = self.interval_for_cell(cell_index)
|
428
289
|
return (self.node_mds[interval], self.node_mds[interval + 1])
|
429
290
|
|
430
|
-
def _set_cell_interval_map(self):
|
431
|
-
"""Sets up an index mapping from blocked cell index to interval index, accounting for unblocked intervals."""
|
432
|
-
self.cell_interval_map = np.zeros(self.cell_count, dtype = int)
|
433
|
-
ci = 0
|
434
|
-
for ii in range(self.node_count - 1):
|
435
|
-
if self.grid_indices[ii] < 0:
|
436
|
-
continue
|
437
|
-
self.cell_interval_map[ci] = ii
|
438
|
-
ci += 1
|
439
|
-
assert ci == self.cell_count
|
440
|
-
|
441
291
|
def cell_indices_kji0(self):
|
442
292
|
"""Returns a numpy int array of shape (N, 3) of cells visited by well, for a single grid situation.
|
443
293
|
|
@@ -456,7 +306,7 @@ class BlockedWell(BaseResqpy):
|
|
456
306
|
grid_for_cell_list = []
|
457
307
|
grid_indices = self.compressed_grid_indices()
|
458
308
|
assert len(grid_indices) == self.cell_count
|
459
|
-
cell_indices = np.empty((self.cell_count, 3), dtype =
|
309
|
+
cell_indices = np.empty((self.cell_count, 3), dtype = np.int32)
|
460
310
|
for cell_number in range(self.cell_count):
|
461
311
|
grid = self.grid_list[grid_indices[cell_number]]
|
462
312
|
grid_for_cell_list.append(grid)
|
@@ -488,7 +338,7 @@ class BlockedWell(BaseResqpy):
|
|
488
338
|
|
489
339
|
if cells_kji0 is None or len(cells_kji0) == 0:
|
490
340
|
return None
|
491
|
-
well_box = np.empty((2, 3), dtype =
|
341
|
+
well_box = np.empty((2, 3), dtype = np.int32)
|
492
342
|
well_box[0] = np.min(cells_kji0, axis = 0)
|
493
343
|
well_box[1] = np.max(cells_kji0, axis = 0)
|
494
344
|
return well_box
|
@@ -716,16 +566,6 @@ class BlockedWell(BaseResqpy):
|
|
716
566
|
|
717
567
|
return self
|
718
568
|
|
719
|
-
def __derive_from_wellspec_check_well_name(self, well_name):
|
720
|
-
"""Set the well name to be used in the wellspec file."""
|
721
|
-
if well_name:
|
722
|
-
self.well_name = well_name
|
723
|
-
else:
|
724
|
-
well_name = self.well_name
|
725
|
-
if not self.title:
|
726
|
-
self.title = well_name
|
727
|
-
return well_name
|
728
|
-
|
729
569
|
def derive_from_cell_list(self, cell_kji0_list, well_name, grid, length_uom = None):
|
730
570
|
"""Populate empty blocked well from numpy int array of shape (N, 3) being list of cells."""
|
731
571
|
|
@@ -847,9 +687,9 @@ class BlockedWell(BaseResqpy):
|
|
847
687
|
self.node_count = len(trajectory_mds)
|
848
688
|
self.node_mds = np.array(trajectory_mds)
|
849
689
|
self.cell_count = len(blocked_cells_kji0)
|
850
|
-
self.grid_indices = np.array(blocked_intervals, dtype =
|
851
|
-
self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0))
|
852
|
-
self.face_pair_indices = np.array(blocked_face_pairs, dtype =
|
690
|
+
self.grid_indices = np.array(blocked_intervals, dtype = np.int32) # NB. only supporting one grid at the moment
|
691
|
+
self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0)).astype(self.cell_index_dtype)
|
692
|
+
self.face_pair_indices = np.array(blocked_face_pairs, dtype = np.int8)
|
853
693
|
self.grid_list = [grid]
|
854
694
|
|
855
695
|
trajectory_points, trajectory_mds = BlockedWell.__add_tail_to_trajectory_if_necessary(
|
@@ -866,258 +706,38 @@ class BlockedWell(BaseResqpy):
|
|
866
706
|
|
867
707
|
return self
|
868
708
|
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
cell_kji0[:] -= 1
|
877
|
-
return cell_kji0
|
878
|
-
|
879
|
-
@staticmethod
|
880
|
-
def __verify_grid_name(grid_name_to_check, row, skipped_warning_grid, well_name):
|
881
|
-
"""Check whether the grid associated with a row of the dataframe matches the expected grid name."""
|
882
|
-
skip_row = False
|
883
|
-
if grid_name_to_check and pd.notna(row['GRID']) and grid_name_to_check != str(row['GRID']).upper():
|
884
|
-
other_grid = str(row['GRID'])
|
885
|
-
if skipped_warning_grid != other_grid:
|
886
|
-
log.warning('skipping perforation(s) in grid ' + other_grid + ' for well ' + str(well_name))
|
887
|
-
skipped_warning_grid = other_grid
|
888
|
-
skip_row = True
|
889
|
-
return skipped_warning_grid, skip_row
|
890
|
-
|
891
|
-
@staticmethod
|
892
|
-
def __calculate_entry_and_exit_axes_polarities_and_points(angles_present, row, cp, well_name, df, i, cell_kji0,
|
893
|
-
blocked_cells_kji0, use_face_centres, xy_units, z_units):
|
894
|
-
if angles_present:
|
895
|
-
entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz = \
|
896
|
-
BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_angles(
|
897
|
-
row = row, cp = cp, well_name = well_name, xy_units = xy_units, z_units = z_units)
|
898
|
-
else:
|
899
|
-
# fabricate entry and exit axes and polarities based on indices alone
|
900
|
-
# note: could use geometry but here a cheap rough-and-ready approach is used
|
901
|
-
log.debug('row ' + str(i) + ': using cell moves')
|
902
|
-
entry_axis, entry_polarity, exit_axis, exit_polarity = BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_indices(
|
903
|
-
df = df, i = i, cell_kji0 = cell_kji0, blocked_cells_kji0 = blocked_cells_kji0)
|
904
|
-
|
905
|
-
entry_xyz, exit_xyz = BlockedWell.__override_vector_based_xyz_entry_and_exit_points_if_necessary(
|
906
|
-
use_face_centres = use_face_centres,
|
907
|
-
entry_axis = entry_axis,
|
908
|
-
exit_axis = exit_axis,
|
909
|
-
entry_polarity = entry_polarity,
|
910
|
-
exit_polarity = exit_polarity,
|
911
|
-
cp = cp)
|
709
|
+
def import_from_rms_cellio(self,
|
710
|
+
cellio_file,
|
711
|
+
well_name,
|
712
|
+
grid,
|
713
|
+
include_overburden_unblocked_interval = False,
|
714
|
+
set_tangent_vectors = False):
|
715
|
+
"""Populates empty blocked well from RMS cell I/O data; creates simulation trajectory and md datum.
|
912
716
|
|
913
|
-
|
717
|
+
arguments:
|
718
|
+
cellio_file (string): path of RMS ascii export file holding blocked well cell I/O data; cell entry and
|
719
|
+
exit points are expected
|
720
|
+
well_name (string): the name of the well as used in the cell I/O file
|
721
|
+
grid (grid.Grid object): the grid object which the cell indices in the cell I/O data relate to
|
722
|
+
set_tangent_vectors (boolean, default False): if True, tangent vectors will be computed from the well
|
723
|
+
trajectory's control points
|
914
724
|
|
915
|
-
|
916
|
-
|
917
|
-
"""
|
725
|
+
returns:
|
726
|
+
self if successful; None otherwise
|
727
|
+
"""
|
918
728
|
|
919
|
-
|
920
|
-
|
921
|
-
if inclination < 0.001:
|
922
|
-
azimuth = 0.0
|
729
|
+
if well_name:
|
730
|
+
self.well_name = well_name
|
923
731
|
else:
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
if xy_units != z_units:
|
928
|
-
well_vector[2] = wam.convert_lengths(well_vector[2], xy_units, z_units)
|
929
|
-
well_vector = vec.unit_vector(well_vector) * 10000.0
|
930
|
-
# todo: the following might be producing NaN's when vector passes precisely through an edge
|
931
|
-
(entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz) = \
|
932
|
-
rqwu.find_entry_and_exit(cp, -well_vector, well_vector, well_name)
|
933
|
-
return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
|
732
|
+
well_name = self.well_name
|
733
|
+
if not self.title:
|
734
|
+
self.title = well_name
|
934
735
|
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
i, cell_kji0, entry_axis, entry_polarity, df)
|
941
|
-
|
942
|
-
return entry_axis, entry_polarity, exit_axis, exit_polarity
|
943
|
-
|
944
|
-
@staticmethod
|
945
|
-
def __fabricate_entry_axis_and_polarity_using_indices(i, cell_kji0, blocked_cells_kji0):
|
946
|
-
"""Fabricate entry and exit axes and polarities based on indices alone.
|
947
|
-
|
948
|
-
note:
|
949
|
-
could use geometry but here a cheap rough-and-ready approach is used
|
950
|
-
"""
|
951
|
-
|
952
|
-
if i == 0:
|
953
|
-
entry_axis, entry_polarity = 0, 0 # K-
|
954
|
-
else:
|
955
|
-
entry_move = cell_kji0 - blocked_cells_kji0[-1]
|
956
|
-
log.debug(f'entry move: {entry_move}')
|
957
|
-
if entry_move[1] == 0 and entry_move[2] == 0: # K move
|
958
|
-
entry_axis = 0
|
959
|
-
entry_polarity = 0 if entry_move[0] >= 0 else 1
|
960
|
-
elif abs(entry_move[1]) > abs(entry_move[2]): # J dominant move
|
961
|
-
entry_axis = 1
|
962
|
-
entry_polarity = 0 if entry_move[1] >= 0 else 1
|
963
|
-
else: # I dominant move
|
964
|
-
entry_axis = 2
|
965
|
-
entry_polarity = 0 if entry_move[2] >= 0 else 1
|
966
|
-
return entry_axis, entry_polarity
|
967
|
-
|
968
|
-
@staticmethod
|
969
|
-
def __fabricate_exit_axis_and_polarity_using_indices(i, cell_kji0, entry_axis, entry_polarity, df):
|
970
|
-
if i == len(df) - 1:
|
971
|
-
exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
|
972
|
-
else:
|
973
|
-
next_cell_kji0 = BlockedWell.__cell_kji0_from_df(df, i + 1)
|
974
|
-
if next_cell_kji0 is None:
|
975
|
-
exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
|
976
|
-
else:
|
977
|
-
exit_move = next_cell_kji0 - cell_kji0
|
978
|
-
log.debug(f'exit move: {exit_move}')
|
979
|
-
if exit_move[1] == 0 and exit_move[2] == 0: # K move
|
980
|
-
exit_axis = 0
|
981
|
-
exit_polarity = 1 if exit_move[0] >= 0 else 0
|
982
|
-
elif abs(exit_move[1]) > abs(exit_move[2]): # J dominant move
|
983
|
-
exit_axis = 1
|
984
|
-
exit_polarity = 1 if exit_move[1] >= 0 else 0
|
985
|
-
else: # I dominant move
|
986
|
-
exit_axis = 2
|
987
|
-
exit_polarity = 1 if exit_move[2] >= 0 else 0
|
988
|
-
return exit_axis, exit_polarity
|
989
|
-
|
990
|
-
@staticmethod
|
991
|
-
def __override_vector_based_xyz_entry_and_exit_points_if_necessary(use_face_centres, entry_axis, exit_axis,
|
992
|
-
entry_polarity, exit_polarity, cp):
|
993
|
-
"""Override the vector based xyz entry and exit with face centres."""
|
994
|
-
|
995
|
-
if use_face_centres: # override the vector based xyz entry and exit points with face centres
|
996
|
-
if entry_axis == 0:
|
997
|
-
entry_xyz = np.mean(cp[entry_polarity, :, :], axis = (0, 1))
|
998
|
-
elif entry_axis == 1:
|
999
|
-
entry_xyz = np.mean(cp[:, entry_polarity, :], axis = (0, 1))
|
1000
|
-
else:
|
1001
|
-
entry_xyz = np.mean(cp[:, :, entry_polarity], axis = (0, 1)) # entry_axis == 2, ie. I
|
1002
|
-
if exit_axis == 0:
|
1003
|
-
exit_xyz = np.mean(cp[exit_polarity, :, :], axis = (0, 1))
|
1004
|
-
elif exit_axis == 1:
|
1005
|
-
exit_xyz = np.mean(cp[:, exit_polarity, :], axis = (0, 1))
|
1006
|
-
else:
|
1007
|
-
exit_xyz = np.mean(cp[:, :, exit_polarity], axis = (0, 1)) # exit_axis == 2, ie. I
|
1008
|
-
return entry_xyz, exit_xyz
|
1009
|
-
|
1010
|
-
@staticmethod
|
1011
|
-
def __add_interval(previous_xyz, entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz,
|
1012
|
-
cell_kji0, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0,
|
1013
|
-
blocked_face_pairs, xy_units, z_units, length_uom):
|
1014
|
-
if previous_xyz is None: # first entry
|
1015
|
-
log.debug('adding mean sea level trajectory start')
|
1016
|
-
previous_xyz = entry_xyz.copy()
|
1017
|
-
previous_xyz[2] = 0.0 # use depth zero as md datum
|
1018
|
-
trajectory_mds.append(0.0)
|
1019
|
-
trajectory_points.append(previous_xyz)
|
1020
|
-
if not vec.isclose(previous_xyz, entry_xyz, tolerance = 0.05): # add an unblocked interval
|
1021
|
-
log.debug('adding unblocked interval')
|
1022
|
-
trajectory_points.append(entry_xyz)
|
1023
|
-
new_md = trajectory_mds[-1] + BlockedWell._md_length(entry_xyz - previous_xyz, xy_units, z_units,
|
1024
|
-
length_uom)
|
1025
|
-
trajectory_mds.append(new_md)
|
1026
|
-
blocked_intervals.append(-1) # unblocked interval
|
1027
|
-
previous_xyz = entry_xyz
|
1028
|
-
log.debug('adding blocked interval for cell kji0: ' + str(cell_kji0))
|
1029
|
-
trajectory_points.append(exit_xyz)
|
1030
|
-
new_md = trajectory_mds[-1] + BlockedWell._md_length(exit_xyz - previous_xyz, xy_units, z_units, length_uom)
|
1031
|
-
trajectory_mds.append(new_md)
|
1032
|
-
blocked_intervals.append(0) # blocked interval
|
1033
|
-
previous_xyz = exit_xyz
|
1034
|
-
blocked_cells_kji0.append(cell_kji0)
|
1035
|
-
blocked_face_pairs.append(((entry_axis, entry_polarity), (exit_axis, exit_polarity)))
|
1036
|
-
|
1037
|
-
return previous_xyz, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0, blocked_face_pairs
|
1038
|
-
|
1039
|
-
@staticmethod
|
1040
|
-
def _md_length(xyz_vector, xy_units, z_units, length_uom):
|
1041
|
-
if length_uom == xy_units and length_uom == z_units:
|
1042
|
-
return vec.naive_length(xyz_vector)
|
1043
|
-
x = wam.convert_lengths(xyz_vector[0], xy_units, length_uom)
|
1044
|
-
y = wam.convert_lengths(xyz_vector[1], xy_units, length_uom)
|
1045
|
-
z = wam.convert_lengths(xyz_vector[2], z_units, length_uom)
|
1046
|
-
return vec.naive_length((x, y, z))
|
1047
|
-
|
1048
|
-
@staticmethod
|
1049
|
-
def __add_tail_to_trajectory_if_necessary(blocked_count, exit_axis, exit_polarity, cell_kji0, grid,
|
1050
|
-
trajectory_points, trajectory_mds):
|
1051
|
-
"""Add tail to trajcetory if last segment terminates at bottom face in bottom layer."""
|
1052
|
-
|
1053
|
-
if blocked_count > 0 and exit_axis == 0 and exit_polarity == 1 and cell_kji0[
|
1054
|
-
0] == grid.nk - 1 and grid.k_direction_is_down:
|
1055
|
-
tail_length = 10.0 # metres or feet
|
1056
|
-
tail_xyz = trajectory_points[-1].copy()
|
1057
|
-
tail_xyz[2] += tail_length * (1.0 if grid.z_inc_down() else -1.0)
|
1058
|
-
trajectory_points.append(tail_xyz)
|
1059
|
-
new_md = trajectory_mds[-1] + tail_length
|
1060
|
-
trajectory_mds.append(new_md)
|
1061
|
-
|
1062
|
-
return trajectory_points, trajectory_mds
|
1063
|
-
|
1064
|
-
def __add_as_properties_if_specified(self,
|
1065
|
-
add_as_properties,
|
1066
|
-
df,
|
1067
|
-
length_uom,
|
1068
|
-
time_index = None,
|
1069
|
-
time_series_uuid = None):
|
1070
|
-
# if add_as_properties is True and present as a list of wellspec column names, both the blocked well and
|
1071
|
-
# the properties will have their hdf5 data written, xml created and be added as parts to the model
|
1072
|
-
|
1073
|
-
if add_as_properties and len(df.columns) > 3:
|
1074
|
-
# NB: atypical writing of hdf5 data and xml creation in order to support related properties
|
1075
|
-
self.write_hdf5()
|
1076
|
-
self.create_xml()
|
1077
|
-
if isinstance(add_as_properties, list):
|
1078
|
-
for col in add_as_properties:
|
1079
|
-
assert col in df.columns[3:] # could just skip missing columns
|
1080
|
-
property_columns = add_as_properties
|
1081
|
-
else:
|
1082
|
-
property_columns = df.columns[3:]
|
1083
|
-
self.add_df_properties(df,
|
1084
|
-
property_columns,
|
1085
|
-
length_uom = length_uom,
|
1086
|
-
time_index = time_index,
|
1087
|
-
time_series_uuid = time_series_uuid)
|
1088
|
-
|
1089
|
-
def import_from_rms_cellio(self,
|
1090
|
-
cellio_file,
|
1091
|
-
well_name,
|
1092
|
-
grid,
|
1093
|
-
include_overburden_unblocked_interval = False,
|
1094
|
-
set_tangent_vectors = False):
|
1095
|
-
"""Populates empty blocked well from RMS cell I/O data; creates simulation trajectory and md datum.
|
1096
|
-
|
1097
|
-
arguments:
|
1098
|
-
cellio_file (string): path of RMS ascii export file holding blocked well cell I/O data; cell entry and
|
1099
|
-
exit points are expected
|
1100
|
-
well_name (string): the name of the well as used in the cell I/O file
|
1101
|
-
grid (grid.Grid object): the grid object which the cell indices in the cell I/O data relate to
|
1102
|
-
set_tangent_vectors (boolean, default False): if True, tangent vectors will be computed from the well
|
1103
|
-
trajectory's control points
|
1104
|
-
|
1105
|
-
returns:
|
1106
|
-
self if successful; None otherwise
|
1107
|
-
"""
|
1108
|
-
|
1109
|
-
if well_name:
|
1110
|
-
self.well_name = well_name
|
1111
|
-
else:
|
1112
|
-
well_name = self.well_name
|
1113
|
-
if not self.title:
|
1114
|
-
self.title = well_name
|
1115
|
-
|
1116
|
-
grid_name = rqet.citation_title_for_node(grid.root)
|
1117
|
-
length_uom = grid.z_units()
|
1118
|
-
grid_z_inc_down = crs.Crs(grid.model, uuid = grid.crs_uuid).z_inc_down
|
1119
|
-
log.debug('grid z increasing downwards: ' + str(grid_z_inc_down) + '(type: ' + str(type(grid_z_inc_down)) + ')')
|
1120
|
-
cellio_z_inc_down = None
|
736
|
+
grid_name = rqet.citation_title_for_node(grid.root)
|
737
|
+
length_uom = grid.z_units()
|
738
|
+
grid_z_inc_down = crs.Crs(grid.model, uuid = grid.crs_uuid).z_inc_down
|
739
|
+
log.debug('grid z increasing downwards: ' + str(grid_z_inc_down) + '(type: ' + str(type(grid_z_inc_down)) + ')')
|
740
|
+
cellio_z_inc_down = None
|
1121
741
|
|
1122
742
|
try:
|
1123
743
|
assert ' ' not in well_name, 'cannot import for well name containing spaces'
|
@@ -1195,9 +815,10 @@ class BlockedWell(BaseResqpy):
|
|
1195
815
|
self.node_count = len(trajectory_mds)
|
1196
816
|
self.node_mds = np.array(trajectory_mds)
|
1197
817
|
self.cell_count = len(blocked_cells_kji0)
|
1198
|
-
|
1199
|
-
|
1200
|
-
self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0))
|
818
|
+
# NB. only supporting one grid at the moment
|
819
|
+
self.grid_indices = np.array(blocked_intervals, dtype = np.int32)
|
820
|
+
self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0)).astype(
|
821
|
+
self.cell_index_dtype)
|
1201
822
|
self.face_pair_indices = np.array(blocked_face_pairs)
|
1202
823
|
self.grid_list = [grid]
|
1203
824
|
|
@@ -1208,70 +829,6 @@ class BlockedWell(BaseResqpy):
|
|
1208
829
|
|
1209
830
|
return self
|
1210
831
|
|
1211
|
-
@staticmethod
|
1212
|
-
def __verify_header_lines_in_cellio_file(fp, well_name, cellio_file):
|
1213
|
-
"""Find and verify the information in the header lines for the specified well in the RMS cellio file."""
|
1214
|
-
while True:
|
1215
|
-
kf.skip_blank_lines_and_comments(fp)
|
1216
|
-
line = fp.readline() # file format version number?
|
1217
|
-
assert line, 'well ' + str(well_name) + ' not found in file ' + str(cellio_file)
|
1218
|
-
fp.readline() # 'Undefined'
|
1219
|
-
words = fp.readline().split()
|
1220
|
-
assert len(words), 'missing header info in cell I/O file'
|
1221
|
-
if words[0].upper() == well_name.upper():
|
1222
|
-
break
|
1223
|
-
while not kf.blank_line(fp):
|
1224
|
-
fp.readline() # skip to block of data for next well
|
1225
|
-
header_lines = int(fp.readline().strip())
|
1226
|
-
for _ in range(header_lines):
|
1227
|
-
fp.readline()
|
1228
|
-
|
1229
|
-
@staticmethod
|
1230
|
-
def __parse_non_blank_line_in_cellio_file(line, grid, cellio_z_inc_down, grid_z_inc_down):
|
1231
|
-
"""Parse each non-blank line in the RMS cellio file for the relevant parameters."""
|
1232
|
-
|
1233
|
-
words = line.split()
|
1234
|
-
assert len(words) >= 9, 'not enough items on data line in cell I/O file, minimum 9 expected'
|
1235
|
-
i1, j1, k1 = int(words[0]), int(words[1]), int(words[2])
|
1236
|
-
cell_kji0 = np.array((k1 - 1, j1 - 1, i1 - 1), dtype = int)
|
1237
|
-
assert np.all(0 <= cell_kji0) and np.all(
|
1238
|
-
cell_kji0 < grid.extent_kji), 'cell I/O cell index not within grid extent'
|
1239
|
-
entry_xyz = np.array((float(words[3]), float(words[4]), float(words[5])))
|
1240
|
-
exit_xyz = np.array((float(words[6]), float(words[7]), float(words[8])))
|
1241
|
-
if cellio_z_inc_down is None:
|
1242
|
-
cellio_z_inc_down = bool(entry_xyz[2] + exit_xyz[2] > 0.0)
|
1243
|
-
if cellio_z_inc_down != grid_z_inc_down:
|
1244
|
-
entry_xyz[2] = -entry_xyz[2]
|
1245
|
-
exit_xyz[2] = -exit_xyz[2]
|
1246
|
-
return cell_kji0, entry_xyz, exit_xyz
|
1247
|
-
|
1248
|
-
@staticmethod
|
1249
|
-
def __calculate_cell_cp_center_and_vectors(grid, cell_kji0, entry_xyz, exit_xyz, well_name):
|
1250
|
-
# calculate the i,j,k coordinates that represent the corner points and center of a perforation cell
|
1251
|
-
# calculate the entry and exit vectors for the perforation cell
|
1252
|
-
|
1253
|
-
cp = grid.corner_points(cell_kji0 = cell_kji0, cache_resqml_array = False)
|
1254
|
-
assert not np.any(np.isnan(
|
1255
|
-
cp)), 'missing geometry for perforation cell(kji0) ' + str(cell_kji0) + ' for well ' + str(well_name)
|
1256
|
-
cell_centre = np.mean(cp, axis = (0, 1, 2))
|
1257
|
-
# let's hope everything is in the same coordinate reference system!
|
1258
|
-
entry_vector = 100.0 * (entry_xyz - cell_centre)
|
1259
|
-
exit_vector = 100.0 * (exit_xyz - cell_centre)
|
1260
|
-
return cp, cell_centre, entry_vector, exit_vector
|
1261
|
-
|
1262
|
-
@staticmethod
|
1263
|
-
def __check_number_of_blocked_well_intervals(blocked_cells_kji0, well_name, grid_name):
|
1264
|
-
"""Check that at least one interval is blocked for the specified well."""
|
1265
|
-
|
1266
|
-
blocked_count = len(blocked_cells_kji0)
|
1267
|
-
if blocked_count == 0:
|
1268
|
-
log.warning(f"No intervals blocked for well {well_name} in grid"
|
1269
|
-
f"{f' {grid_name}' if grid_name is not None else ''}.")
|
1270
|
-
return None
|
1271
|
-
else:
|
1272
|
-
log.info(f"{blocked_count} interval{rqwu._pl(blocked_count)} blocked for well {well_name} in"
|
1273
|
-
f" grid{f' {grid_name}' if grid_name is not None else ''}.")
|
1274
|
-
|
1275
832
|
def dataframe(self,
|
1276
833
|
i_col = 'IW',
|
1277
834
|
j_col = 'JW',
|
@@ -1410,20 +967,21 @@ class BlockedWell(BaseResqpy):
|
|
1410
967
|
time_series_uuid (UUID, optional): the uuid of the time series for time dependent properties being added
|
1411
968
|
|
1412
969
|
notes:
|
1413
|
-
units of length along wellbore will be those of the trajectory's length_uom (also applies to K.H values) unless
|
1414
|
-
|
1415
|
-
the constraints are applied independently for each row and a row is excluded if it fails any constraint;
|
1416
|
-
the min_k0 and max_k0 arguments do not stop later rows within the layer range from being included;
|
1417
|
-
the min_length and min_kh limits apply to individual cell intervals and thus depend on cell size;
|
1418
|
-
the water and oil saturation limits are for saturations at a single time and affect whether the interval
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
add_as_properties
|
1426
|
-
|
970
|
+
- units of length along wellbore will be those of the trajectory's length_uom (also applies to K.H values) unless
|
971
|
+
the length_uom argument is used;
|
972
|
+
- the constraints are applied independently for each row and a row is excluded if it fails any constraint;
|
973
|
+
- the min_k0 and max_k0 arguments do not stop later rows within the layer range from being included;
|
974
|
+
- the min_length and min_kh limits apply to individual cell intervals and thus depend on cell size;
|
975
|
+
- the water and oil saturation limits are for saturations at a single time and affect whether the interval
|
976
|
+
is included in the dataframe
|
977
|
+
– to turn perforations off and on over time create a time series dependent bunch of boolean properties on
|
978
|
+
the blocked well, with title 'STAT' or local property kind 'well connection open';
|
979
|
+
- the saturation limits do not stop deeper intervals with qualifying saturations from being included;
|
980
|
+
- the k0_list, perforation_list and region_list arguments should be set to None to disable the
|
981
|
+
corresponding functionality, if set to an empty list, no rows will be included in the dataframe;
|
982
|
+
- if add_as_properties is True, the blocked well must already have been added as a part to the model;
|
983
|
+
- add_as_properties and use_properties cannot both be True;
|
984
|
+
- add_as_properties and use_properties are only currently functional for single grid blocked wells;
|
1427
985
|
|
1428
986
|
:meta common:
|
1429
987
|
"""
|
@@ -1503,7 +1061,7 @@ class BlockedWell(BaseResqpy):
|
|
1503
1061
|
for grid in self.grid_list:
|
1504
1062
|
grid.cache_all_geometry_arrays()
|
1505
1063
|
|
1506
|
-
k_face_check = np.zeros((2, 2), dtype =
|
1064
|
+
k_face_check = np.zeros((2, 2), dtype = np.int8)
|
1507
1065
|
k_face_check[1, 1] = 1 # now represents entry, exit of K-, K+
|
1508
1066
|
k_face_check_end = k_face_check.copy()
|
1509
1067
|
k_face_check_end[1] = -1 # entry through K-, terminating (TD) within cell
|
@@ -1637,41 +1195,46 @@ class BlockedWell(BaseResqpy):
|
|
1637
1195
|
if skip_interval_due_to_min_kh:
|
1638
1196
|
continue
|
1639
1197
|
|
1640
|
-
length, radw_i, skin_i, radb, wi, wbc
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
|
1648
|
-
|
1649
|
-
|
1198
|
+
length, radw_i, skin_i, radb, wi, wbc, stat_i = \
|
1199
|
+
BlockedWell.__get_pc_arrays_for_interval(pc = pc,
|
1200
|
+
pc_timeless = pc_timeless,
|
1201
|
+
pc_titles = pc_titles,
|
1202
|
+
ci = ci,
|
1203
|
+
length = length,
|
1204
|
+
radw = radw,
|
1205
|
+
skin = skin,
|
1206
|
+
stat = stat,
|
1207
|
+
length_uom = length_uom,
|
1208
|
+
grid = grid,
|
1209
|
+
traj_crs = traj_crs)
|
1650
1210
|
if skin_i is None:
|
1651
1211
|
skin_i = 0.0
|
1652
1212
|
if radw_i is None:
|
1653
1213
|
radw_i = (0.33 if length_uom == 'ft' else 0.1)
|
1654
|
-
|
1655
|
-
|
1656
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
|
1660
|
-
|
1661
|
-
|
1662
|
-
|
1663
|
-
|
1664
|
-
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
1669
|
-
|
1670
|
-
|
1671
|
-
|
1672
|
-
|
1673
|
-
|
1674
|
-
|
1214
|
+
if stat_i is None:
|
1215
|
+
stat_i = stat
|
1216
|
+
|
1217
|
+
radb, wi, wbc = \
|
1218
|
+
BlockedWell.__get_well_inflow_parameters_for_interval(do_well_inflow = do_well_inflow,
|
1219
|
+
isotropic_perm = isotropic_perm,
|
1220
|
+
ntg_is_one = ntg_is_one,
|
1221
|
+
k_i = k_i,
|
1222
|
+
k_j = k_j,
|
1223
|
+
k_k = k_k,
|
1224
|
+
sine_anglv = sine_anglv,
|
1225
|
+
cosine_anglv = cosine_anglv,
|
1226
|
+
sine_angla = sine_angla,
|
1227
|
+
cosine_angla = cosine_angla,
|
1228
|
+
grid = grid,
|
1229
|
+
cell_kji0 = cell_kji0,
|
1230
|
+
radw = radw_i,
|
1231
|
+
radb = radb,
|
1232
|
+
wi = wi,
|
1233
|
+
wbc = wbc,
|
1234
|
+
skin = skin_i,
|
1235
|
+
kh = kh,
|
1236
|
+
length_uom = length_uom,
|
1237
|
+
column_list = column_list)
|
1675
1238
|
|
1676
1239
|
xyz = self.__get_xyz_for_interval(doing_xyz = doing_xyz,
|
1677
1240
|
length_mode = length_mode,
|
@@ -1703,7 +1266,7 @@ class BlockedWell(BaseResqpy):
|
|
1703
1266
|
kh = kh,
|
1704
1267
|
xyz = xyz,
|
1705
1268
|
md = md,
|
1706
|
-
stat =
|
1269
|
+
stat = stat_i,
|
1707
1270
|
part_perf_fraction = part_perf_fraction,
|
1708
1271
|
radb = radb,
|
1709
1272
|
wi = wi,
|
@@ -1751,6 +1314,7 @@ class BlockedWell(BaseResqpy):
|
|
1751
1314
|
fraction of wellbore frame interval in cell,
|
1752
1315
|
fraction of cell's wellbore interval in wellbore frame interval)
|
1753
1316
|
"""
|
1317
|
+
|
1754
1318
|
return bwf.blocked_well_frame_contributions_list(self, wbf)
|
1755
1319
|
|
1756
1320
|
def add_properties_from_wellbore_frame(self,
|
@@ -1815,1469 +1379,2055 @@ class BlockedWell(BaseResqpy):
|
|
1815
1379
|
set_perforation_fraction = set_perforation_fraction,
|
1816
1380
|
set_frame_interval = set_frame_interval)
|
1817
1381
|
|
1818
|
-
def
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1825
|
-
|
1826
|
-
return interval_count
|
1827
|
-
|
1828
|
-
@staticmethod
|
1829
|
-
def __prop_array(uuid_or_dict, grid):
|
1830
|
-
assert uuid_or_dict is not None and grid is not None
|
1831
|
-
if isinstance(uuid_or_dict, dict):
|
1832
|
-
prop_uuid = uuid_or_dict[grid.uuid]
|
1833
|
-
else:
|
1834
|
-
prop_uuid = uuid_or_dict # uuid either in form of string or uuid.UUID
|
1835
|
-
return grid.property_collection.single_array_ref(uuid = prop_uuid)
|
1836
|
-
|
1837
|
-
@staticmethod
|
1838
|
-
def __get_ref_vector(grid, grid_crs, cell_kji0, mode):
|
1839
|
-
# returns unit vector with true direction, ie. accounts for differing xy & z units in grid's crs
|
1840
|
-
# gravity = np.array((0.0, 0.0, 1.0))
|
1841
|
-
if mode == 'normal well i+':
|
1842
|
-
return None # ANGLA only: option for no projection onto a plane
|
1843
|
-
ref_vector = None
|
1844
|
-
# options for anglv or angla reference: 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down'
|
1845
|
-
if mode == 'z+':
|
1846
|
-
ref_vector = np.array((0.0, 0.0, 1.0))
|
1847
|
-
elif mode == 'z down':
|
1848
|
-
if grid_crs.z_inc_down:
|
1849
|
-
ref_vector = np.array((0.0, 0.0, 1.0))
|
1850
|
-
else:
|
1851
|
-
ref_vector = np.array((0.0, 0.0, -1.0))
|
1852
|
-
else:
|
1853
|
-
cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
|
1854
|
-
if grid_crs.xy_units != grid_crs.z_units:
|
1855
|
-
wam.convert_lengths(cell_axial_vectors[..., 2], grid_crs.z_units, grid_crs.xy_units)
|
1856
|
-
if mode in ['k+', 'k down']:
|
1857
|
-
ref_vector = vec.unit_vector(cell_axial_vectors[0])
|
1858
|
-
if mode == 'k down' and not grid.k_direction_is_down:
|
1859
|
-
ref_vector = -ref_vector
|
1860
|
-
else: # normal to plane of ij axes
|
1861
|
-
ref_vector = vec.unit_vector(vec.cross_product(cell_axial_vectors[1], cell_axial_vectors[2]))
|
1862
|
-
if mode == 'normal ij down':
|
1863
|
-
if grid_crs.z_inc_down:
|
1864
|
-
if ref_vector[2] < 0.0:
|
1865
|
-
ref_vector = -ref_vector
|
1866
|
-
else:
|
1867
|
-
if ref_vector[2] > 0.0:
|
1868
|
-
ref_vector = -ref_vector
|
1869
|
-
if ref_vector is None or ref_vector[2] == 0.0:
|
1870
|
-
if grid_crs.z_inc_down:
|
1871
|
-
ref_vector = np.array((0.0, 0.0, 1.0))
|
1872
|
-
else:
|
1873
|
-
ref_vector = np.array((0.0, 0.0, -1.0))
|
1874
|
-
return ref_vector
|
1875
|
-
|
1876
|
-
@staticmethod
|
1877
|
-
def __verify_angle_references(anglv_ref, angla_plane_ref):
|
1878
|
-
"""Verify that the references for anglv and angla are one of the acceptable options."""
|
1879
|
-
|
1880
|
-
assert anglv_ref in ['gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down']
|
1881
|
-
if anglv_ref == 'gravity':
|
1882
|
-
anglv_ref = 'z down'
|
1883
|
-
if angla_plane_ref is None:
|
1884
|
-
angla_plane_ref = anglv_ref
|
1885
|
-
assert angla_plane_ref in [
|
1886
|
-
'gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down', 'normal well i+'
|
1887
|
-
]
|
1888
|
-
if angla_plane_ref == 'gravity':
|
1889
|
-
angla_plane_ref = 'z down'
|
1890
|
-
return anglv_ref, angla_plane_ref
|
1891
|
-
|
1892
|
-
@staticmethod
|
1893
|
-
def __verify_saturation_ranges_and_property_uuids(max_satw, min_sato, max_satg, satw_uuid, sato_uuid, satg_uuid):
|
1894
|
-
# verify that the fluid saturation limits fall within 0.0 to 1.0 and that the uuid of the required
|
1895
|
-
# saturation property array has been specified.
|
1896
|
-
|
1897
|
-
if max_satw is not None and max_satw >= 1.0:
|
1898
|
-
max_satw = None
|
1899
|
-
if min_sato is not None and min_sato <= 0.0:
|
1900
|
-
min_sato = None
|
1901
|
-
if max_satg is not None and max_satg >= 1.0:
|
1902
|
-
max_satg = None
|
1903
|
-
|
1904
|
-
phase_list = ['water', 'oil', 'gas']
|
1905
|
-
phase_saturation_limits_list = [max_satw, min_sato, max_satg]
|
1906
|
-
uuids_list = [satw_uuid, sato_uuid, satg_uuid]
|
1907
|
-
|
1908
|
-
for phase, phase_limit, uuid in zip(phase_list, phase_saturation_limits_list, uuids_list):
|
1909
|
-
if phase_limit is not None:
|
1910
|
-
assert uuid is not None, f'{phase} saturation limit specified without saturation property array'
|
1382
|
+
def add_df_properties(self,
|
1383
|
+
df,
|
1384
|
+
columns,
|
1385
|
+
length_uom = None,
|
1386
|
+
time_index = None,
|
1387
|
+
time_series_uuid = None,
|
1388
|
+
realization = None):
|
1389
|
+
"""Creates a property part for each column in the dataframe, based on the dataframe values.
|
1911
1390
|
|
1912
|
-
|
1391
|
+
arguments:
|
1392
|
+
df (pd.DataFrame): dataframe containing the columns that will be converted to properties
|
1393
|
+
columns (List[str]): list of the column names that will be converted to properties
|
1394
|
+
length_uom (str, optional): the length unit of measure
|
1395
|
+
time_index (int, optional): if adding a timestamp to the property, this is the timestamp
|
1396
|
+
index of the TimeSeries timestamps attribute
|
1397
|
+
time_series_uuid (uuid.UUID, optional): if adding a timestamp to the property, this is
|
1398
|
+
the uuid of the TimeSeries object
|
1399
|
+
realization (int, optional): if present, is used as the realization number for all the
|
1400
|
+
properties
|
1913
1401
|
|
1914
|
-
|
1915
|
-
|
1916
|
-
use_properties, skin, stat, radw):
|
1917
|
-
"""Determine which extra columns, if any, should be added as properties to the dataframe.
|
1402
|
+
returns:
|
1403
|
+
None
|
1918
1404
|
|
1919
|
-
|
1920
|
-
|
1405
|
+
notes:
|
1406
|
+
the column name is used as the property citation title;
|
1407
|
+
the blocked well must already exist as a part in the model;
|
1408
|
+
this method currently only handles single grid situations;
|
1409
|
+
dataframe rows must be in the same order as the cells in the blocked well
|
1921
1410
|
"""
|
1922
1411
|
|
1923
|
-
|
1924
|
-
|
1925
|
-
|
1926
|
-
|
1927
|
-
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
assert not (add_as_properties and use_properties)
|
1412
|
+
# todo: enhance to handle multiple grids
|
1413
|
+
assert len(self.grid_list) == 1
|
1414
|
+
if columns is None or len(columns) == 0 or len(df) == 0:
|
1415
|
+
return
|
1416
|
+
if length_uom is None:
|
1417
|
+
length_uom = self.trajectory.md_uom
|
1418
|
+
extra_pc = rqp.PropertyCollection()
|
1419
|
+
extra_pc.set_support(support = self)
|
1420
|
+
assert len(df) == self.cell_count
|
1933
1421
|
|
1934
|
-
|
1935
|
-
|
1422
|
+
for column in columns:
|
1423
|
+
extra = column.upper()
|
1424
|
+
uom, pk, discrete = self._get_uom_pk_discrete_for_df_properties(extra = extra, length_uom = length_uom)
|
1425
|
+
if discrete:
|
1426
|
+
null_value = -1
|
1427
|
+
na_value = -1
|
1428
|
+
dtype = np.int32
|
1429
|
+
else:
|
1430
|
+
null_value = None
|
1431
|
+
na_value = np.nan
|
1432
|
+
dtype = float
|
1433
|
+
# 'SKIN': use defaults for now; todo: create local property kind for skin
|
1434
|
+
if column == 'STAT':
|
1435
|
+
col_as_list = list(df[column])
|
1436
|
+
expanded = np.array([(0 if (str(st).upper() in ['OFF', '0', 'FALSE']) else 1) for st in col_as_list],
|
1437
|
+
dtype = np.int8)
|
1438
|
+
dtype = np.int8
|
1439
|
+
else:
|
1440
|
+
expanded = df[column].to_numpy(dtype = dtype, copy = True, na_value = na_value)
|
1441
|
+
extra_pc.add_cached_array_to_imported_list(
|
1442
|
+
expanded,
|
1443
|
+
'blocked well dataframe',
|
1444
|
+
extra,
|
1445
|
+
discrete = discrete,
|
1446
|
+
uom = uom,
|
1447
|
+
property_kind = pk,
|
1448
|
+
local_property_kind_uuid = None,
|
1449
|
+
facet_type = None,
|
1450
|
+
facet = None,
|
1451
|
+
realization = realization,
|
1452
|
+
indexable_element = 'cells',
|
1453
|
+
count = 1,
|
1454
|
+
time_index = time_index,
|
1455
|
+
null_value = null_value,
|
1456
|
+
)
|
1457
|
+
extra_pc.write_hdf5_for_imported_list()
|
1458
|
+
extra_pc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_series_uuid,
|
1459
|
+
find_local_property_kinds = True)
|
1936
1460
|
|
1937
|
-
|
1461
|
+
def static_kh(self,
|
1462
|
+
ntg_uuid = None,
|
1463
|
+
perm_i_uuid = None,
|
1464
|
+
perm_j_uuid = None,
|
1465
|
+
perm_k_uuid = None,
|
1466
|
+
satw_uuid = None,
|
1467
|
+
sato_uuid = None,
|
1468
|
+
satg_uuid = None,
|
1469
|
+
region_uuid = None,
|
1470
|
+
active_only = False,
|
1471
|
+
min_k0 = None,
|
1472
|
+
max_k0 = None,
|
1473
|
+
k0_list = None,
|
1474
|
+
min_length = None,
|
1475
|
+
min_kh = None,
|
1476
|
+
max_depth = None,
|
1477
|
+
max_satw = None,
|
1478
|
+
min_sato = None,
|
1479
|
+
max_satg = None,
|
1480
|
+
perforation_list = None,
|
1481
|
+
region_list = None,
|
1482
|
+
set_k_face_intervals_vertical = False,
|
1483
|
+
anglv_ref = 'gravity',
|
1484
|
+
angla_plane_ref = None,
|
1485
|
+
length_mode = 'MD',
|
1486
|
+
length_uom = None,
|
1487
|
+
use_face_centres = False,
|
1488
|
+
preferential_perforation = True):
|
1489
|
+
"""Returns the total static K.H (permeability x height).
|
1938
1490
|
|
1939
|
-
|
1940
|
-
|
1491
|
+
notes:
|
1492
|
+
length units are those of trajectory md_uom unless length_upm is set;
|
1493
|
+
see doc string for dataframe() method for argument descriptions; perm_i_uuid required
|
1494
|
+
"""
|
1941
1495
|
|
1942
|
-
|
1943
|
-
|
1944
|
-
|
1945
|
-
|
1946
|
-
|
1947
|
-
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
1496
|
+
df = self.dataframe(i_col = 'I',
|
1497
|
+
j_col = 'J',
|
1498
|
+
k_col = 'K',
|
1499
|
+
one_based = False,
|
1500
|
+
extra_columns_list = ['KH'],
|
1501
|
+
ntg_uuid = ntg_uuid,
|
1502
|
+
perm_i_uuid = perm_i_uuid,
|
1503
|
+
perm_j_uuid = perm_j_uuid,
|
1504
|
+
perm_k_uuid = perm_k_uuid,
|
1505
|
+
satw_uuid = satw_uuid,
|
1506
|
+
sato_uuid = sato_uuid,
|
1507
|
+
satg_uuid = satg_uuid,
|
1508
|
+
region_uuid = region_uuid,
|
1509
|
+
active_only = active_only,
|
1510
|
+
min_k0 = min_k0,
|
1511
|
+
max_k0 = max_k0,
|
1512
|
+
k0_list = k0_list,
|
1513
|
+
min_length = min_length,
|
1514
|
+
min_kh = min_kh,
|
1515
|
+
max_depth = max_depth,
|
1516
|
+
max_satw = max_satw,
|
1517
|
+
min_sato = min_sato,
|
1518
|
+
max_satg = max_satg,
|
1519
|
+
perforation_list = perforation_list,
|
1520
|
+
region_list = region_list,
|
1521
|
+
set_k_face_intervals_vertical = set_k_face_intervals_vertical,
|
1522
|
+
anglv_ref = anglv_ref,
|
1523
|
+
angla_plane_ref = angla_plane_ref,
|
1524
|
+
length_mode = length_mode,
|
1525
|
+
length_uom = length_uom,
|
1526
|
+
use_face_centres = use_face_centres,
|
1527
|
+
preferential_perforation = preferential_perforation)
|
1952
1528
|
|
1953
|
-
|
1954
|
-
log.warning('empty perforation list specified for blocked well dataframe: no rows will be included')
|
1529
|
+
return sum(df['KH'])
|
1955
1530
|
|
1956
|
-
|
1957
|
-
|
1958
|
-
|
1959
|
-
|
1960
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1965
|
-
|
1966
|
-
|
1967
|
-
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
1985
|
-
|
1531
|
+
def write_wellspec(self,
|
1532
|
+
wellspec_file,
|
1533
|
+
well_name = None,
|
1534
|
+
mode = 'a',
|
1535
|
+
extra_columns_list = [],
|
1536
|
+
ntg_uuid = None,
|
1537
|
+
perm_i_uuid = None,
|
1538
|
+
perm_j_uuid = None,
|
1539
|
+
perm_k_uuid = None,
|
1540
|
+
satw_uuid = None,
|
1541
|
+
sato_uuid = None,
|
1542
|
+
satg_uuid = None,
|
1543
|
+
region_uuid = None,
|
1544
|
+
radw = None,
|
1545
|
+
skin = None,
|
1546
|
+
stat = None,
|
1547
|
+
active_only = False,
|
1548
|
+
min_k0 = None,
|
1549
|
+
max_k0 = None,
|
1550
|
+
k0_list = None,
|
1551
|
+
min_length = None,
|
1552
|
+
min_kh = None,
|
1553
|
+
max_depth = None,
|
1554
|
+
max_satw = None,
|
1555
|
+
min_sato = None,
|
1556
|
+
max_satg = None,
|
1557
|
+
perforation_list = None,
|
1558
|
+
region_list = None,
|
1559
|
+
set_k_face_intervals_vertical = False,
|
1560
|
+
depth_inc_down = True,
|
1561
|
+
anglv_ref = 'gravity',
|
1562
|
+
angla_plane_ref = None,
|
1563
|
+
length_mode = 'MD',
|
1564
|
+
length_uom = None,
|
1565
|
+
preferential_perforation = True,
|
1566
|
+
space_instead_of_tab_separator = True,
|
1567
|
+
align_columns = True,
|
1568
|
+
preceeding_blank_lines = 0,
|
1569
|
+
trailing_blank_lines = 0,
|
1570
|
+
length_uom_comment = False,
|
1571
|
+
write_nexus_units = True,
|
1572
|
+
float_format = '5.3',
|
1573
|
+
use_properties = False,
|
1574
|
+
property_time_index = None):
|
1575
|
+
"""Writes Nexus WELLSPEC keyword to an ascii file.
|
1986
1576
|
|
1987
|
-
|
1577
|
+
returns:
|
1578
|
+
pandas DataFrame containing data that has been written to the wellspec file
|
1988
1579
|
|
1989
|
-
|
1990
|
-
|
1991
|
-
|
1992
|
-
|
1580
|
+
note:
|
1581
|
+
see doc string for dataframe() method for most of the argument descriptions;
|
1582
|
+
align_columns and float_format arguments are deprecated and no longer used
|
1583
|
+
"""
|
1993
1584
|
|
1994
|
-
|
1995
|
-
min_kh = None
|
1996
|
-
doing_kh = False
|
1997
|
-
if ('KH' in column_list or min_kh is not None) and 'KH' not in pc_titles:
|
1998
|
-
assert perm_i_uuid is not None, 'KH requested (or minimum specified) without I direction permeabilty being specified'
|
1999
|
-
doing_kh = True
|
2000
|
-
if 'WBC' in column_list and 'WBC' not in pc_titles:
|
2001
|
-
assert perm_i_uuid is not None, 'WBC requested without I direction permeabilty being specified'
|
2002
|
-
doing_kh = True
|
1585
|
+
assert wellspec_file, 'no output file specified to write WELLSPEC to'
|
2003
1586
|
|
2004
|
-
|
1587
|
+
col_width_dict = {
|
1588
|
+
'IW': 4,
|
1589
|
+
'JW': 4,
|
1590
|
+
'L': 4,
|
1591
|
+
'ANGLA': 8,
|
1592
|
+
'ANGLV': 8,
|
1593
|
+
'LENGTH': 8,
|
1594
|
+
'KH': 10,
|
1595
|
+
'DEPTH': 10,
|
1596
|
+
'MD': 10,
|
1597
|
+
'X': 8,
|
1598
|
+
'Y': 12,
|
1599
|
+
'SKIN': 7,
|
1600
|
+
'RADW': 5,
|
1601
|
+
'RADB': 8,
|
1602
|
+
'PPERF': 5
|
1603
|
+
}
|
2005
1604
|
|
2006
|
-
|
2007
|
-
def __verify_perm_j_k_uuids_for_kh_and_well_inflow(doing_kh, do_well_inflow, perm_i_uuid, perm_j_uuid, perm_k_uuid):
|
2008
|
-
# verify that the J and K direction permeabilities have been specified if well inflow properties or
|
2009
|
-
# permeability thickness properties are to be added to the dataframe.
|
1605
|
+
well_name = self.__get_well_name(well_name = well_name)
|
2010
1606
|
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
|
2018
|
-
|
2019
|
-
|
2020
|
-
|
2021
|
-
|
2022
|
-
|
1607
|
+
df = self.dataframe(one_based = True,
|
1608
|
+
extra_columns_list = extra_columns_list,
|
1609
|
+
ntg_uuid = ntg_uuid,
|
1610
|
+
perm_i_uuid = perm_i_uuid,
|
1611
|
+
perm_j_uuid = perm_j_uuid,
|
1612
|
+
perm_k_uuid = perm_k_uuid,
|
1613
|
+
satw_uuid = satw_uuid,
|
1614
|
+
sato_uuid = sato_uuid,
|
1615
|
+
satg_uuid = satg_uuid,
|
1616
|
+
region_uuid = region_uuid,
|
1617
|
+
radw = radw,
|
1618
|
+
skin = skin,
|
1619
|
+
stat = stat,
|
1620
|
+
active_only = active_only,
|
1621
|
+
min_k0 = min_k0,
|
1622
|
+
max_k0 = max_k0,
|
1623
|
+
k0_list = k0_list,
|
1624
|
+
min_length = min_length,
|
1625
|
+
min_kh = min_kh,
|
1626
|
+
max_depth = max_depth,
|
1627
|
+
max_satw = max_satw,
|
1628
|
+
min_sato = min_sato,
|
1629
|
+
max_satg = max_satg,
|
1630
|
+
perforation_list = perforation_list,
|
1631
|
+
region_list = region_list,
|
1632
|
+
depth_inc_down = depth_inc_down,
|
1633
|
+
set_k_face_intervals_vertical = set_k_face_intervals_vertical,
|
1634
|
+
anglv_ref = anglv_ref,
|
1635
|
+
angla_plane_ref = angla_plane_ref,
|
1636
|
+
length_mode = length_mode,
|
1637
|
+
length_uom = length_uom,
|
1638
|
+
preferential_perforation = preferential_perforation,
|
1639
|
+
use_properties = use_properties,
|
1640
|
+
property_time_index = property_time_index)
|
2023
1641
|
|
2024
|
-
|
1642
|
+
sep = ' ' if space_instead_of_tab_separator else '\t'
|
2025
1643
|
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
1644
|
+
with open(wellspec_file, mode = mode) as fp:
|
1645
|
+
for _ in range(preceeding_blank_lines):
|
1646
|
+
fp.write('\n')
|
2029
1647
|
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
2034
|
-
|
2035
|
-
|
2036
|
-
if k0_list is not None and len(k0_list) == 0:
|
2037
|
-
log.warning('no layers included for blocked well dataframe: no rows will be included')
|
1648
|
+
self.__write_wellspec_file_units_metadata(write_nexus_units = write_nexus_units,
|
1649
|
+
fp = fp,
|
1650
|
+
length_uom = length_uom,
|
1651
|
+
length_uom_comment = length_uom_comment,
|
1652
|
+
extra_columns_list = extra_columns_list,
|
1653
|
+
well_name = well_name)
|
2038
1654
|
|
2039
|
-
|
2040
|
-
def __verify_if_angles_xyz_and_length_to_be_added(column_list, pc_titles, doing_kh, do_well_inflow, length_mode):
|
2041
|
-
# determine if angla, anglv, x, y, z and length data are to be added as properties to the dataframe
|
1655
|
+
BlockedWell.__write_wellspec_file_columns(df = df, fp = fp, col_width_dict = col_width_dict, sep = sep)
|
2042
1656
|
|
2043
|
-
|
2044
|
-
('ANGLV' in column_list and 'ANGLV' not in pc_titles), (doing_kh), (do_well_inflow)])
|
2045
|
-
doing_xyz = any([('X' in column_list and 'X' not in pc_titles), ('Y' in column_list and 'Y' not in pc_titles),
|
2046
|
-
('DEPTH' in column_list and 'DEPTH' not in pc_titles)])
|
2047
|
-
doing_entry_exit = any([(doing_angles),
|
2048
|
-
('LENGTH' in column_list and 'LENGTH' not in pc_titles and length_mode == 'straight')])
|
1657
|
+
fp.write('\n')
|
2049
1658
|
|
2050
|
-
|
2051
|
-
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
# length_mode == 'straight')
|
1659
|
+
BlockedWell.__write_wellspec_file_rows_from_dataframe(df = df,
|
1660
|
+
fp = fp,
|
1661
|
+
col_width_dict = col_width_dict,
|
1662
|
+
sep = sep)
|
1663
|
+
for _ in range(trailing_blank_lines):
|
1664
|
+
fp.write('\n')
|
2057
1665
|
|
2058
|
-
return
|
1666
|
+
return df
|
2059
1667
|
|
2060
|
-
def
|
2061
|
-
|
2062
|
-
# verify that each grid's crs units are consistent in all directions
|
1668
|
+
def kji0_marker(self, active_only = True):
|
1669
|
+
"""Convenience method returning (k0, j0, i0), grid_uuid of first blocked interval."""
|
2063
1670
|
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
grid_crs = crs.Crs(self.model, uuid = grid.crs_uuid)
|
2069
|
-
grid_crs_list.append(grid_crs)
|
2070
|
-
return grid_crs_list
|
1671
|
+
cells, grids = self.cell_indices_and_grid_list()
|
1672
|
+
if cells is None or grids is None or len(grids) == 0:
|
1673
|
+
return None, None, None, None
|
1674
|
+
return cells[0], grids[0].uuid
|
2071
1675
|
|
2072
|
-
def
|
1676
|
+
def xyz_marker(self, active_only = True):
|
1677
|
+
"""Convenience method returning (x, y, z), crs_uuid of perforation in first blocked interval.
|
2073
1678
|
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
traj_crs = crs.Crs(self.trajectory.model, uuid = self.trajectory.crs_uuid)
|
2079
|
-
traj_z_inc_down = traj_crs.z_inc_down
|
1679
|
+
notes:
|
1680
|
+
active_only argument not yet in use;
|
1681
|
+
returns None, None if no blocked interval found
|
1682
|
+
"""
|
2080
1683
|
|
2081
|
-
|
1684
|
+
cells, grids = self.cell_indices_and_grid_list()
|
1685
|
+
if cells is None or grids is None or len(grids) == 0:
|
1686
|
+
return None, None
|
1687
|
+
node_index = 0
|
1688
|
+
while node_index < self.node_count - 1 and self.grid_indices[node_index] == -1:
|
1689
|
+
node_index += 1
|
1690
|
+
if node_index >= self.node_count - 1:
|
1691
|
+
return None, None
|
1692
|
+
md = 0.5 * (self.node_mds[node_index] + self.node_mds[node_index + 1])
|
1693
|
+
xyz = self.trajectory.xyz_for_md(md)
|
1694
|
+
return xyz, self.trajectory.crs_uuid
|
2082
1695
|
|
2083
|
-
|
2084
|
-
|
2085
|
-
"""Check whether the maximum depth specified has been exceeded with the current interval."""
|
1696
|
+
def create_feature_and_interpretation(self, shared_interpretation = True):
|
1697
|
+
"""Instantiate new empty WellboreFeature and WellboreInterpretation objects.
|
2086
1698
|
|
2087
|
-
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
1699
|
+
note:
|
1700
|
+
uses the Blocked well citation title or other related object title as the well name
|
1701
|
+
"""
|
1702
|
+
title = self.well_name
|
1703
|
+
if not title:
|
1704
|
+
title = self.title
|
1705
|
+
if not title and self.trajectory is not None:
|
1706
|
+
title = rqw.well_name(self.trajectory)
|
1707
|
+
if not title:
|
1708
|
+
title = 'WELL'
|
1709
|
+
if self.trajectory is not None:
|
1710
|
+
traj_interp_uuid = self.model.uuid(obj_type = 'WellboreInterpretation', related_uuid = self.trajectory.uuid)
|
1711
|
+
if traj_interp_uuid is not None:
|
1712
|
+
if shared_interpretation:
|
1713
|
+
self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
|
1714
|
+
uuid = traj_interp_uuid)
|
1715
|
+
traj_feature_uuid = self.model.uuid(obj_type = 'WellboreFeature', related_uuid = traj_interp_uuid)
|
1716
|
+
if traj_feature_uuid is not None:
|
1717
|
+
self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, uuid = traj_feature_uuid)
|
1718
|
+
if self.wellbore_feature is None:
|
1719
|
+
self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, feature_name = title)
|
1720
|
+
self.feature_to_be_written = True
|
1721
|
+
if self.wellbore_interpretation is None:
|
1722
|
+
title = title if not self.wellbore_feature.title else self.wellbore_feature.title
|
1723
|
+
self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
|
1724
|
+
title = title,
|
1725
|
+
wellbore_feature = self.wellbore_feature)
|
1726
|
+
if self.trajectory.wellbore_interpretation is None and shared_interpretation:
|
1727
|
+
self.trajectory.wellbore_interpretation = self.wellbore_interpretation
|
1728
|
+
self.interpretation_to_be_written = True
|
2100
1729
|
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
|
2108
|
-
|
2109
|
-
|
2110
|
-
|
2111
|
-
saturation_limit_exceeded_1 = (max_satw is not None and
|
2112
|
-
BlockedWell.__prop_array(satw_uuid, grid)[tuple_kji0] > max_satw)
|
2113
|
-
saturation_limit_exceeded_2 = (min_sato is not None and
|
2114
|
-
BlockedWell.__prop_array(sato_uuid, grid)[tuple_kji0] < min_sato)
|
2115
|
-
saturation_limit_exceeded_3 = (max_satg is not None and
|
2116
|
-
BlockedWell.__prop_array(satg_uuid, grid)[tuple_kji0] > max_satg)
|
2117
|
-
skip_interval = any([
|
2118
|
-
max_depth_exceeded, inactive_grid, out_of_bounds_layer_1, out_of_bounds_layer_2, out_of_bounds_region,
|
2119
|
-
saturation_limit_exceeded_1, saturation_limit_exceeded_2, saturation_limit_exceeded_3
|
2120
|
-
])
|
1730
|
+
def create_md_datum_and_trajectory(self,
|
1731
|
+
grid,
|
1732
|
+
trajectory_mds,
|
1733
|
+
trajectory_points,
|
1734
|
+
length_uom,
|
1735
|
+
well_name,
|
1736
|
+
set_depth_zero = False,
|
1737
|
+
set_tangent_vectors = False,
|
1738
|
+
create_feature_and_interp = True):
|
1739
|
+
"""Creates an Md Datum object and a (simulation) Trajectory object for this blocked well.
|
2121
1740
|
|
2122
|
-
|
1741
|
+
note:
|
1742
|
+
not usually called directly; used by import methods
|
1743
|
+
"""
|
2123
1744
|
|
2124
|
-
|
2125
|
-
|
1745
|
+
if not well_name:
|
1746
|
+
well_name = self.title
|
2126
1747
|
|
2127
|
-
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
2135
|
-
if perf_end <= self.node_mds[interval] or perf_start >= self.node_mds[interval + 1]:
|
2136
|
-
continue
|
2137
|
-
if perf_start <= self.node_mds[interval]:
|
2138
|
-
if perf_end >= self.node_mds[interval + 1]:
|
2139
|
-
perf_length += self.node_mds[interval + 1] - self.node_mds[interval]
|
2140
|
-
break
|
2141
|
-
else:
|
2142
|
-
perf_length += perf_end - self.node_mds[interval]
|
2143
|
-
else:
|
2144
|
-
if perf_end >= self.node_mds[interval + 1]:
|
2145
|
-
perf_length += self.node_mds[interval + 1] - perf_start
|
2146
|
-
else:
|
2147
|
-
perf_length += perf_end - perf_start
|
2148
|
-
if perf_length < length_tol:
|
2149
|
-
skip_interval = True
|
2150
|
-
perf_length = 0.0
|
2151
|
-
part_perf_fraction = min(1.0, perf_length / (self.node_mds[interval + 1] - self.node_mds[interval]))
|
1748
|
+
# create md datum node for synthetic trajectory, using crs for grid
|
1749
|
+
datum_location = trajectory_points[0].copy()
|
1750
|
+
if set_depth_zero:
|
1751
|
+
datum_location[2] = 0.0
|
1752
|
+
datum = rqw.MdDatum(self.model,
|
1753
|
+
crs_uuid = grid.crs_uuid,
|
1754
|
+
location = datum_location,
|
1755
|
+
md_reference = 'mean sea level')
|
2152
1756
|
|
2153
|
-
|
1757
|
+
# create synthetic trajectory object, using crs for grid
|
1758
|
+
trajectory_mds_array = np.array(trajectory_mds)
|
1759
|
+
trajectory_xyz_array = np.array(trajectory_points)
|
1760
|
+
trajectory_df = pd.DataFrame({
|
1761
|
+
'MD': trajectory_mds_array,
|
1762
|
+
'X': trajectory_xyz_array[..., 0],
|
1763
|
+
'Y': trajectory_xyz_array[..., 1],
|
1764
|
+
'Z': trajectory_xyz_array[..., 2]
|
1765
|
+
})
|
1766
|
+
self.trajectory = rqw.Trajectory(self.model,
|
1767
|
+
md_datum = datum,
|
1768
|
+
data_frame = trajectory_df,
|
1769
|
+
length_uom = length_uom,
|
1770
|
+
well_name = well_name,
|
1771
|
+
set_tangent_vectors = set_tangent_vectors)
|
1772
|
+
self.trajectory_to_be_written = True
|
2154
1773
|
|
2155
|
-
|
2156
|
-
|
2157
|
-
# calculate the entry and exit points for the interval and set the entry and exit coordinate reference system
|
1774
|
+
if create_feature_and_interp:
|
1775
|
+
self.create_feature_and_interpretation()
|
2158
1776
|
|
2159
|
-
|
2160
|
-
|
2161
|
-
|
2162
|
-
|
2163
|
-
|
2164
|
-
|
2165
|
-
|
2166
|
-
|
2167
|
-
if self.face_pair_indices[ci, 1, 0] >= 0:
|
2168
|
-
exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 1, 0],
|
2169
|
-
self.face_pair_indices[ci, 1, 1])
|
2170
|
-
else:
|
2171
|
-
exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0],
|
2172
|
-
1 - self.face_pair_indices[ci, 0, 1])
|
2173
|
-
ee_crs = grid_crs
|
2174
|
-
else:
|
2175
|
-
entry_xyz = self.trajectory.xyz_for_md(self.node_mds[interval])
|
2176
|
-
exit_xyz = self.trajectory.xyz_for_md(self.node_mds[interval + 1])
|
2177
|
-
ee_crs = traj_crs
|
1777
|
+
def create_xml(self,
|
1778
|
+
ext_uuid = None,
|
1779
|
+
create_for_trajectory_if_needed = True,
|
1780
|
+
add_as_part = True,
|
1781
|
+
add_relationships = True,
|
1782
|
+
title = None,
|
1783
|
+
originator = None):
|
1784
|
+
"""Create a blocked wellbore representation node from this BlockedWell object, optionally add as part.
|
2178
1785
|
|
2179
|
-
|
1786
|
+
note:
|
1787
|
+
trajectory xml node must be in place before calling this function;
|
1788
|
+
witsml log reference, interval stratigraphic units, and cell fluid phase units not yet supported
|
2180
1789
|
|
2181
|
-
|
2182
|
-
|
2183
|
-
"""Calculate the length of the interval."""
|
1790
|
+
:meta common:
|
1791
|
+
"""
|
2184
1792
|
|
2185
|
-
|
2186
|
-
if length_mode == 'MD':
|
2187
|
-
length = self.node_mds[interval + 1] - self.node_mds[interval]
|
2188
|
-
if length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
|
2189
|
-
length = wam.convert_lengths(length, self.trajectory.md_uom, length_uom)
|
2190
|
-
else: # use straight line length between entry and exit
|
2191
|
-
entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
|
2192
|
-
length = vec.naive_length(exit_xyz - entry_xyz)
|
2193
|
-
if length_uom is not None:
|
2194
|
-
length = wam.convert_lengths(length, ee_crs.z_units, length_uom)
|
2195
|
-
elif self.trajectory is not None:
|
2196
|
-
length = wam.convert_lengths(length, ee_crs.z_units, self.trajectory.md_uom)
|
2197
|
-
if perforation_list is not None:
|
2198
|
-
length *= part_perf_fraction
|
2199
|
-
if min_length is not None and length < min_length:
|
2200
|
-
skip_interval = True
|
1793
|
+
assert self.trajectory is not None, 'trajectory object missing'
|
2201
1794
|
|
2202
|
-
|
1795
|
+
if ext_uuid is None:
|
1796
|
+
ext_uuid = self.model.h5_uuid()
|
2203
1797
|
|
2204
|
-
|
2205
|
-
|
2206
|
-
if
|
2207
|
-
|
2208
|
-
|
2209
|
-
if crs.xy_units != required_uom:
|
2210
|
-
xyz[0] = wam.convert_lengths(xyz[0], crs.xy_units, required_uom)
|
2211
|
-
xyz[1] = wam.convert_lengths(xyz[1], crs.xy_units, required_uom)
|
2212
|
-
if crs.z_units != required_uom:
|
2213
|
-
xyz[2] = wam.convert_lengths(xyz[2], crs.z_units, required_uom)
|
2214
|
-
return xyz
|
1798
|
+
if title:
|
1799
|
+
self.title = title
|
1800
|
+
if not self.title:
|
1801
|
+
self.title = self.well_name
|
1802
|
+
title = self.title
|
2215
1803
|
|
2216
|
-
|
2217
|
-
|
2218
|
-
|
2219
|
-
BlockedWell._single_uom_xyz(exit_xyz, ee_crs, ee_crs.z_units))
|
1804
|
+
self.__create_wellbore_feature_and_interpretation_xml_if_needed(add_as_part = add_as_part,
|
1805
|
+
add_relationships = add_relationships,
|
1806
|
+
originator = originator)
|
2220
1807
|
|
2221
|
-
|
2222
|
-
|
2223
|
-
|
2224
|
-
|
1808
|
+
self.__create_trajectory_xml_if_needed(create_for_trajectory_if_needed = create_for_trajectory_if_needed,
|
1809
|
+
add_as_part = add_as_part,
|
1810
|
+
add_relationships = add_relationships,
|
1811
|
+
originator = originator,
|
1812
|
+
ext_uuid = ext_uuid,
|
1813
|
+
title = title)
|
2225
1814
|
|
2226
|
-
|
2227
|
-
cosine_anglv = cosine_angla = 1.0
|
2228
|
-
anglv = pc.single_array_ref(citation_title = 'ANGLV')[ci] if 'ANGLV' in pc_titles else None
|
2229
|
-
angla = pc.single_array_ref(citation_title = 'ANGLA')[ci] if 'ANGLA' in pc_titles else None
|
1815
|
+
assert self.trajectory.root is not None, 'trajectory xml not established'
|
2230
1816
|
|
2231
|
-
|
2232
|
-
(np.all(self.face_pair_indices[ci] == k_face_check) or
|
2233
|
-
np.all(self.face_pair_indices[ci] == k_face_check_end))):
|
2234
|
-
anglv, sine_anglv, cosine_anglv, vector, a_ref_vector = BlockedWell.__get_anglv_for_interval(
|
2235
|
-
anglv = anglv,
|
2236
|
-
entry_xyz = entry_xyz,
|
2237
|
-
exit_xyz = exit_xyz,
|
2238
|
-
ee_crs = ee_crs,
|
2239
|
-
traj_z_inc_down = traj_z_inc_down,
|
2240
|
-
grid = grid,
|
2241
|
-
grid_crs = grid_crs,
|
2242
|
-
cell_kji0 = cell_kji0,
|
2243
|
-
anglv_ref = anglv_ref,
|
2244
|
-
angla_plane_ref = angla_plane_ref)
|
2245
|
-
if anglv != 0.0:
|
2246
|
-
angla, sine_angla, cosine_angla = BlockedWell.__get_angla_for_interval(angla = angla,
|
2247
|
-
grid = grid,
|
2248
|
-
cell_kji0 = cell_kji0,
|
2249
|
-
vector = vector,
|
2250
|
-
a_ref_vector = a_ref_vector)
|
2251
|
-
if angla is None:
|
2252
|
-
angla = 0.0
|
2253
|
-
if anglv is None:
|
2254
|
-
anglv = 0.0
|
1817
|
+
bw_node = super().create_xml(title = title, originator = originator, add_as_part = False)
|
2255
1818
|
|
2256
|
-
|
1819
|
+
# wellbore frame elements
|
2257
1820
|
|
2258
|
-
|
2259
|
-
|
2260
|
-
|
1821
|
+
nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node, gnull_node, \
|
1822
|
+
gis_values_node, fis_node, fnull_node, fis_values_node = \
|
1823
|
+
self.__create_bw_node_sub_elements(bw_node = bw_node)
|
2261
1824
|
|
2262
|
-
|
2263
|
-
|
1825
|
+
self.__create_hdf5_dataset_references(ext_uuid = ext_uuid,
|
1826
|
+
mds_values_node = mds_values_node,
|
1827
|
+
cis_values_node = cis_values_node,
|
1828
|
+
gis_values_node = gis_values_node,
|
1829
|
+
fis_values_node = fis_values_node)
|
2264
1830
|
|
2265
|
-
|
2266
|
-
|
2267
|
-
if grid.crs.xy_units != grid.crs.z_units:
|
2268
|
-
i_axis[2] = wam.convert_lengths(i_axis[2], grid.crs.z_units, grid.crs.xy_units)
|
2269
|
-
i_axis = vec.unit_vector(i_axis)
|
2270
|
-
if a_ref_vector is not None: # project vector and i axis onto a plane
|
2271
|
-
vector -= vec.dot_product(vector, a_ref_vector) * a_ref_vector
|
2272
|
-
vector = vec.unit_vector(vector)
|
2273
|
-
# log.debug('i axis unit vector: ' + str(i_axis))
|
2274
|
-
i_axis -= vec.dot_product(i_axis, a_ref_vector) * a_ref_vector
|
2275
|
-
i_axis = vec.unit_vector(i_axis)
|
2276
|
-
# log.debug('i axis unit vector in reference plane: ' + str(i_axis))
|
2277
|
-
if angla is not None:
|
2278
|
-
angla_rad = vec.radians_from_degrees(angla)
|
2279
|
-
cosine_angla = maths.cos(angla_rad)
|
2280
|
-
sine_angla = maths.sin(angla_rad)
|
2281
|
-
else:
|
2282
|
-
cosine_angla = min(max(vec.dot_product(vector, i_axis), -1.0), 1.0)
|
2283
|
-
angla_rad = maths.acos(cosine_angla)
|
2284
|
-
# negate angla if vector is 'clockwise from' i_axis when viewed from above, projected in the xy plane
|
2285
|
-
# todo: have discussion around angla sign under different ijk handedness (and z inc direction?)
|
2286
|
-
sine_angla = maths.sin(angla_rad)
|
2287
|
-
angla = vec.degrees_from_radians(angla_rad)
|
2288
|
-
if vec.clockwise((0.0, 0.0), i_axis, vector) > 0.0:
|
2289
|
-
angla = -angla
|
2290
|
-
angla_rad = -angla_rad ## as angle_rad before --> typo?
|
2291
|
-
sine_angla = -sine_angla
|
1831
|
+
traj_root, grid_root, interp_root = self.__create_trajectory_grid_wellbore_interpretation_reference_nodes(
|
1832
|
+
bw_node = bw_node)
|
2292
1833
|
|
2293
|
-
|
1834
|
+
self.__add_as_part_and_add_relationships_if_required(add_as_part = add_as_part,
|
1835
|
+
add_relationships = add_relationships,
|
1836
|
+
bw_node = bw_node,
|
1837
|
+
interp_root = interp_root,
|
1838
|
+
ext_uuid = ext_uuid)
|
2294
1839
|
|
2295
|
-
return
|
1840
|
+
return bw_node
|
2296
1841
|
|
2297
|
-
|
2298
|
-
|
2299
|
-
anglv_ref, angla_plane_ref):
|
2300
|
-
"""Get anglv and related trigonometric transforms for the interval."""
|
1842
|
+
def write_hdf5(self, file_name = None, mode = 'a', create_for_trajectory_if_needed = True):
|
1843
|
+
"""Create or append to an hdf5 file, writing datasets for the measured depths, grid, cell & face indices.
|
2301
1844
|
|
2302
|
-
|
2303
|
-
|
1845
|
+
:meta common:
|
1846
|
+
"""
|
2304
1847
|
|
2305
|
-
|
2306
|
-
vector = vec.unit_vector(np.array(exit_xyz) - np.array(entry_xyz)) # nominal wellbore vector for interval
|
2307
|
-
if traj_z_inc_down is not None and traj_z_inc_down != grid_crs.z_inc_down:
|
2308
|
-
vector[2] = -vector[2]
|
2309
|
-
if grid.crs.xy_units == grid.crs.z_units:
|
2310
|
-
unit_adjusted_vector = vector
|
2311
|
-
else:
|
2312
|
-
unit_adjusted_vector = vector.copy()
|
2313
|
-
unit_adjusted_vector[2] = wam.convert_lengths(unit_adjusted_vector[2], grid.crs.z_units, grid.crs.xy_units)
|
2314
|
-
v_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, anglv_ref)
|
2315
|
-
# log.debug('v ref vector: ' + str(v_ref_vector))
|
2316
|
-
if angla_plane_ref == anglv_ref:
|
2317
|
-
a_ref_vector = v_ref_vector
|
2318
|
-
else:
|
2319
|
-
a_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, angla_plane_ref)
|
2320
|
-
# log.debug('a ref vector: ' + str(a_ref_vector))
|
2321
|
-
if anglv is not None:
|
2322
|
-
anglv_rad = vec.radians_from_degrees(anglv)
|
2323
|
-
cosine_anglv = maths.cos(anglv_rad)
|
2324
|
-
sine_anglv = maths.sin(anglv_rad)
|
2325
|
-
else:
|
2326
|
-
cosine_anglv = min(max(vec.dot_product(unit_adjusted_vector, v_ref_vector), -1.0), 1.0)
|
2327
|
-
anglv_rad = maths.acos(cosine_anglv)
|
2328
|
-
sine_anglv = maths.sin(anglv_rad)
|
2329
|
-
anglv = vec.degrees_from_radians(anglv_rad)
|
2330
|
-
# log.debug('anglv: ' + str(anglv))
|
1848
|
+
# NB: array data must all have been set up prior to calling this function
|
2331
1849
|
|
2332
|
-
|
1850
|
+
if self.uuid is None:
|
1851
|
+
self.uuid = bu.new_uuid()
|
2333
1852
|
|
2334
|
-
|
2335
|
-
def __get_ntg_and_directional_perm_for_interval(doing_kh, do_well_inflow, ntg_uuid, grid, tuple_kji0,
|
2336
|
-
isotropic_perm, preferential_perforation, part_perf_fraction,
|
2337
|
-
perm_i_uuid, perm_j_uuid, perm_k_uuid):
|
2338
|
-
"""Get the net-to-gross and directional permeability arrays for the interval."""
|
1853
|
+
h5_reg = rwh5.H5Register(self.model)
|
2339
1854
|
|
2340
|
-
|
2341
|
-
|
2342
|
-
|
2343
|
-
if ntg_uuid is None:
|
2344
|
-
ntg = 1.0
|
2345
|
-
ntg_is_one = True
|
2346
|
-
else:
|
2347
|
-
ntg = BlockedWell.__prop_array(ntg_uuid, grid)[tuple_kji0]
|
2348
|
-
ntg_is_one = maths.isclose(ntg, 1.0, rel_tol = 0.001)
|
2349
|
-
if isotropic_perm and ntg_is_one:
|
2350
|
-
k_i = k_j = k_k = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
|
2351
|
-
else:
|
2352
|
-
if preferential_perforation and not ntg_is_one:
|
2353
|
-
if part_perf_fraction <= ntg:
|
2354
|
-
ntg = 1.0 # effective ntg when perforated intervals are in pay
|
2355
|
-
else:
|
2356
|
-
ntg /= part_perf_fraction # adjusted ntg when some perforations in non-pay
|
2357
|
-
# todo: check netgross facet type in property perm i & j parts: if set to gross then don't multiply by ntg below
|
2358
|
-
k_i = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0] * ntg
|
2359
|
-
k_j = BlockedWell.__prop_array(perm_j_uuid, grid)[tuple_kji0] * ntg
|
2360
|
-
k_k = BlockedWell.__prop_array(perm_k_uuid, grid)[tuple_kji0]
|
1855
|
+
if create_for_trajectory_if_needed and self.trajectory_to_be_written:
|
1856
|
+
self.trajectory.write_hdf5(file_name, mode = mode)
|
1857
|
+
mode = 'a'
|
2361
1858
|
|
2362
|
-
|
1859
|
+
h5_reg.register_dataset(self.uuid, 'NodeMd', self.node_mds)
|
1860
|
+
h5_reg.register_dataset(self.uuid, 'CellIndices', self.cell_indices) # could use int32?
|
1861
|
+
h5_reg.register_dataset(self.uuid, 'GridIndices', self.grid_indices) # could use int32?
|
1862
|
+
# convert face index pairs from [axis, polarity] back to strange local face numbering
|
1863
|
+
mask = (self.face_pair_indices.flatten() == -1).reshape((-1, 2)) # 2nd axis is (axis, polarity)
|
1864
|
+
masked_face_indices = np.where(mask, 0, self.face_pair_indices.reshape((-1, 2))) # 2nd axis is (axis, polarity)
|
1865
|
+
# using flat array for raw_face_indices array
|
1866
|
+
# other resqml writing code might use an array with one int per entry point and one per exit point, with 2nd axis as (entry, exit)
|
1867
|
+
raw_face_indices = np.where(mask[:, 0], -1, self.face_index_map[masked_face_indices[:, 0],
|
1868
|
+
masked_face_indices[:,
|
1869
|
+
1]].flatten()).reshape(-1)
|
2363
1870
|
|
2364
|
-
|
2365
|
-
def __get_kh_for_interval(doing_kh, isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j,
|
2366
|
-
k_k, anglv, sine_anglv, cosine_anglv, sine_angla, cosine_angla, min_kh, pc, pc_titles,
|
2367
|
-
ci):
|
2368
|
-
"""Get the permeability-thickness value for the interval."""
|
1871
|
+
h5_reg.register_dataset(self.uuid, 'LocalFacePairPerCellIndices', raw_face_indices) # could use uint8?
|
2369
1872
|
|
2370
|
-
|
2371
|
-
if doing_kh:
|
2372
|
-
kh = BlockedWell.__get_kh_if_doing_kh(isotropic_perm = isotropic_perm,
|
2373
|
-
ntg_is_one = ntg_is_one,
|
2374
|
-
length = length,
|
2375
|
-
perm_i_uuid = perm_i_uuid,
|
2376
|
-
grid = grid,
|
2377
|
-
tuple_kji0 = tuple_kji0,
|
2378
|
-
k_i = k_i,
|
2379
|
-
k_j = k_j,
|
2380
|
-
k_k = k_k,
|
2381
|
-
anglv = anglv,
|
2382
|
-
sine_anglv = sine_anglv,
|
2383
|
-
cosine_anglv = cosine_anglv,
|
2384
|
-
sine_angla = sine_angla,
|
2385
|
-
cosine_angla = cosine_angla)
|
2386
|
-
if min_kh is not None and kh < min_kh:
|
2387
|
-
skip_interval = True
|
2388
|
-
elif 'KH' in pc_titles:
|
2389
|
-
kh = pc.single_array_ref(citation_title = 'KH')[ci]
|
2390
|
-
else:
|
2391
|
-
kh = None
|
2392
|
-
return skip_interval, kh
|
1873
|
+
h5_reg.write(file = file_name, mode = mode)
|
2393
1874
|
|
2394
|
-
|
2395
|
-
|
2396
|
-
sine_anglv, cosine_anglv, sine_angla, cosine_angla):
|
2397
|
-
# note: this is believed to return required value even when grid crs has mixed xy & z units;
|
2398
|
-
# angles are true angles accounting for any mixed units
|
2399
|
-
if isotropic_perm and ntg_is_one:
|
2400
|
-
kh = length * BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
|
2401
|
-
else:
|
2402
|
-
if np.isnan(k_i) or np.isnan(k_j):
|
2403
|
-
kh = 0.0
|
2404
|
-
elif anglv == 0.0:
|
2405
|
-
kh = length * maths.sqrt(k_i * k_j)
|
2406
|
-
elif np.isnan(k_k):
|
2407
|
-
kh = 0.0
|
2408
|
-
else:
|
2409
|
-
k_e = maths.pow(k_i * k_j * k_k, 1.0 / 3.0)
|
2410
|
-
if k_e == 0.0:
|
2411
|
-
kh = 0.0
|
2412
|
-
else:
|
2413
|
-
l_i = length * maths.sqrt(k_e / k_i) * sine_anglv * cosine_angla
|
2414
|
-
l_j = length * maths.sqrt(k_e / k_j) * sine_anglv * sine_angla
|
2415
|
-
l_k = length * maths.sqrt(k_e / k_k) * cosine_anglv
|
2416
|
-
l_p = maths.sqrt(l_i * l_i + l_j * l_j + l_k * l_k)
|
2417
|
-
kh = k_e * l_p
|
2418
|
-
return kh
|
1875
|
+
def add_grid_property_to_blocked_well(self, uuid_list):
|
1876
|
+
"""Add properties to blocked wells from a list of uuids for properties on the supporting grid."""
|
2419
1877
|
|
2420
|
-
|
2421
|
-
def __get_pc_arrays_for_interval(pc, pc_timeless, pc_titles, ci, length, radw, skin, length_uom, grid, traj_crs):
|
2422
|
-
"""Get the property collection arrays for the interval."""
|
1878
|
+
part_list = [self.model.part_for_uuid(uuid) for uuid in uuid_list]
|
2423
1879
|
|
2424
|
-
|
1880
|
+
assert len(self.grid_list) == 1, "only blocked wells with a single grid can be handled currently"
|
1881
|
+
grid = self.grid_list[0]
|
1882
|
+
# filter to only those properties on the grid
|
1883
|
+
parts = self.model.parts_list_filtered_by_supporting_uuid(part_list, grid.uuid)
|
1884
|
+
if len(parts) < len(uuid_list):
|
1885
|
+
log.warning(
|
1886
|
+
f"{len(uuid_list)-len(parts)} uuids ignored as they do not belong to the same grid as the blocked well")
|
2425
1887
|
|
2426
|
-
|
2427
|
-
|
2428
|
-
|
1888
|
+
gridpc = grid.extract_property_collection()
|
1889
|
+
# only 'cell' properties are handled
|
1890
|
+
cell_parts = [part for part in parts if gridpc.indexable_for_part(part) == 'cells']
|
1891
|
+
if len(cell_parts) < len(parts):
|
1892
|
+
log.warning(f"{len(parts)-len(cell_parts)} uuids ignored as they do not have indexable element of cells")
|
2429
1893
|
|
2430
|
-
|
2431
|
-
|
2432
|
-
|
2433
|
-
|
2434
|
-
|
2435
|
-
|
2436
|
-
|
2437
|
-
|
2438
|
-
|
2439
|
-
|
2440
|
-
|
2441
|
-
|
2442
|
-
v = pc_timeless.cached_part_array_ref(p)[ci]
|
2443
|
-
pc_uom = pc.uom_for_part(p)
|
2444
|
-
if pc_uom is not None and uom is not None and pc_uom != uom:
|
2445
|
-
v = wam.convert_lengths(v, pc_uom, uom)
|
2446
|
-
return v
|
1894
|
+
if len(cell_parts) > 0:
|
1895
|
+
bwpc = rqp.PropertyCollection(support = self)
|
1896
|
+
if len(gridpc.string_lookup_uuid_list()) > 0:
|
1897
|
+
sl_dict = {}
|
1898
|
+
for part in cell_parts:
|
1899
|
+
if gridpc.string_lookup_uuid_for_part(part) in sl_dict.keys():
|
1900
|
+
sl_dict[gridpc.string_lookup_uuid_for_part(part)] = \
|
1901
|
+
sl_dict[gridpc.string_lookup_uuid_for_part(part)] + [part]
|
1902
|
+
else:
|
1903
|
+
sl_dict[gridpc.string_lookup_uuid_for_part(part)] = [part]
|
1904
|
+
else:
|
1905
|
+
sl_dict = {None: cell_parts}
|
2447
1906
|
|
2448
|
-
|
2449
|
-
|
2450
|
-
|
2451
|
-
|
2452
|
-
|
2453
|
-
|
2454
|
-
|
2455
|
-
|
2456
|
-
|
2457
|
-
|
2458
|
-
|
2459
|
-
|
2460
|
-
|
2461
|
-
|
2462
|
-
|
2463
|
-
wi = get_item(None, 'WI', pc_titles, pc, pc_timeless, ci, None)
|
2464
|
-
wbc = get_item(None, 'WBC', pc_titles, pc, pc_timeless, ci, None)
|
1907
|
+
sl_ts_dict = {}
|
1908
|
+
for sl_uuid in sl_dict.keys():
|
1909
|
+
if len(gridpc.time_series_uuid_list()) > 0:
|
1910
|
+
# dictionary with keys for string_lookup uuids and None where missing
|
1911
|
+
# values for each key are a list of property parts associated with that lookup uuid, or None
|
1912
|
+
time_dict = {}
|
1913
|
+
for part in sl_dict[sl_uuid]:
|
1914
|
+
if gridpc.time_series_uuid_for_part(part) in time_dict.keys():
|
1915
|
+
time_dict[gridpc.time_series_uuid_for_part(part)] = \
|
1916
|
+
time_dict[gridpc.time_series_uuid_for_part(part)] + [part]
|
1917
|
+
else:
|
1918
|
+
time_dict[gridpc.time_series_uuid_for_part(part)] = [part]
|
1919
|
+
else:
|
1920
|
+
time_dict = {None: sl_dict[sl_uuid]}
|
1921
|
+
sl_ts_dict[sl_uuid] = time_dict
|
2465
1922
|
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
|
2472
|
-
|
2473
|
-
|
2474
|
-
|
2475
|
-
|
2476
|
-
|
2477
|
-
|
2478
|
-
|
2479
|
-
|
2480
|
-
|
2481
|
-
|
2482
|
-
|
2483
|
-
|
2484
|
-
|
2485
|
-
|
1923
|
+
for sl_uuid in sl_ts_dict.keys():
|
1924
|
+
time_dict = sl_ts_dict[sl_uuid]
|
1925
|
+
for time_uuid in time_dict.keys():
|
1926
|
+
parts = time_dict[time_uuid]
|
1927
|
+
for part in parts:
|
1928
|
+
array = gridpc.cached_part_array_ref(part)
|
1929
|
+
indices = self.cell_indices_for_grid_uuid(grid.uuid)
|
1930
|
+
bwarray = np.empty(shape = (indices.shape[0],), dtype = array.dtype)
|
1931
|
+
for i, ind in enumerate(indices):
|
1932
|
+
bwarray[i] = array[tuple(ind)]
|
1933
|
+
bwpc.add_cached_array_to_imported_list(
|
1934
|
+
bwarray,
|
1935
|
+
source_info = f'property from grid {grid.title}',
|
1936
|
+
keyword = gridpc.citation_title_for_part(part),
|
1937
|
+
discrete = (not gridpc.continuous_for_part(part)),
|
1938
|
+
uom = gridpc.uom_for_part(part),
|
1939
|
+
time_index = gridpc.time_index_for_part(part),
|
1940
|
+
null_value = gridpc.null_value_for_part(part),
|
1941
|
+
property_kind = gridpc.property_kind_for_part(part),
|
1942
|
+
local_property_kind_uuid = gridpc.local_property_kind_uuid(part),
|
1943
|
+
facet_type = gridpc.facet_type_for_part(part),
|
1944
|
+
facet = gridpc.facet_for_part(part),
|
1945
|
+
realization = gridpc.realization_for_part(part),
|
1946
|
+
indexable_element = 'cells')
|
1947
|
+
bwpc.write_hdf5_for_imported_list(use_int32 = False)
|
1948
|
+
bwpc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_uuid,
|
1949
|
+
string_lookup_uuid = sl_uuid)
|
1950
|
+
else:
|
1951
|
+
log.debug(
|
1952
|
+
"no properties added - uuids either not 'cell' properties or blocked well is associated with multiple grids"
|
1953
|
+
)
|
2486
1954
|
|
2487
|
-
|
2488
|
-
|
2489
|
-
wam.convert_lengths(cell_axial_vectors[..., 2], grid.crs.z_units, length_uom)
|
2490
|
-
d2 = np.empty(3)
|
2491
|
-
for axis in range(3):
|
2492
|
-
d2[axis] = np.sum(cell_axial_vectors[axis] * cell_axial_vectors[axis])
|
2493
|
-
if radb is None:
|
2494
|
-
radb_e = BlockedWell.__calculate_radb_e(k_ei = k_ei,
|
2495
|
-
k_ej = k_ej,
|
2496
|
-
k_ek = k_ek,
|
2497
|
-
k_i = k_i,
|
2498
|
-
k_j = k_j,
|
2499
|
-
k_k = k_k,
|
2500
|
-
d2 = d2,
|
2501
|
-
sine_anglv = sine_anglv,
|
2502
|
-
cosine_anglv = cosine_anglv,
|
2503
|
-
sine_angla = sine_angla,
|
2504
|
-
cosine_angla = cosine_angla)
|
2505
|
-
radb = radw * radb_e / radw_e
|
2506
|
-
log.debug(f'RADB value calculated in BlockedWell dataframe method as: {radb}')
|
2507
|
-
if wi is None:
|
2508
|
-
wi = 0.0 if radb <= 0.0 else 2.0 * maths.pi / (maths.log(radb / radw) + skin)
|
2509
|
-
if 'WBC' in column_list and wbc is None:
|
2510
|
-
assert length_uom == 'm' or length_uom.startswith('ft'), \
|
2511
|
-
'WBC only calculable for length uom of m or ft*'
|
2512
|
-
conversion_constant = 8.5270171e-5 if length_uom == 'm' else 0.006328286
|
2513
|
-
wbc = conversion_constant * kh * wi # note: pperf aleady accounted for in kh
|
1955
|
+
def _set_cell_interval_map(self):
|
1956
|
+
"""Sets up an index mapping from blocked cell index to interval index, accounting for unblocked intervals."""
|
2514
1957
|
|
2515
|
-
|
1958
|
+
self.cell_interval_map = np.zeros(self.cell_count, dtype = np.int32)
|
1959
|
+
ci = 0
|
1960
|
+
for ii in range(self.node_count - 1):
|
1961
|
+
if self.grid_indices[ii] < 0:
|
1962
|
+
continue
|
1963
|
+
self.cell_interval_map[ci] = ii
|
1964
|
+
ci += 1
|
1965
|
+
assert ci == self.cell_count
|
2516
1966
|
|
2517
|
-
|
2518
|
-
|
2519
|
-
cosine_angla):
|
1967
|
+
def __derive_from_wellspec_check_well_name(self, well_name):
|
1968
|
+
"""Set the well name to be used in the wellspec file."""
|
2520
1969
|
|
2521
|
-
if
|
2522
|
-
|
2523
|
-
radw_e = radw
|
1970
|
+
if well_name:
|
1971
|
+
self.well_name = well_name
|
2524
1972
|
else:
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
r_wj = 0.0 if k_ej == 0.0 else 0.5 * radw * (maths.sqrt(k_ej / k_i) + maths.sqrt(k_ej / k_k))
|
2530
|
-
r_wk = 0.0 if k_ek == 0.0 else 0.5 * radw * (maths.sqrt(k_ek / k_i) + maths.sqrt(k_ek / k_j))
|
2531
|
-
rwi = r_wi * sine_anglv * cosine_angla
|
2532
|
-
rwj = r_wj * sine_anglv * sine_angla
|
2533
|
-
rwk = r_wk * cosine_anglv
|
2534
|
-
radw_e = maths.sqrt(rwi * rwi + rwj * rwj + rwk * rwk)
|
2535
|
-
if radw_e == 0.0:
|
2536
|
-
radw_e = radw # no permeability in this situation anyway
|
2537
|
-
|
2538
|
-
return k_ei, k_ej, k_ek, radw_e
|
1973
|
+
well_name = self.well_name
|
1974
|
+
if not self.title:
|
1975
|
+
self.title = well_name
|
1976
|
+
return well_name
|
2539
1977
|
|
2540
1978
|
@staticmethod
|
2541
|
-
def
|
2542
|
-
|
2543
|
-
|
2544
|
-
|
2545
|
-
|
2546
|
-
|
2547
|
-
|
2548
|
-
|
2549
|
-
radb_e = maths.sqrt(rbi * rbi + rbj * rbj + rbk * rbk)
|
1979
|
+
def __cell_kji0_from_df(df, df_row):
|
1980
|
+
row = df.iloc[df_row]
|
1981
|
+
if pd.isna(row.iloc[0]) or pd.isna(row.iloc[1]) or pd.isna(row.iloc[2]):
|
1982
|
+
return None
|
1983
|
+
cell_kji0 = np.empty((3,), dtype = np.int32)
|
1984
|
+
cell_kji0[:] = row.iloc[2], row.iloc[1], row.iloc[0]
|
1985
|
+
cell_kji0[:] -= 1
|
1986
|
+
return cell_kji0
|
2550
1987
|
|
2551
|
-
|
1988
|
+
@staticmethod
|
1989
|
+
def __verify_grid_name(grid_name_to_check, row, skipped_warning_grid, well_name):
|
1990
|
+
"""Check whether the grid associated with a row of the dataframe matches the expected grid name."""
|
1991
|
+
skip_row = False
|
1992
|
+
if grid_name_to_check and pd.notna(row['GRID']) and grid_name_to_check != str(row['GRID']).upper():
|
1993
|
+
other_grid = str(row['GRID'])
|
1994
|
+
if skipped_warning_grid != other_grid:
|
1995
|
+
log.warning('skipping perforation(s) in grid ' + other_grid + ' for well ' + str(well_name))
|
1996
|
+
skipped_warning_grid = other_grid
|
1997
|
+
skip_row = True
|
1998
|
+
return skipped_warning_grid, skip_row
|
2552
1999
|
|
2553
|
-
|
2554
|
-
|
2555
|
-
|
2000
|
+
@staticmethod
|
2001
|
+
def __calculate_entry_and_exit_axes_polarities_and_points(angles_present, row, cp, well_name, df, i, cell_kji0,
|
2002
|
+
blocked_cells_kji0, use_face_centres, xy_units, z_units):
|
2003
|
+
if angles_present:
|
2004
|
+
entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz = \
|
2005
|
+
BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_angles(
|
2006
|
+
row = row, cp = cp, well_name = well_name, xy_units = xy_units, z_units = z_units)
|
2007
|
+
else:
|
2008
|
+
# fabricate entry and exit axes and polarities based on indices alone
|
2009
|
+
# note: could use geometry but here a cheap rough-and-ready approach is used
|
2010
|
+
log.debug('row ' + str(i) + ': using cell moves')
|
2011
|
+
entry_axis, entry_polarity, exit_axis, exit_polarity = BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_indices(
|
2012
|
+
df = df, i = i, cell_kji0 = cell_kji0, blocked_cells_kji0 = blocked_cells_kji0)
|
2556
2013
|
|
2557
|
-
|
2558
|
-
|
2559
|
-
|
2560
|
-
|
2561
|
-
|
2562
|
-
|
2563
|
-
|
2564
|
-
traj_z_inc_down = traj_z_inc_down,
|
2565
|
-
entry_xyz = entry_xyz,
|
2566
|
-
exit_xyz = exit_xyz,
|
2567
|
-
ee_crs = ee_crs)
|
2568
|
-
xyz = np.array(xyz)
|
2569
|
-
for i, col_header in enumerate(['X', 'Y', 'DEPTH']):
|
2570
|
-
if col_header in pc_titles:
|
2571
|
-
xyz[i] = pc.single_array_ref(citation_title = col_header)[ci]
|
2014
|
+
entry_xyz, exit_xyz = BlockedWell.__override_vector_based_xyz_entry_and_exit_points_if_necessary(
|
2015
|
+
use_face_centres = use_face_centres,
|
2016
|
+
entry_axis = entry_axis,
|
2017
|
+
exit_axis = exit_axis,
|
2018
|
+
entry_polarity = entry_polarity,
|
2019
|
+
exit_polarity = exit_polarity,
|
2020
|
+
cp = cp)
|
2572
2021
|
|
2573
|
-
return
|
2022
|
+
return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
|
2574
2023
|
|
2575
|
-
|
2576
|
-
|
2024
|
+
@staticmethod
|
2025
|
+
def __calculate_entry_and_exit_axes_polarities_and_points_using_angles(row, cp, well_name, xy_units, z_units):
|
2026
|
+
"""Calculate entry and exit axes, polarities and points using azimuth and inclination angles."""
|
2577
2027
|
|
2578
|
-
|
2579
|
-
|
2580
|
-
|
2581
|
-
|
2582
|
-
if depth_inc_down and traj_z_inc_down is False:
|
2583
|
-
xyz[2] = -xyz[2]
|
2028
|
+
angla = row['ANGLA']
|
2029
|
+
inclination = row['ANGLV']
|
2030
|
+
if inclination < 0.001:
|
2031
|
+
azimuth = 0.0
|
2584
2032
|
else:
|
2585
|
-
|
2586
|
-
|
2587
|
-
|
2588
|
-
|
2589
|
-
|
2590
|
-
|
2591
|
-
|
2033
|
+
i_vector = np.sum(cp[:, :, 1] - cp[:, :, 0], axis = (0, 1))
|
2034
|
+
azimuth = vec.azimuth(i_vector) - angla # see Nexus keyword reference doc
|
2035
|
+
well_vector = vec.unit_vector_from_azimuth_and_inclination(azimuth, inclination) * 10000.0
|
2036
|
+
if xy_units != z_units:
|
2037
|
+
well_vector[2] = wam.convert_lengths(well_vector[2], xy_units, z_units)
|
2038
|
+
well_vector = vec.unit_vector(well_vector) * 10000.0
|
2039
|
+
# todo: the following might be producing NaN's when vector passes precisely through an edge
|
2040
|
+
(entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz) = \
|
2041
|
+
rqwu.find_entry_and_exit(cp, -well_vector, well_vector, well_name)
|
2042
|
+
return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
|
2592
2043
|
|
2593
|
-
def
|
2594
|
-
"""Convert the measured depth to the correct units or get the measured depth from the property collection."""
|
2044
|
+
def __calculate_entry_and_exit_axes_polarities_and_points_using_indices(df, i, cell_kji0, blocked_cells_kji0):
|
2595
2045
|
|
2596
|
-
|
2597
|
-
|
2598
|
-
|
2599
|
-
|
2046
|
+
entry_axis, entry_polarity = BlockedWell.__fabricate_entry_axis_and_polarity_using_indices(
|
2047
|
+
i, cell_kji0, blocked_cells_kji0)
|
2048
|
+
exit_axis, exit_polarity = BlockedWell.__fabricate_exit_axis_and_polarity_using_indices(
|
2049
|
+
i, cell_kji0, entry_axis, entry_polarity, df)
|
2600
2050
|
|
2601
|
-
return
|
2051
|
+
return entry_axis, entry_polarity, exit_axis, exit_polarity
|
2602
2052
|
|
2603
2053
|
@staticmethod
|
2604
|
-
def
|
2605
|
-
|
2606
|
-
cell_kji0, row_ci_list, ci):
|
2607
|
-
"""Append the row of data corresponding to the interval to the dataframe."""
|
2608
|
-
|
2609
|
-
column_names = [
|
2610
|
-
'GRID', 'RADW', 'SKIN', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'STAT', 'PPERF', 'RADB',
|
2611
|
-
'WI', 'WBC'
|
2612
|
-
]
|
2613
|
-
column_values = [
|
2614
|
-
grid_name, radw, skin, angla, anglv, length, kh, xyz[2], md, xyz[0], xyz[1], stat, part_perf_fraction, radb,
|
2615
|
-
wi, wbc
|
2616
|
-
]
|
2617
|
-
column_values_dict = dict(zip(column_names, column_values))
|
2054
|
+
def __fabricate_entry_axis_and_polarity_using_indices(i, cell_kji0, blocked_cells_kji0):
|
2055
|
+
"""Fabricate entry and exit axes and polarities based on indices alone.
|
2618
2056
|
|
2619
|
-
|
2620
|
-
|
2621
|
-
|
2622
|
-
if col_index < 3:
|
2623
|
-
if one_based:
|
2624
|
-
row_dict[col] = [cell_kji0[2 - col_index] + 1]
|
2625
|
-
else:
|
2626
|
-
row_dict[col] = [cell_kji0[2 - col_index]]
|
2627
|
-
else:
|
2628
|
-
row_dict[col] = [column_values_dict[col]]
|
2057
|
+
note:
|
2058
|
+
could use geometry but here a cheap rough-and-ready approach is used
|
2059
|
+
"""
|
2629
2060
|
|
2630
|
-
|
2631
|
-
|
2632
|
-
|
2061
|
+
if i == 0:
|
2062
|
+
entry_axis, entry_polarity = 0, 0 # K-
|
2063
|
+
else:
|
2064
|
+
entry_move = cell_kji0 - blocked_cells_kji0[-1]
|
2065
|
+
log.debug(f'entry move: {entry_move}')
|
2066
|
+
if entry_move[1] == 0 and entry_move[2] == 0: # K move
|
2067
|
+
entry_axis = 0
|
2068
|
+
entry_polarity = 0 if entry_move[0] >= 0 else 1
|
2069
|
+
elif abs(entry_move[1]) > abs(entry_move[2]): # J dominant move
|
2070
|
+
entry_axis = 1
|
2071
|
+
entry_polarity = 0 if entry_move[1] >= 0 else 1
|
2072
|
+
else: # I dominant move
|
2073
|
+
entry_axis = 2
|
2074
|
+
entry_polarity = 0 if entry_move[2] >= 0 else 1
|
2075
|
+
return entry_axis, entry_polarity
|
2076
|
+
|
2077
|
+
@staticmethod
|
2078
|
+
def __fabricate_exit_axis_and_polarity_using_indices(i, cell_kji0, entry_axis, entry_polarity, df):
|
2079
|
+
if i == len(df) - 1:
|
2080
|
+
exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
|
2081
|
+
else:
|
2082
|
+
next_cell_kji0 = BlockedWell.__cell_kji0_from_df(df, i + 1)
|
2083
|
+
if next_cell_kji0 is None:
|
2084
|
+
exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
|
2633
2085
|
else:
|
2634
|
-
|
2635
|
-
|
2086
|
+
exit_move = next_cell_kji0 - cell_kji0
|
2087
|
+
log.debug(f'exit move: {exit_move}')
|
2088
|
+
if exit_move[1] == 0 and exit_move[2] == 0: # K move
|
2089
|
+
exit_axis = 0
|
2090
|
+
exit_polarity = 1 if exit_move[0] >= 0 else 0
|
2091
|
+
elif abs(exit_move[1]) > abs(exit_move[2]): # J dominant move
|
2092
|
+
exit_axis = 1
|
2093
|
+
exit_polarity = 1 if exit_move[1] >= 0 else 0
|
2094
|
+
else: # I dominant move
|
2095
|
+
exit_axis = 2
|
2096
|
+
exit_polarity = 1 if exit_move[2] >= 0 else 0
|
2097
|
+
return exit_axis, exit_polarity
|
2636
2098
|
|
2637
|
-
|
2099
|
+
@staticmethod
|
2100
|
+
def __override_vector_based_xyz_entry_and_exit_points_if_necessary(use_face_centres, entry_axis, exit_axis,
|
2101
|
+
entry_polarity, exit_polarity, cp):
|
2102
|
+
"""Override the vector based xyz entry and exit with face centres."""
|
2638
2103
|
|
2639
|
-
|
2104
|
+
if use_face_centres: # override the vector based xyz entry and exit points with face centres
|
2105
|
+
if entry_axis == 0:
|
2106
|
+
entry_xyz = np.mean(cp[entry_polarity, :, :], axis = (0, 1))
|
2107
|
+
elif entry_axis == 1:
|
2108
|
+
entry_xyz = np.mean(cp[:, entry_polarity, :], axis = (0, 1))
|
2109
|
+
else:
|
2110
|
+
entry_xyz = np.mean(cp[:, :, entry_polarity], axis = (0, 1)) # entry_axis == 2, ie. I
|
2111
|
+
if exit_axis == 0:
|
2112
|
+
exit_xyz = np.mean(cp[exit_polarity, :, :], axis = (0, 1))
|
2113
|
+
elif exit_axis == 1:
|
2114
|
+
exit_xyz = np.mean(cp[:, exit_polarity, :], axis = (0, 1))
|
2115
|
+
else:
|
2116
|
+
exit_xyz = np.mean(cp[:, :, exit_polarity], axis = (0, 1)) # exit_axis == 2, ie. I
|
2117
|
+
return entry_xyz, exit_xyz
|
2640
2118
|
|
2641
|
-
|
2642
|
-
|
2643
|
-
|
2644
|
-
|
2645
|
-
|
2646
|
-
|
2647
|
-
|
2648
|
-
|
2119
|
+
@staticmethod
|
2120
|
+
def __add_interval(previous_xyz, entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz,
|
2121
|
+
cell_kji0, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0,
|
2122
|
+
blocked_face_pairs, xy_units, z_units, length_uom):
|
2123
|
+
if previous_xyz is None: # first entry
|
2124
|
+
log.debug('adding mean sea level trajectory start')
|
2125
|
+
previous_xyz = entry_xyz.copy()
|
2126
|
+
previous_xyz[2] = 0.0 # use depth zero as md datum
|
2127
|
+
trajectory_mds.append(0.0)
|
2128
|
+
trajectory_points.append(previous_xyz)
|
2129
|
+
if not vec.isclose(previous_xyz, entry_xyz, tolerance = 0.05): # add an unblocked interval
|
2130
|
+
log.debug('adding unblocked interval')
|
2131
|
+
trajectory_points.append(entry_xyz)
|
2132
|
+
new_md = trajectory_mds[-1] + BlockedWell._md_length(entry_xyz - previous_xyz, xy_units, z_units,
|
2133
|
+
length_uom)
|
2134
|
+
trajectory_mds.append(new_md)
|
2135
|
+
blocked_intervals.append(-1) # unblocked interval
|
2136
|
+
previous_xyz = entry_xyz
|
2137
|
+
log.debug('adding blocked interval for cell kji0: ' + str(cell_kji0))
|
2138
|
+
trajectory_points.append(exit_xyz)
|
2139
|
+
new_md = trajectory_mds[-1] + BlockedWell._md_length(exit_xyz - previous_xyz, xy_units, z_units, length_uom)
|
2140
|
+
trajectory_mds.append(new_md)
|
2141
|
+
blocked_intervals.append(0) # blocked interval
|
2142
|
+
previous_xyz = exit_xyz
|
2143
|
+
blocked_cells_kji0.append(cell_kji0)
|
2144
|
+
blocked_face_pairs.append(((entry_axis, entry_polarity), (exit_axis, exit_polarity)))
|
2649
2145
|
|
2650
|
-
|
2146
|
+
return previous_xyz, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0, blocked_face_pairs
|
2147
|
+
|
2148
|
+
@staticmethod
|
2149
|
+
def _md_length(xyz_vector, xy_units, z_units, length_uom):
|
2150
|
+
if length_uom == xy_units and length_uom == z_units:
|
2151
|
+
return vec.naive_length(xyz_vector)
|
2152
|
+
x = wam.convert_lengths(xyz_vector[0], xy_units, length_uom)
|
2153
|
+
y = wam.convert_lengths(xyz_vector[1], xy_units, length_uom)
|
2154
|
+
z = wam.convert_lengths(xyz_vector[2], z_units, length_uom)
|
2155
|
+
return vec.naive_length((x, y, z))
|
2156
|
+
|
2157
|
+
@staticmethod
|
2158
|
+
def __add_tail_to_trajectory_if_necessary(blocked_count, exit_axis, exit_polarity, cell_kji0, grid,
|
2159
|
+
trajectory_points, trajectory_mds):
|
2160
|
+
"""Add tail to trajcetory if last segment terminates at bottom face in bottom layer."""
|
2161
|
+
|
2162
|
+
if blocked_count > 0 and exit_axis == 0 and exit_polarity == 1 and cell_kji0[
|
2163
|
+
0] == grid.nk - 1 and grid.k_direction_is_down:
|
2164
|
+
tail_length = 10.0 # metres or feet
|
2165
|
+
tail_xyz = trajectory_points[-1].copy()
|
2166
|
+
tail_xyz[2] += tail_length * (1.0 if grid.z_inc_down() else -1.0)
|
2167
|
+
trajectory_points.append(tail_xyz)
|
2168
|
+
new_md = trajectory_mds[-1] + tail_length
|
2169
|
+
trajectory_mds.append(new_md)
|
2170
|
+
|
2171
|
+
return trajectory_points, trajectory_mds
|
2172
|
+
|
2173
|
+
def __add_as_properties_if_specified(self,
|
2174
|
+
add_as_properties,
|
2175
|
+
df,
|
2176
|
+
length_uom,
|
2177
|
+
time_index = None,
|
2178
|
+
time_series_uuid = None):
|
2179
|
+
# if add_as_properties is True and present as a list of wellspec column names, both the blocked well and
|
2180
|
+
# the properties will have their hdf5 data written, xml created and be added as parts to the model
|
2181
|
+
|
2182
|
+
if add_as_properties and len(df.columns) > 3:
|
2183
|
+
# NB: atypical writing of hdf5 data and xml creation in order to support related properties
|
2184
|
+
self.write_hdf5()
|
2185
|
+
self.create_xml()
|
2651
2186
|
if isinstance(add_as_properties, list):
|
2652
2187
|
for col in add_as_properties:
|
2653
|
-
assert col in
|
2188
|
+
assert col in df.columns[3:] # could just skip missing columns
|
2654
2189
|
property_columns = add_as_properties
|
2655
2190
|
else:
|
2656
|
-
property_columns =
|
2191
|
+
property_columns = df.columns[3:]
|
2657
2192
|
self.add_df_properties(df,
|
2658
2193
|
property_columns,
|
2659
2194
|
length_uom = length_uom,
|
2660
2195
|
time_index = time_index,
|
2661
2196
|
time_series_uuid = time_series_uuid)
|
2662
2197
|
|
2663
|
-
def
|
2664
|
-
|
2665
|
-
columns,
|
2666
|
-
length_uom = None,
|
2667
|
-
time_index = None,
|
2668
|
-
time_series_uuid = None,
|
2669
|
-
realization = None):
|
2670
|
-
"""Creates a property part for each column in the dataframe, based on the dataframe values.
|
2198
|
+
def __set_grid(self, grid, wellspec_file, cellio_file, column_ji0):
|
2199
|
+
"""Set the grid to which the blocked well belongs."""
|
2671
2200
|
|
2672
|
-
|
2673
|
-
|
2674
|
-
|
2675
|
-
|
2676
|
-
|
2677
|
-
|
2678
|
-
time_series_uuid (uuid.UUID, optional): if adding a timestamp to the property, this is
|
2679
|
-
the uuid of the TimeSeries object
|
2680
|
-
realization (int, optional): if present, is used as the realization number for all the
|
2681
|
-
properties
|
2201
|
+
if grid is None and (self.trajectory is not None or wellspec_file is not None or cellio_file is not None or
|
2202
|
+
column_ji0 is not None):
|
2203
|
+
grid_final = self.model.grid()
|
2204
|
+
else:
|
2205
|
+
grid_final = grid
|
2206
|
+
return grid_final
|
2682
2207
|
|
2683
|
-
|
2684
|
-
|
2208
|
+
def __check_cellio_init_okay(self, cellio_file, well_name, grid):
|
2209
|
+
"""Checks if BlockedWell object initialization from a cellio file is okay."""
|
2685
2210
|
|
2686
|
-
|
2687
|
-
|
2688
|
-
|
2689
|
-
this method currently only handles single grid situations;
|
2690
|
-
dataframe rows must be in the same order as the cells in the blocked well
|
2691
|
-
"""
|
2692
|
-
# todo: enhance to handle multiple grids
|
2693
|
-
assert len(self.grid_list) == 1
|
2694
|
-
if columns is None or len(columns) == 0 or len(df) == 0:
|
2695
|
-
return
|
2696
|
-
if length_uom is None:
|
2697
|
-
length_uom = self.trajectory.md_uom
|
2698
|
-
extra_pc = rqp.PropertyCollection()
|
2699
|
-
extra_pc.set_support(support = self)
|
2700
|
-
assert len(df) == self.cell_count
|
2211
|
+
okay = self.import_from_rms_cellio(cellio_file, well_name, grid)
|
2212
|
+
if not okay:
|
2213
|
+
self.node_count = 0
|
2701
2214
|
|
2702
|
-
|
2703
|
-
|
2704
|
-
uom, pk, discrete = self._get_uom_pk_discrete_for_df_properties(extra = extra, length_uom = length_uom)
|
2705
|
-
if discrete:
|
2706
|
-
null_value = -1
|
2707
|
-
na_value = -1
|
2708
|
-
dtype = np.int32
|
2709
|
-
else:
|
2710
|
-
null_value = None
|
2711
|
-
na_value = np.NaN
|
2712
|
-
dtype = float
|
2713
|
-
# 'SKIN': use defaults for now; todo: create local property kind for skin
|
2714
|
-
if column == 'STAT':
|
2715
|
-
col_as_list = list(df[column])
|
2716
|
-
expanded = np.array([(0 if (str(st).upper() in ['OFF', '0']) else 1) for st in col_as_list],
|
2717
|
-
dtype = int)
|
2718
|
-
else:
|
2719
|
-
expanded = df[column].to_numpy(dtype = dtype, copy = True, na_value = na_value)
|
2720
|
-
extra_pc.add_cached_array_to_imported_list(
|
2721
|
-
expanded,
|
2722
|
-
'blocked well dataframe',
|
2723
|
-
extra,
|
2724
|
-
discrete = discrete,
|
2725
|
-
uom = uom,
|
2726
|
-
property_kind = pk,
|
2727
|
-
local_property_kind_uuid = None,
|
2728
|
-
facet_type = None,
|
2729
|
-
facet = None,
|
2730
|
-
realization = realization,
|
2731
|
-
indexable_element = 'cells',
|
2732
|
-
count = 1,
|
2733
|
-
time_index = time_index,
|
2734
|
-
null_value = null_value,
|
2735
|
-
)
|
2736
|
-
extra_pc.write_hdf5_for_imported_list()
|
2737
|
-
extra_pc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_series_uuid,
|
2738
|
-
find_local_property_kinds = True)
|
2215
|
+
def _load_from_xml(self):
|
2216
|
+
"""Loads the blocked wellbore object from an xml node (and associated hdf5 data)."""
|
2739
2217
|
|
2740
|
-
|
2741
|
-
|
2742
|
-
# todo: this is horribly inefficient, building a whole dictionary for every call but only using one entry
|
2743
|
-
if length_uom not in ['m', 'ft']:
|
2744
|
-
raise ValueError(f"The length_uom {length_uom} must be either 'm' or 'ft'.")
|
2745
|
-
if extra == 'TEMP' and (temperature_uom is None or
|
2746
|
-
temperature_uom not in wam.valid_uoms('thermodynamic temperature')):
|
2747
|
-
raise ValueError(f"The temperature_uom must be in {wam.valid_uoms('thermodynamic temperature')}.")
|
2218
|
+
node = self.root
|
2219
|
+
assert node is not None
|
2748
2220
|
|
2749
|
-
|
2750
|
-
extra = extra)
|
2751
|
-
uom_pk_discrete_dict = {
|
2752
|
-
'ANGLA': ('dega', 'azimuth', False),
|
2753
|
-
'ANGLV': ('dega', 'inclination', False),
|
2754
|
-
'KH': (f'mD.{length_uom}', 'permeability length', False),
|
2755
|
-
'PPERF': (f'{length_uom}/{length_uom}', 'perforation fraction', False),
|
2756
|
-
'STAT': (None, 'well connection open', True),
|
2757
|
-
'LENGTH': length_uom_pk_discrete,
|
2758
|
-
'MD': (length_uom, 'measured depth', False),
|
2759
|
-
'X': length_uom_pk_discrete,
|
2760
|
-
'Y': length_uom_pk_discrete,
|
2761
|
-
'DEPTH': (length_uom, 'depth', False),
|
2762
|
-
'RADW': (length_uom, 'wellbore radius', False),
|
2763
|
-
'RADB': (length_uom, 'block equivalent radius', False),
|
2764
|
-
'RADBP': length_uom_pk_discrete,
|
2765
|
-
'RADWP': length_uom_pk_discrete,
|
2766
|
-
'FM': (f'{length_uom}/{length_uom}', 'matrix fraction', False),
|
2767
|
-
'IRELPM': (None, 'relative permeability index', True), # TODO: change to 'region initialization' with facet
|
2768
|
-
'SECT': (None, 'wellbore section index', True),
|
2769
|
-
'LAYER': (None, 'layer index', True),
|
2770
|
-
'ANGLE': ('dega', 'plane angle', False),
|
2771
|
-
'TEMP': (temperature_uom, 'thermodynamic temperature', False),
|
2772
|
-
'MDCON': length_uom_pk_discrete,
|
2773
|
-
'K': ('mD', 'rock permeability', False),
|
2774
|
-
'DZ': (length_uom, 'cell length', False), # TODO: add direction facet
|
2775
|
-
'DTOP': (length_uom, 'depth', False),
|
2776
|
-
'DBOT': (length_uom, 'depth', False),
|
2777
|
-
'SKIN': ('Euc', 'skin', False),
|
2778
|
-
'WI': ('Euc', 'well connection index', False),
|
2779
|
-
}
|
2780
|
-
return uom_pk_discrete_dict.get(extra, ('Euc', 'generic continuous', False))
|
2221
|
+
self.__find_trajectory_uuid(node = node)
|
2781
2222
|
|
2782
|
-
|
2783
|
-
|
2784
|
-
|
2785
|
-
|
2786
|
-
|
2787
|
-
|
2788
|
-
|
2789
|
-
|
2223
|
+
self.node_count = rqet.find_tag_int(node, 'NodeCount')
|
2224
|
+
assert self.node_count is not None and self.node_count >= 2, 'problem with blocked well node count'
|
2225
|
+
|
2226
|
+
mds_node = rqet.find_tag(node, 'NodeMd')
|
2227
|
+
assert mds_node is not None, 'blocked well node measured depths hdf5 reference not found in xml'
|
2228
|
+
rqwu.load_hdf5_array(self, mds_node, 'node_mds')
|
2229
|
+
|
2230
|
+
# Statement below has no effect, is this a bug?
|
2231
|
+
self.node_mds is not None and self.node_mds.ndim == 1 and self.node_mds.size == self.node_count
|
2232
|
+
|
2233
|
+
self.cell_count = rqet.find_tag_int(node, 'CellCount')
|
2234
|
+
assert self.cell_count is not None and self.cell_count > 0
|
2235
|
+
|
2236
|
+
# todo: remove this if block once RMS export issue resolved
|
2237
|
+
if self.cell_count == self.node_count:
|
2238
|
+
extended_mds = np.empty((self.node_mds.size + 1,))
|
2239
|
+
extended_mds[:-1] = self.node_mds
|
2240
|
+
extended_mds[-1] = self.node_mds[-1] + 1.0
|
2241
|
+
self.node_mds = extended_mds
|
2242
|
+
self.node_count += 1
|
2243
|
+
|
2244
|
+
assert self.cell_count < self.node_count
|
2245
|
+
|
2246
|
+
unique_grid_indices = self.__find_gi_node_and_load_hdf5_array(node = node)
|
2247
|
+
|
2248
|
+
self.__find_grid_node(node = node, unique_grid_indices = unique_grid_indices)
|
2249
|
+
|
2250
|
+
self.__find_ci_node_and_load_hdf5_array(node = node)
|
2251
|
+
|
2252
|
+
self.__find_fi_node_and_load_hdf5_array(node)
|
2253
|
+
|
2254
|
+
interp_uuid = rqet.find_nested_tags_text(node, ['RepresentedInterpretation', 'UUID'])
|
2255
|
+
if interp_uuid is None:
|
2256
|
+
self.wellbore_interpretation = None
|
2790
2257
|
else:
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2258
|
+
self.wellbore_interpretation = rqo.WellboreInterpretation(self.model, uuid = interp_uuid)
|
2259
|
+
|
2260
|
+
# Set up matches between cell_indices and grid_indices
|
2261
|
+
self.cell_grid_link = self.map_cell_and_grid_indices()
|
2262
|
+
|
2263
|
+
def __find_trajectory_uuid(self, node):
|
2264
|
+
"""Find and verify the uuid of the trajectory associated with the BlockedWell object."""
|
2265
|
+
|
2266
|
+
trajectory_uuid = bu.uuid_from_string(rqet.find_nested_tags_text(node, ['Trajectory', 'UUID']))
|
2267
|
+
assert trajectory_uuid is not None, 'blocked well trajectory reference not found in xml'
|
2268
|
+
if self.trajectory is None:
|
2269
|
+
self.trajectory = rqw.Trajectory(self.model, uuid = trajectory_uuid)
|
2794
2270
|
else:
|
2795
|
-
|
2796
|
-
return uom, pk, False
|
2271
|
+
assert bu.matching_uuids(self.trajectory.uuid, trajectory_uuid), 'blocked well trajectory uuid mismatch'
|
2797
2272
|
|
2798
|
-
def
|
2799
|
-
|
2800
|
-
perm_i_uuid = None,
|
2801
|
-
perm_j_uuid = None,
|
2802
|
-
perm_k_uuid = None,
|
2803
|
-
satw_uuid = None,
|
2804
|
-
sato_uuid = None,
|
2805
|
-
satg_uuid = None,
|
2806
|
-
region_uuid = None,
|
2807
|
-
active_only = False,
|
2808
|
-
min_k0 = None,
|
2809
|
-
max_k0 = None,
|
2810
|
-
k0_list = None,
|
2811
|
-
min_length = None,
|
2812
|
-
min_kh = None,
|
2813
|
-
max_depth = None,
|
2814
|
-
max_satw = None,
|
2815
|
-
min_sato = None,
|
2816
|
-
max_satg = None,
|
2817
|
-
perforation_list = None,
|
2818
|
-
region_list = None,
|
2819
|
-
set_k_face_intervals_vertical = False,
|
2820
|
-
anglv_ref = 'gravity',
|
2821
|
-
angla_plane_ref = None,
|
2822
|
-
length_mode = 'MD',
|
2823
|
-
length_uom = None,
|
2824
|
-
use_face_centres = False,
|
2825
|
-
preferential_perforation = True):
|
2826
|
-
"""Returns the total static K.H (permeability x height).
|
2273
|
+
def __find_ci_node_and_load_hdf5_array(self, node):
|
2274
|
+
"""Find the BlockedWell object's cell indices hdf5 reference node and load the array."""
|
2827
2275
|
|
2828
|
-
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2276
|
+
ci_node = rqet.find_tag(node, 'CellIndices')
|
2277
|
+
assert ci_node is not None, 'blocked well cell indices hdf5 reference not found in xml'
|
2278
|
+
rqwu.load_hdf5_array(self, ci_node, 'cell_indices', dtype = self.cell_index_dtype)
|
2279
|
+
assert (self.cell_indices is not None and self.cell_indices.ndim == 1 and
|
2280
|
+
self.cell_indices.size == self.cell_count), 'mismatch in number of cell indices for blocked well'
|
2281
|
+
self.cellind_null = rqet.find_tag_int(ci_node, 'NullValue')
|
2282
|
+
if self.cellind_null is None:
|
2283
|
+
self.cellind_null = -1 # if no Null found assume -1 default
|
2832
2284
|
|
2833
|
-
|
2834
|
-
|
2835
|
-
k_col = 'K',
|
2836
|
-
one_based = False,
|
2837
|
-
extra_columns_list = ['KH'],
|
2838
|
-
ntg_uuid = ntg_uuid,
|
2839
|
-
perm_i_uuid = perm_i_uuid,
|
2840
|
-
perm_j_uuid = perm_j_uuid,
|
2841
|
-
perm_k_uuid = perm_k_uuid,
|
2842
|
-
satw_uuid = satw_uuid,
|
2843
|
-
sato_uuid = sato_uuid,
|
2844
|
-
satg_uuid = satg_uuid,
|
2845
|
-
region_uuid = region_uuid,
|
2846
|
-
active_only = active_only,
|
2847
|
-
min_k0 = min_k0,
|
2848
|
-
max_k0 = max_k0,
|
2849
|
-
k0_list = k0_list,
|
2850
|
-
min_length = min_length,
|
2851
|
-
min_kh = min_kh,
|
2852
|
-
max_depth = max_depth,
|
2853
|
-
max_satw = max_satw,
|
2854
|
-
min_sato = min_sato,
|
2855
|
-
max_satg = max_satg,
|
2856
|
-
perforation_list = perforation_list,
|
2857
|
-
region_list = region_list,
|
2858
|
-
set_k_face_intervals_vertical = set_k_face_intervals_vertical,
|
2859
|
-
anglv_ref = anglv_ref,
|
2860
|
-
angla_plane_ref = angla_plane_ref,
|
2861
|
-
length_mode = length_mode,
|
2862
|
-
length_uom = length_uom,
|
2863
|
-
use_face_centres = use_face_centres,
|
2864
|
-
preferential_perforation = preferential_perforation)
|
2285
|
+
def __find_fi_node_and_load_hdf5_array(self, node):
|
2286
|
+
"""Find the BlockedWell object's face indices hdf5 reference node and load the array."""
|
2865
2287
|
|
2866
|
-
|
2288
|
+
fi_node = rqet.find_tag(node, 'LocalFacePairPerCellIndices')
|
2289
|
+
assert fi_node is not None, 'blocked well face indices hdf5 reference not found in xml'
|
2290
|
+
rqwu.load_hdf5_array(self, fi_node, 'raw_face_indices', dtype = np.int8)
|
2291
|
+
assert self.raw_face_indices is not None, 'failed to load face indices for blocked well'
|
2292
|
+
assert self.raw_face_indices.size == 2 * self.cell_count, 'mismatch in number of cell faces for blocked well'
|
2293
|
+
if self.raw_face_indices.ndim > 1:
|
2294
|
+
self.raw_face_indices = self.raw_face_indices.reshape((self.raw_face_indices.size,))
|
2295
|
+
mask = np.where(self.raw_face_indices == -1)
|
2296
|
+
self.raw_face_indices[mask] = 0
|
2297
|
+
self.face_pair_indices = self.face_index_inverse_map[self.raw_face_indices]
|
2298
|
+
self.face_pair_indices[mask] = (-1, -1)
|
2299
|
+
self.face_pair_indices = self.face_pair_indices.reshape((-1, 2, 2))
|
2300
|
+
del self.raw_face_indices
|
2301
|
+
self.facepair_null = rqet.find_tag_int(fi_node, 'NullValue')
|
2302
|
+
if self.facepair_null is None:
|
2303
|
+
self.facepair_null = -1
|
2867
2304
|
|
2868
|
-
def
|
2869
|
-
|
2870
|
-
well_name = None,
|
2871
|
-
mode = 'a',
|
2872
|
-
extra_columns_list = [],
|
2873
|
-
ntg_uuid = None,
|
2874
|
-
perm_i_uuid = None,
|
2875
|
-
perm_j_uuid = None,
|
2876
|
-
perm_k_uuid = None,
|
2877
|
-
satw_uuid = None,
|
2878
|
-
sato_uuid = None,
|
2879
|
-
satg_uuid = None,
|
2880
|
-
region_uuid = None,
|
2881
|
-
radw = None,
|
2882
|
-
skin = None,
|
2883
|
-
stat = None,
|
2884
|
-
active_only = False,
|
2885
|
-
min_k0 = None,
|
2886
|
-
max_k0 = None,
|
2887
|
-
k0_list = None,
|
2888
|
-
min_length = None,
|
2889
|
-
min_kh = None,
|
2890
|
-
max_depth = None,
|
2891
|
-
max_satw = None,
|
2892
|
-
min_sato = None,
|
2893
|
-
max_satg = None,
|
2894
|
-
perforation_list = None,
|
2895
|
-
region_list = None,
|
2896
|
-
set_k_face_intervals_vertical = False,
|
2897
|
-
depth_inc_down = True,
|
2898
|
-
anglv_ref = 'gravity',
|
2899
|
-
angla_plane_ref = None,
|
2900
|
-
length_mode = 'MD',
|
2901
|
-
length_uom = None,
|
2902
|
-
preferential_perforation = True,
|
2903
|
-
space_instead_of_tab_separator = True,
|
2904
|
-
align_columns = True,
|
2905
|
-
preceeding_blank_lines = 0,
|
2906
|
-
trailing_blank_lines = 0,
|
2907
|
-
length_uom_comment = False,
|
2908
|
-
write_nexus_units = True,
|
2909
|
-
float_format = '5.3',
|
2910
|
-
use_properties = False,
|
2911
|
-
property_time_index = None):
|
2912
|
-
"""Writes Nexus WELLSPEC keyword to an ascii file.
|
2305
|
+
def __find_gi_node_and_load_hdf5_array(self, node):
|
2306
|
+
"""Find the BlockedWell object's grid indices hdf5 reference node and load the array."""
|
2913
2307
|
|
2914
|
-
|
2915
|
-
|
2308
|
+
gi_node = rqet.find_tag(node, 'GridIndices')
|
2309
|
+
assert gi_node is not None, 'blocked well grid indices hdf5 reference not found in xml'
|
2310
|
+
rqwu.load_hdf5_array(self, gi_node, 'grid_indices', dtype = np.int32)
|
2311
|
+
# assert self.grid_indices is not None and self.grid_indices.ndim == 1 and self.grid_indices.size == self.node_count - 1
|
2312
|
+
# temporary code to handle blocked wells with incorrectly shaped grid indices wrt. nodes
|
2313
|
+
assert self.grid_indices is not None and self.grid_indices.ndim == 1
|
2314
|
+
if self.grid_indices.size != self.node_count - 1:
|
2315
|
+
if self.grid_indices.size == self.cell_count and self.node_count == 2 * self.cell_count:
|
2316
|
+
log.warning(f'handling node duplication or missing unblocked intervals in blocked well: {self.title}')
|
2317
|
+
expanded_grid_indices = np.full(self.node_count - 1, -1, dtype = np.int32)
|
2318
|
+
expanded_grid_indices[::2] = self.grid_indices
|
2319
|
+
self.grid_indices = expanded_grid_indices
|
2320
|
+
else:
|
2321
|
+
raise ValueError(
|
2322
|
+
f'incorrect grid indices size with respect to node count in blocked well: {self.title}')
|
2323
|
+
# end of temporary code
|
2324
|
+
unique_grid_indices = np.unique(self.grid_indices) # sorted list of unique values
|
2325
|
+
self.gridind_null = rqet.find_tag_int(gi_node, 'NullValue')
|
2326
|
+
if self.gridind_null is None:
|
2327
|
+
self.gridind_null = -1 # if no Null found assume -1 default
|
2328
|
+
return unique_grid_indices
|
2916
2329
|
|
2917
|
-
|
2918
|
-
|
2919
|
-
align_columns and float_format arguments are deprecated and no longer used
|
2920
|
-
"""
|
2330
|
+
def __find_grid_node(self, node, unique_grid_indices):
|
2331
|
+
"""Find the BlockedWell object's grid reference node(s)."""
|
2921
2332
|
|
2922
|
-
|
2333
|
+
grid_node_list = rqet.list_of_tag(node, 'Grid')
|
2334
|
+
assert len(grid_node_list) > 0, 'blocked well grid reference(s) not found in xml'
|
2335
|
+
assert unique_grid_indices[0] >= -1 and unique_grid_indices[-1] < len(
|
2336
|
+
grid_node_list), 'blocked well grid index out of range'
|
2337
|
+
assert np.count_nonzero(
|
2338
|
+
self.grid_indices >= 0) == self.cell_count, 'mismatch in number of blocked well intervals'
|
2339
|
+
self.grid_list = []
|
2340
|
+
for grid_ref_node in grid_node_list:
|
2341
|
+
grid_node = self.model.referenced_node(grid_ref_node)
|
2342
|
+
assert grid_node is not None, 'grid referenced in blocked well xml is not present in model'
|
2343
|
+
grid_uuid = rqet.uuid_for_part_root(grid_node)
|
2344
|
+
grid_obj = self.model.grid(uuid = grid_uuid, find_properties = False)
|
2345
|
+
self.grid_list.append(grid_obj)
|
2346
|
+
if grid_obj.is_big():
|
2347
|
+
self.cell_index_dtype = np.int64
|
2923
2348
|
|
2924
|
-
|
2925
|
-
|
2926
|
-
|
2927
|
-
|
2928
|
-
|
2929
|
-
|
2930
|
-
|
2931
|
-
'
|
2932
|
-
'
|
2933
|
-
|
2934
|
-
'
|
2935
|
-
|
2936
|
-
|
2937
|
-
|
2938
|
-
|
2939
|
-
|
2940
|
-
|
2349
|
+
@staticmethod
|
2350
|
+
def __verify_header_lines_in_cellio_file(fp, well_name, cellio_file):
|
2351
|
+
"""Find and verify the information in the header lines for the specified well in the RMS cellio file."""
|
2352
|
+
|
2353
|
+
while True:
|
2354
|
+
kf.skip_blank_lines_and_comments(fp)
|
2355
|
+
line = fp.readline() # file format version number?
|
2356
|
+
assert line, 'well ' + str(well_name) + ' not found in file ' + str(cellio_file)
|
2357
|
+
fp.readline() # 'Undefined'
|
2358
|
+
words = fp.readline().split()
|
2359
|
+
assert len(words), 'missing header info in cell I/O file'
|
2360
|
+
if words[0].upper() == well_name.upper():
|
2361
|
+
break
|
2362
|
+
while not kf.blank_line(fp):
|
2363
|
+
fp.readline() # skip to block of data for next well
|
2364
|
+
header_lines = int(fp.readline().strip())
|
2365
|
+
for _ in range(header_lines):
|
2366
|
+
fp.readline()
|
2367
|
+
|
2368
|
+
@staticmethod
|
2369
|
+
def __parse_non_blank_line_in_cellio_file(line, grid, cellio_z_inc_down, grid_z_inc_down):
|
2370
|
+
"""Parse each non-blank line in the RMS cellio file for the relevant parameters."""
|
2371
|
+
|
2372
|
+
words = line.split()
|
2373
|
+
assert len(words) >= 9, 'not enough items on data line in cell I/O file, minimum 9 expected'
|
2374
|
+
i1, j1, k1 = int(words[0]), int(words[1]), int(words[2])
|
2375
|
+
cell_kji0 = np.array((k1 - 1, j1 - 1, i1 - 1), dtype = np.int32)
|
2376
|
+
assert np.all(0 <= cell_kji0) and np.all(
|
2377
|
+
cell_kji0 < grid.extent_kji), 'cell I/O cell index not within grid extent'
|
2378
|
+
entry_xyz = np.array((float(words[3]), float(words[4]), float(words[5])))
|
2379
|
+
exit_xyz = np.array((float(words[6]), float(words[7]), float(words[8])))
|
2380
|
+
if cellio_z_inc_down is None:
|
2381
|
+
cellio_z_inc_down = bool(entry_xyz[2] + exit_xyz[2] > 0.0)
|
2382
|
+
if cellio_z_inc_down != grid_z_inc_down:
|
2383
|
+
entry_xyz[2] = -entry_xyz[2]
|
2384
|
+
exit_xyz[2] = -exit_xyz[2]
|
2385
|
+
return cell_kji0, entry_xyz, exit_xyz
|
2386
|
+
|
2387
|
+
@staticmethod
|
2388
|
+
def __calculate_cell_cp_center_and_vectors(grid, cell_kji0, entry_xyz, exit_xyz, well_name):
|
2389
|
+
# calculate the i,j,k coordinates that represent the corner points and center of a perforation cell
|
2390
|
+
# calculate the entry and exit vectors for the perforation cell
|
2391
|
+
|
2392
|
+
cp = grid.corner_points(cell_kji0 = cell_kji0, cache_resqml_array = False)
|
2393
|
+
assert not np.any(np.isnan(
|
2394
|
+
cp)), 'missing geometry for perforation cell(kji0) ' + str(cell_kji0) + ' for well ' + str(well_name)
|
2395
|
+
cell_centre = np.mean(cp, axis = (0, 1, 2))
|
2396
|
+
# let's hope everything is in the same coordinate reference system!
|
2397
|
+
entry_vector = 100.0 * (entry_xyz - cell_centre)
|
2398
|
+
exit_vector = 100.0 * (exit_xyz - cell_centre)
|
2399
|
+
return cp, cell_centre, entry_vector, exit_vector
|
2400
|
+
|
2401
|
+
@staticmethod
|
2402
|
+
def __check_number_of_blocked_well_intervals(blocked_cells_kji0, well_name, grid_name):
|
2403
|
+
"""Check that at least one interval is blocked for the specified well."""
|
2404
|
+
|
2405
|
+
blocked_count = len(blocked_cells_kji0)
|
2406
|
+
if blocked_count == 0:
|
2407
|
+
log.warning(f"No intervals blocked for well {well_name} in grid"
|
2408
|
+
f"{f' {grid_name}' if grid_name is not None else ''}.")
|
2409
|
+
return None
|
2410
|
+
else:
|
2411
|
+
log.info(f"{blocked_count} interval{rqwu._pl(blocked_count)} blocked for well {well_name} in"
|
2412
|
+
f" grid{f' {grid_name}' if grid_name is not None else ''}.")
|
2413
|
+
|
2414
|
+
def __get_interval_count(self):
|
2415
|
+
"""Get the number of intervals to be added to the dataframe."""
|
2416
|
+
|
2417
|
+
if self.node_count is None or self.node_count < 2:
|
2418
|
+
interval_count = 0
|
2419
|
+
else:
|
2420
|
+
interval_count = self.node_count - 1
|
2421
|
+
|
2422
|
+
return interval_count
|
2423
|
+
|
2424
|
+
@staticmethod
|
2425
|
+
def __prop_array(uuid_or_dict, grid):
|
2426
|
+
assert uuid_or_dict is not None and grid is not None
|
2427
|
+
if isinstance(uuid_or_dict, dict):
|
2428
|
+
prop_uuid = uuid_or_dict[grid.uuid]
|
2429
|
+
else:
|
2430
|
+
prop_uuid = uuid_or_dict # uuid either in form of string or uuid.UUID
|
2431
|
+
return grid.property_collection.single_array_ref(uuid = prop_uuid)
|
2432
|
+
|
2433
|
+
@staticmethod
|
2434
|
+
def __get_ref_vector(grid, grid_crs, cell_kji0, mode):
|
2435
|
+
# returns unit vector with true direction, ie. accounts for differing xy & z units in grid's crs
|
2436
|
+
# gravity = np.array((0.0, 0.0, 1.0))
|
2437
|
+
if mode == 'normal well i+':
|
2438
|
+
return None # ANGLA only: option for no projection onto a plane
|
2439
|
+
ref_vector = None
|
2440
|
+
# options for anglv or angla reference: 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down'
|
2441
|
+
if mode == 'z+':
|
2442
|
+
ref_vector = np.array((0.0, 0.0, 1.0))
|
2443
|
+
elif mode == 'z down':
|
2444
|
+
if grid_crs.z_inc_down:
|
2445
|
+
ref_vector = np.array((0.0, 0.0, 1.0))
|
2446
|
+
else:
|
2447
|
+
ref_vector = np.array((0.0, 0.0, -1.0))
|
2448
|
+
else:
|
2449
|
+
cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
|
2450
|
+
if grid_crs.xy_units != grid_crs.z_units:
|
2451
|
+
wam.convert_lengths(cell_axial_vectors[..., 2], grid_crs.z_units, grid_crs.xy_units)
|
2452
|
+
if mode in ['k+', 'k down']:
|
2453
|
+
ref_vector = vec.unit_vector(cell_axial_vectors[0])
|
2454
|
+
if mode == 'k down' and not grid.k_direction_is_down:
|
2455
|
+
ref_vector = -ref_vector
|
2456
|
+
else: # normal to plane of ij axes
|
2457
|
+
ref_vector = vec.unit_vector(vec.cross_product(cell_axial_vectors[1], cell_axial_vectors[2]))
|
2458
|
+
if mode == 'normal ij down':
|
2459
|
+
if grid_crs.z_inc_down:
|
2460
|
+
if ref_vector[2] < 0.0:
|
2461
|
+
ref_vector = -ref_vector
|
2462
|
+
else:
|
2463
|
+
if ref_vector[2] > 0.0:
|
2464
|
+
ref_vector = -ref_vector
|
2465
|
+
if ref_vector is None or ref_vector[2] == 0.0:
|
2466
|
+
if grid_crs.z_inc_down:
|
2467
|
+
ref_vector = np.array((0.0, 0.0, 1.0))
|
2468
|
+
else:
|
2469
|
+
ref_vector = np.array((0.0, 0.0, -1.0))
|
2470
|
+
return ref_vector
|
2471
|
+
|
2472
|
+
@staticmethod
|
2473
|
+
def __verify_angle_references(anglv_ref, angla_plane_ref):
|
2474
|
+
"""Verify that the references for anglv and angla are one of the acceptable options."""
|
2475
|
+
|
2476
|
+
assert anglv_ref in ['gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down']
|
2477
|
+
if anglv_ref == 'gravity':
|
2478
|
+
anglv_ref = 'z down'
|
2479
|
+
if angla_plane_ref is None:
|
2480
|
+
angla_plane_ref = anglv_ref
|
2481
|
+
assert angla_plane_ref in [
|
2482
|
+
'gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down', 'normal well i+'
|
2483
|
+
]
|
2484
|
+
if angla_plane_ref == 'gravity':
|
2485
|
+
angla_plane_ref = 'z down'
|
2486
|
+
return anglv_ref, angla_plane_ref
|
2487
|
+
|
2488
|
+
@staticmethod
|
2489
|
+
def __verify_saturation_ranges_and_property_uuids(max_satw, min_sato, max_satg, satw_uuid, sato_uuid, satg_uuid):
|
2490
|
+
# verify that the fluid saturation limits fall within 0.0 to 1.0 and that the uuid of the required
|
2491
|
+
# saturation property array has been specified.
|
2492
|
+
|
2493
|
+
if max_satw is not None and max_satw >= 1.0:
|
2494
|
+
max_satw = None
|
2495
|
+
if min_sato is not None and min_sato <= 0.0:
|
2496
|
+
min_sato = None
|
2497
|
+
if max_satg is not None and max_satg >= 1.0:
|
2498
|
+
max_satg = None
|
2499
|
+
|
2500
|
+
phase_list = ['water', 'oil', 'gas']
|
2501
|
+
phase_saturation_limits_list = [max_satw, min_sato, max_satg]
|
2502
|
+
uuids_list = [satw_uuid, sato_uuid, satg_uuid]
|
2503
|
+
|
2504
|
+
for phase, phase_limit, uuid in zip(phase_list, phase_saturation_limits_list, uuids_list):
|
2505
|
+
if phase_limit is not None:
|
2506
|
+
assert uuid is not None, f'{phase} saturation limit specified without saturation property array'
|
2507
|
+
|
2508
|
+
return max_satw, min_sato, max_satg
|
2509
|
+
|
2510
|
+
@staticmethod
|
2511
|
+
def __verify_extra_properties_to_be_added_to_dataframe(extra_columns_list, column_list, add_as_properties,
|
2512
|
+
use_properties, skin, stat, radw):
|
2513
|
+
"""Determine which extra columns, if any, should be added as properties to the dataframe.
|
2514
|
+
|
2515
|
+
note:
|
2516
|
+
if skin, stat or radw are None, default values are specified
|
2517
|
+
"""
|
2518
|
+
|
2519
|
+
if extra_columns_list:
|
2520
|
+
for extra in extra_columns_list:
|
2521
|
+
assert extra.upper() in [
|
2522
|
+
'GRID', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'PPERF', 'RADB',
|
2523
|
+
'WI', 'WBC', 'STAT'
|
2524
|
+
]
|
2525
|
+
column_list.append(extra.upper())
|
2526
|
+
else:
|
2527
|
+
add_as_properties = use_properties = False
|
2528
|
+
assert not (add_as_properties and use_properties)
|
2529
|
+
|
2530
|
+
column_list, skin, stat, radw = BlockedWell.__check_skin_stat_radw_to_be_added_as_properties(
|
2531
|
+
skin = skin, stat = stat, radw = radw, column_list = column_list)
|
2532
|
+
|
2533
|
+
return column_list, add_as_properties, use_properties, skin, stat, radw
|
2534
|
+
|
2535
|
+
@staticmethod
|
2536
|
+
def __check_perforation_properties_to_be_added(column_list, perforation_list):
|
2537
|
+
|
2538
|
+
if all(['LENGTH' in column_list, 'PPERF' in column_list, 'KH' not in column_list, perforation_list
|
2539
|
+
is not None]):
|
2540
|
+
log.warning(
|
2541
|
+
'both LENGTH and PPERF will include effects of partial perforation; only one should be used in WELLSPEC'
|
2542
|
+
)
|
2543
|
+
elif all([
|
2544
|
+
perforation_list is not None, 'LENGTH' not in column_list, 'PPERF' not in column_list, 'KH'
|
2545
|
+
not in column_list, 'WBC' not in column_list
|
2546
|
+
]):
|
2547
|
+
log.warning('perforation list supplied but no use of LENGTH, KH, PPERF nor WBC')
|
2548
|
+
|
2549
|
+
if perforation_list is not None and len(perforation_list) == 0:
|
2550
|
+
log.warning('empty perforation list specified for blocked well dataframe: no rows will be included')
|
2551
|
+
|
2552
|
+
@staticmethod
|
2553
|
+
def __check_skin_stat_radw_to_be_added_as_properties(skin, stat, radw, column_list):
|
2554
|
+
"""Verify whether skin should be added as a property in the dataframe."""
|
2555
|
+
|
2556
|
+
if skin is not None and 'SKIN' not in column_list:
|
2557
|
+
column_list.append('SKIN')
|
2558
|
+
|
2559
|
+
if stat is not None:
|
2560
|
+
assert str(stat).upper() in ['ON', 'OFF']
|
2561
|
+
stat = str(stat).upper()
|
2562
|
+
if 'STAT' not in column_list:
|
2563
|
+
column_list.append('STAT')
|
2564
|
+
|
2565
|
+
|
2566
|
+
# else:
|
2567
|
+
# stat = 'ON'
|
2568
|
+
|
2569
|
+
if radw is not None and 'RADW' not in column_list:
|
2570
|
+
column_list.append('RADW')
|
2571
|
+
|
2572
|
+
return column_list, skin, stat, radw
|
2573
|
+
|
2574
|
+
@staticmethod
|
2575
|
+
def __verify_perm_i_uuid_for_well_inflow(column_list, perm_i_uuid, pc_titles):
|
2576
|
+
# Verify that the I direction permeability has been specified if well inflow properties are to be added
|
2577
|
+
# to the dataframe.
|
2578
|
+
|
2579
|
+
do_well_inflow = (('WI' in column_list and 'WI' not in pc_titles) or
|
2580
|
+
('WBC' in column_list and 'WBC' not in pc_titles) or
|
2581
|
+
('RADB' in column_list and 'RADB' not in pc_titles))
|
2582
|
+
if do_well_inflow:
|
2583
|
+
assert perm_i_uuid is not None, 'WI, RADB or WBC requested without I direction permeabilty being specified'
|
2584
|
+
|
2585
|
+
return do_well_inflow
|
2586
|
+
|
2587
|
+
@staticmethod
|
2588
|
+
def __verify_perm_i_uuid_for_kh(min_kh, column_list, perm_i_uuid, pc_titles):
|
2589
|
+
# verify that the I direction permeability has been specified if permeability thickness and
|
2590
|
+
# wellbore constant properties are to be added to the dataframe.
|
2591
|
+
|
2592
|
+
if min_kh is not None and min_kh <= 0.0:
|
2593
|
+
min_kh = None
|
2594
|
+
doing_kh = False
|
2595
|
+
if ('KH' in column_list or min_kh is not None) and 'KH' not in pc_titles:
|
2596
|
+
assert perm_i_uuid is not None, 'KH requested (or minimum specified) without I direction permeabilty being specified'
|
2597
|
+
doing_kh = True
|
2598
|
+
if 'WBC' in column_list and 'WBC' not in pc_titles:
|
2599
|
+
assert perm_i_uuid is not None, 'WBC requested without I direction permeabilty being specified'
|
2600
|
+
doing_kh = True
|
2601
|
+
|
2602
|
+
return min_kh, doing_kh
|
2603
|
+
|
2604
|
+
@staticmethod
|
2605
|
+
def __verify_perm_j_k_uuids_for_kh_and_well_inflow(doing_kh, do_well_inflow, perm_i_uuid, perm_j_uuid, perm_k_uuid):
|
2606
|
+
# verify that the J and K direction permeabilities have been specified if well inflow properties or
|
2607
|
+
# permeability thickness properties are to be added to the dataframe.
|
2608
|
+
|
2609
|
+
isotropic_perm = None
|
2610
|
+
if doing_kh or do_well_inflow:
|
2611
|
+
if perm_j_uuid is None and perm_k_uuid is None:
|
2612
|
+
isotropic_perm = True
|
2613
|
+
else:
|
2614
|
+
if perm_j_uuid is None:
|
2615
|
+
perm_j_uuid = perm_i_uuid
|
2616
|
+
if perm_k_uuid is None:
|
2617
|
+
perm_k_uuid = perm_i_uuid
|
2618
|
+
# following line assumes arguments are passed in same form; if not, some unnecessary maths might be done
|
2619
|
+
isotropic_perm = (bu.matching_uuids(perm_i_uuid, perm_j_uuid) and
|
2620
|
+
bu.matching_uuids(perm_i_uuid, perm_k_uuid))
|
2621
|
+
|
2622
|
+
return perm_j_uuid, perm_k_uuid, isotropic_perm
|
2623
|
+
|
2624
|
+
@staticmethod
|
2625
|
+
def __verify_k_layers_to_be_included(min_k0, max_k0, k0_list):
|
2626
|
+
# verify that the k layers to be included in the dataframe exist within the appropriate range
|
2627
|
+
|
2628
|
+
if min_k0 is None:
|
2629
|
+
min_k0 = 0
|
2630
|
+
else:
|
2631
|
+
assert min_k0 >= 0
|
2632
|
+
if max_k0 is not None:
|
2633
|
+
assert min_k0 <= max_k0
|
2634
|
+
if k0_list is not None and len(k0_list) == 0:
|
2635
|
+
log.warning('no layers included for blocked well dataframe: no rows will be included')
|
2636
|
+
|
2637
|
+
@staticmethod
|
2638
|
+
def __verify_if_angles_xyz_and_length_to_be_added(column_list, pc_titles, doing_kh, do_well_inflow, length_mode):
|
2639
|
+
# determine if angla, anglv, x, y, z and length data are to be added as properties to the dataframe
|
2640
|
+
|
2641
|
+
doing_angles = any([('ANGLA' in column_list and 'ANGLA' not in pc_titles),
|
2642
|
+
('ANGLV' in column_list and 'ANGLV' not in pc_titles), (doing_kh), (do_well_inflow)])
|
2643
|
+
doing_xyz = any([('X' in column_list and 'X' not in pc_titles), ('Y' in column_list and 'Y' not in pc_titles),
|
2644
|
+
('DEPTH' in column_list and 'DEPTH' not in pc_titles)])
|
2645
|
+
doing_entry_exit = any([(doing_angles),
|
2646
|
+
('LENGTH' in column_list and 'LENGTH' not in pc_titles and length_mode == 'straight')])
|
2647
|
+
|
2648
|
+
# doing_angles = (('ANGLA' in column_list and 'ANGLA' not in pc_titles) or
|
2649
|
+
# ('ANGLV' in column_list and 'ANGLV' not in pc_titles) or doing_kh or do_well_inflow)
|
2650
|
+
# doing_xyz = (('X' in column_list and 'X' not in pc_titles) or (
|
2651
|
+
# 'Y' in column_list and 'Y' not in pc_titles) or
|
2652
|
+
# ('DEPTH' in column_list and 'DEPTH' not in pc_titles))
|
2653
|
+
# doing_entry_exit = doing_angles or ('LENGTH' in column_list and 'LENGTH' not in pc_titles and
|
2654
|
+
# length_mode == 'straight')
|
2655
|
+
|
2656
|
+
return doing_angles, doing_xyz, doing_entry_exit
|
2657
|
+
|
2658
|
+
def __verify_number_of_grids_and_crs_units(self, column_list):
|
2659
|
+
# verify that a GRID column is included in the dataframe if the well intersects more than one grid
|
2660
|
+
# verify that each grid's crs units are consistent in all directions
|
2661
|
+
|
2662
|
+
if 'GRID' not in column_list and self.number_of_grids() > 1:
|
2663
|
+
log.error('creating blocked well dataframe without GRID column for well that intersects more than one grid')
|
2664
|
+
grid_crs_list = []
|
2665
|
+
for grid in self.grid_list:
|
2666
|
+
grid_crs = crs.Crs(self.model, uuid = grid.crs_uuid)
|
2667
|
+
grid_crs_list.append(grid_crs)
|
2668
|
+
return grid_crs_list
|
2669
|
+
|
2670
|
+
def __get_trajectory_crs_and_z_inc_down(self):
|
2671
|
+
|
2672
|
+
if self.trajectory is None or self.trajectory.crs_uuid is None:
|
2673
|
+
traj_crs = None
|
2674
|
+
traj_z_inc_down = None
|
2675
|
+
else:
|
2676
|
+
traj_crs = crs.Crs(self.trajectory.model, uuid = self.trajectory.crs_uuid)
|
2677
|
+
traj_z_inc_down = traj_crs.z_inc_down
|
2678
|
+
|
2679
|
+
return traj_crs, traj_z_inc_down
|
2680
|
+
|
2681
|
+
@staticmethod
|
2682
|
+
def __check_cell_depth(max_depth, grid, cell_kji0, grid_crs):
|
2683
|
+
"""Check whether the maximum depth specified has been exceeded with the current interval."""
|
2684
|
+
|
2685
|
+
max_depth_exceeded = False
|
2686
|
+
if max_depth is not None:
|
2687
|
+
cell_depth = grid.centre_point(cell_kji0)[2]
|
2688
|
+
if not grid_crs.z_inc_down:
|
2689
|
+
cell_depth = -cell_depth
|
2690
|
+
if cell_depth > max_depth:
|
2691
|
+
max_depth_exceeded = True
|
2692
|
+
return max_depth_exceeded
|
2693
|
+
|
2694
|
+
@staticmethod
|
2695
|
+
def __skip_interval_check(max_depth, grid, cell_kji0, grid_crs, active_only, tuple_kji0, min_k0, max_k0, k0_list,
|
2696
|
+
region_list, region_uuid, max_satw, satw_uuid, min_sato, sato_uuid, max_satg, satg_uuid):
|
2697
|
+
"""Check whether any conditions are met that mean the interval should be skipped."""
|
2698
|
+
|
2699
|
+
max_depth_exceeded = BlockedWell.__check_cell_depth(max_depth = max_depth,
|
2700
|
+
grid = grid,
|
2701
|
+
cell_kji0 = cell_kji0,
|
2702
|
+
grid_crs = grid_crs)
|
2703
|
+
inactive_grid = active_only and grid.inactive is not None and grid.inactive[tuple_kji0]
|
2704
|
+
out_of_bounds_layer_1 = (min_k0 is not None and cell_kji0[0] < min_k0) or (max_k0 is not None and
|
2705
|
+
cell_kji0[0] > max_k0)
|
2706
|
+
out_of_bounds_layer_2 = k0_list is not None and cell_kji0[0] not in k0_list
|
2707
|
+
out_of_bounds_region = (region_list is not None and
|
2708
|
+
BlockedWell.__prop_array(region_uuid, grid)[tuple_kji0] not in region_list)
|
2709
|
+
saturation_limit_exceeded_1 = (max_satw is not None and
|
2710
|
+
BlockedWell.__prop_array(satw_uuid, grid)[tuple_kji0] > max_satw)
|
2711
|
+
saturation_limit_exceeded_2 = (min_sato is not None and
|
2712
|
+
BlockedWell.__prop_array(sato_uuid, grid)[tuple_kji0] < min_sato)
|
2713
|
+
saturation_limit_exceeded_3 = (max_satg is not None and
|
2714
|
+
BlockedWell.__prop_array(satg_uuid, grid)[tuple_kji0] > max_satg)
|
2715
|
+
skip_interval = any([
|
2716
|
+
max_depth_exceeded, inactive_grid, out_of_bounds_layer_1, out_of_bounds_layer_2, out_of_bounds_region,
|
2717
|
+
saturation_limit_exceeded_1, saturation_limit_exceeded_2, saturation_limit_exceeded_3
|
2718
|
+
])
|
2719
|
+
|
2720
|
+
return skip_interval
|
2721
|
+
|
2722
|
+
def __get_part_perf_fraction_for_interval(self, pc, pc_titles, ci, perforation_list, interval, length_tol = 0.01):
|
2723
|
+
"""Get the partial perforation fraction for the interval."""
|
2724
|
+
|
2725
|
+
skip_interval = False
|
2726
|
+
if 'PPERF' in pc_titles:
|
2727
|
+
part_perf_fraction = pc.single_array_ref(citation_title = 'PPERF')[ci]
|
2728
|
+
else:
|
2729
|
+
part_perf_fraction = 1.0
|
2730
|
+
if perforation_list is not None:
|
2731
|
+
perf_length = 0.0
|
2732
|
+
for perf_start, perf_end in perforation_list:
|
2733
|
+
if perf_end <= self.node_mds[interval] or perf_start >= self.node_mds[interval + 1]:
|
2734
|
+
continue
|
2735
|
+
if perf_start <= self.node_mds[interval]:
|
2736
|
+
if perf_end >= self.node_mds[interval + 1]:
|
2737
|
+
perf_length += self.node_mds[interval + 1] - self.node_mds[interval]
|
2738
|
+
break
|
2739
|
+
else:
|
2740
|
+
perf_length += perf_end - self.node_mds[interval]
|
2741
|
+
else:
|
2742
|
+
if perf_end >= self.node_mds[interval + 1]:
|
2743
|
+
perf_length += self.node_mds[interval + 1] - perf_start
|
2744
|
+
else:
|
2745
|
+
perf_length += perf_end - perf_start
|
2746
|
+
if perf_length < length_tol:
|
2747
|
+
skip_interval = True
|
2748
|
+
perf_length = 0.0
|
2749
|
+
part_perf_fraction = min(1.0, perf_length / (self.node_mds[interval + 1] - self.node_mds[interval]))
|
2750
|
+
|
2751
|
+
return skip_interval, part_perf_fraction
|
2752
|
+
|
2753
|
+
def __get_entry_exit_xyz_and_crs_for_interval(self, doing_entry_exit, use_face_centres, grid, cell_kji0, interval,
|
2754
|
+
ci, grid_crs, traj_crs):
|
2755
|
+
# calculate the entry and exit points for the interval and set the entry and exit coordinate reference system
|
2756
|
+
|
2757
|
+
entry_xyz = None
|
2758
|
+
exit_xyz = None
|
2759
|
+
ee_crs = None
|
2760
|
+
if doing_entry_exit:
|
2761
|
+
assert self.trajectory is not None
|
2762
|
+
if use_face_centres:
|
2763
|
+
entry_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0], self.face_pair_indices[ci, 0,
|
2764
|
+
1])
|
2765
|
+
if self.face_pair_indices[ci, 1, 0] >= 0:
|
2766
|
+
exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 1, 0],
|
2767
|
+
self.face_pair_indices[ci, 1, 1])
|
2768
|
+
else:
|
2769
|
+
exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0],
|
2770
|
+
1 - self.face_pair_indices[ci, 0, 1])
|
2771
|
+
ee_crs = grid_crs
|
2772
|
+
else:
|
2773
|
+
entry_xyz = self.trajectory.xyz_for_md(self.node_mds[interval])
|
2774
|
+
exit_xyz = self.trajectory.xyz_for_md(self.node_mds[interval + 1])
|
2775
|
+
ee_crs = traj_crs
|
2776
|
+
|
2777
|
+
return entry_xyz, exit_xyz, ee_crs
|
2778
|
+
|
2779
|
+
def __get_length_of_interval(self, length_mode, interval, length_uom, entry_xyz, exit_xyz, ee_crs, perforation_list,
|
2780
|
+
part_perf_fraction, min_length):
|
2781
|
+
"""Calculate the length of the interval."""
|
2782
|
+
|
2783
|
+
skip_interval = False
|
2784
|
+
if length_mode == 'MD':
|
2785
|
+
length = self.node_mds[interval + 1] - self.node_mds[interval]
|
2786
|
+
if length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
|
2787
|
+
length = wam.convert_lengths(length, self.trajectory.md_uom, length_uom)
|
2788
|
+
else: # use straight line length between entry and exit
|
2789
|
+
entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
|
2790
|
+
length = vec.naive_length(exit_xyz - entry_xyz)
|
2791
|
+
if length_uom is not None:
|
2792
|
+
length = wam.convert_lengths(length, ee_crs.z_units, length_uom)
|
2793
|
+
elif self.trajectory is not None:
|
2794
|
+
length = wam.convert_lengths(length, ee_crs.z_units, self.trajectory.md_uom)
|
2795
|
+
if perforation_list is not None:
|
2796
|
+
length *= part_perf_fraction
|
2797
|
+
if min_length is not None and length < min_length:
|
2798
|
+
skip_interval = True
|
2799
|
+
|
2800
|
+
return skip_interval, length
|
2801
|
+
|
2802
|
+
@staticmethod
|
2803
|
+
def _single_uom_xyz(xyz, crs, required_uom):
|
2804
|
+
if xyz is None:
|
2805
|
+
return None
|
2806
|
+
xyz = np.array(xyz, dtype = float)
|
2807
|
+
if crs.xy_units != required_uom:
|
2808
|
+
xyz[0] = wam.convert_lengths(xyz[0], crs.xy_units, required_uom)
|
2809
|
+
xyz[1] = wam.convert_lengths(xyz[1], crs.xy_units, required_uom)
|
2810
|
+
if crs.z_units != required_uom:
|
2811
|
+
xyz[2] = wam.convert_lengths(xyz[2], crs.z_units, required_uom)
|
2812
|
+
return xyz
|
2813
|
+
|
2814
|
+
@staticmethod
|
2815
|
+
def _single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs):
|
2816
|
+
return (BlockedWell._single_uom_xyz(entry_xyz, ee_crs, ee_crs.z_units),
|
2817
|
+
BlockedWell._single_uom_xyz(exit_xyz, ee_crs, ee_crs.z_units))
|
2818
|
+
|
2819
|
+
def __get_angles_for_interval(self, pc, pc_titles, doing_angles, set_k_face_intervals_vertical, ci, k_face_check,
|
2820
|
+
k_face_check_end, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs,
|
2821
|
+
cell_kji0, anglv_ref, angla_plane_ref):
|
2822
|
+
"""Calculate angla, anglv and related trigonometirc transforms for the interval."""
|
2823
|
+
|
2824
|
+
sine_anglv = sine_angla = 0.0
|
2825
|
+
cosine_anglv = cosine_angla = 1.0
|
2826
|
+
anglv = pc.single_array_ref(citation_title = 'ANGLV')[ci] if 'ANGLV' in pc_titles else None
|
2827
|
+
angla = pc.single_array_ref(citation_title = 'ANGLA')[ci] if 'ANGLA' in pc_titles else None
|
2828
|
+
|
2829
|
+
if doing_angles and not (set_k_face_intervals_vertical and
|
2830
|
+
(np.all(self.face_pair_indices[ci] == k_face_check) or
|
2831
|
+
np.all(self.face_pair_indices[ci] == k_face_check_end))):
|
2832
|
+
anglv, sine_anglv, cosine_anglv, vector, a_ref_vector = BlockedWell.__get_anglv_for_interval(
|
2833
|
+
anglv = anglv,
|
2834
|
+
entry_xyz = entry_xyz,
|
2835
|
+
exit_xyz = exit_xyz,
|
2836
|
+
ee_crs = ee_crs,
|
2837
|
+
traj_z_inc_down = traj_z_inc_down,
|
2838
|
+
grid = grid,
|
2839
|
+
grid_crs = grid_crs,
|
2840
|
+
cell_kji0 = cell_kji0,
|
2841
|
+
anglv_ref = anglv_ref,
|
2842
|
+
angla_plane_ref = angla_plane_ref)
|
2843
|
+
if anglv != 0.0:
|
2844
|
+
angla, sine_angla, cosine_angla = BlockedWell.__get_angla_for_interval(angla = angla,
|
2845
|
+
grid = grid,
|
2846
|
+
cell_kji0 = cell_kji0,
|
2847
|
+
vector = vector,
|
2848
|
+
a_ref_vector = a_ref_vector)
|
2849
|
+
if angla is None:
|
2850
|
+
angla = 0.0
|
2851
|
+
if anglv is None:
|
2852
|
+
anglv = 0.0
|
2853
|
+
|
2854
|
+
return anglv, sine_anglv, cosine_anglv, angla, sine_angla, cosine_angla
|
2855
|
+
|
2856
|
+
@staticmethod
|
2857
|
+
def __get_angla_for_interval(angla, grid, cell_kji0, vector, a_ref_vector):
|
2858
|
+
"""Calculate angla and related trigonometric transforms for the interval."""
|
2859
|
+
|
2860
|
+
if vector is None:
|
2861
|
+
return None, None, None
|
2862
|
+
|
2863
|
+
# project well vector and i-axis vector onto plane defined by normal vector a_ref_vector
|
2864
|
+
i_axis = grid.interface_vector(cell_kji0, 2)
|
2865
|
+
if grid.crs.xy_units != grid.crs.z_units:
|
2866
|
+
i_axis[2] = wam.convert_lengths(i_axis[2], grid.crs.z_units, grid.crs.xy_units)
|
2867
|
+
i_axis = vec.unit_vector(i_axis)
|
2868
|
+
if a_ref_vector is not None: # project vector and i axis onto a plane
|
2869
|
+
vector -= vec.dot_product(vector, a_ref_vector) * a_ref_vector
|
2870
|
+
vector = vec.unit_vector(vector)
|
2871
|
+
# log.debug('i axis unit vector: ' + str(i_axis))
|
2872
|
+
i_axis -= vec.dot_product(i_axis, a_ref_vector) * a_ref_vector
|
2873
|
+
i_axis = vec.unit_vector(i_axis)
|
2874
|
+
# log.debug('i axis unit vector in reference plane: ' + str(i_axis))
|
2875
|
+
if angla is not None:
|
2876
|
+
angla_rad = vec.radians_from_degrees(angla)
|
2877
|
+
cosine_angla = maths.cos(angla_rad)
|
2878
|
+
sine_angla = maths.sin(angla_rad)
|
2879
|
+
else:
|
2880
|
+
cosine_angla = min(max(vec.dot_product(vector, i_axis), -1.0), 1.0)
|
2881
|
+
angla_rad = maths.acos(cosine_angla)
|
2882
|
+
# negate angla if vector is 'clockwise from' i_axis when viewed from above, projected in the xy plane
|
2883
|
+
# todo: have discussion around angla sign under different ijk handedness (and z inc direction?)
|
2884
|
+
sine_angla = maths.sin(angla_rad)
|
2885
|
+
angla = vec.degrees_from_radians(angla_rad)
|
2886
|
+
if vec.clockwise((0.0, 0.0), i_axis, vector) > 0.0:
|
2887
|
+
angla = -angla
|
2888
|
+
angla_rad = -angla_rad ## as angle_rad before --> typo?
|
2889
|
+
sine_angla = -sine_angla
|
2890
|
+
|
2891
|
+
# log.debug('angla: ' + str(angla))
|
2892
|
+
|
2893
|
+
return angla, sine_angla, cosine_angla
|
2894
|
+
|
2895
|
+
@staticmethod
|
2896
|
+
def __get_anglv_for_interval(anglv, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs, cell_kji0,
|
2897
|
+
anglv_ref, angla_plane_ref):
|
2898
|
+
"""Get anglv and related trigonometric transforms for the interval."""
|
2899
|
+
|
2900
|
+
if entry_xyz is None or exit_xyz is None:
|
2901
|
+
return None, None, None, None, None
|
2902
|
+
|
2903
|
+
entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
|
2904
|
+
vector = vec.unit_vector(np.array(exit_xyz) - np.array(entry_xyz)) # nominal wellbore vector for interval
|
2905
|
+
if traj_z_inc_down is not None and traj_z_inc_down != grid_crs.z_inc_down:
|
2906
|
+
vector[2] = -vector[2]
|
2907
|
+
if grid.crs.xy_units == grid.crs.z_units:
|
2908
|
+
unit_adjusted_vector = vector
|
2909
|
+
else:
|
2910
|
+
unit_adjusted_vector = vector.copy()
|
2911
|
+
unit_adjusted_vector[2] = wam.convert_lengths(unit_adjusted_vector[2], grid.crs.z_units, grid.crs.xy_units)
|
2912
|
+
v_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, anglv_ref)
|
2913
|
+
# log.debug('v ref vector: ' + str(v_ref_vector))
|
2914
|
+
if angla_plane_ref == anglv_ref:
|
2915
|
+
a_ref_vector = v_ref_vector
|
2916
|
+
else:
|
2917
|
+
a_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, angla_plane_ref)
|
2918
|
+
# log.debug('a ref vector: ' + str(a_ref_vector))
|
2919
|
+
if anglv is not None:
|
2920
|
+
anglv_rad = vec.radians_from_degrees(anglv)
|
2921
|
+
cosine_anglv = maths.cos(anglv_rad)
|
2922
|
+
sine_anglv = maths.sin(anglv_rad)
|
2923
|
+
else:
|
2924
|
+
cosine_anglv = min(max(vec.dot_product(unit_adjusted_vector, v_ref_vector), -1.0), 1.0)
|
2925
|
+
anglv_rad = maths.acos(cosine_anglv)
|
2926
|
+
sine_anglv = maths.sin(anglv_rad)
|
2927
|
+
anglv = vec.degrees_from_radians(anglv_rad)
|
2928
|
+
# log.debug('anglv: ' + str(anglv))
|
2929
|
+
|
2930
|
+
return anglv, sine_anglv, cosine_anglv, vector, a_ref_vector
|
2931
|
+
|
2932
|
+
@staticmethod
|
2933
|
+
def __get_ntg_and_directional_perm_for_interval(doing_kh, do_well_inflow, ntg_uuid, grid, tuple_kji0,
|
2934
|
+
isotropic_perm, preferential_perforation, part_perf_fraction,
|
2935
|
+
perm_i_uuid, perm_j_uuid, perm_k_uuid):
|
2936
|
+
"""Get the net-to-gross and directional permeability arrays for the interval."""
|
2937
|
+
|
2938
|
+
ntg_is_one = False
|
2939
|
+
k_i = k_j = k_k = None
|
2940
|
+
if doing_kh or do_well_inflow:
|
2941
|
+
if ntg_uuid is None:
|
2942
|
+
ntg = 1.0
|
2943
|
+
ntg_is_one = True
|
2944
|
+
else:
|
2945
|
+
ntg = BlockedWell.__prop_array(ntg_uuid, grid)[tuple_kji0]
|
2946
|
+
ntg_is_one = maths.isclose(ntg, 1.0, rel_tol = 0.001)
|
2947
|
+
if isotropic_perm and ntg_is_one:
|
2948
|
+
k_i = k_j = k_k = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
|
2949
|
+
else:
|
2950
|
+
if preferential_perforation and not ntg_is_one:
|
2951
|
+
if part_perf_fraction <= ntg:
|
2952
|
+
ntg = 1.0 # effective ntg when perforated intervals are in pay
|
2953
|
+
else:
|
2954
|
+
ntg /= part_perf_fraction # adjusted ntg when some perforations in non-pay
|
2955
|
+
# todo: check netgross facet type in property perm i & j parts: if set to gross then don't multiply by ntg below
|
2956
|
+
k_i = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0] * ntg
|
2957
|
+
k_j = BlockedWell.__prop_array(perm_j_uuid, grid)[tuple_kji0] * ntg
|
2958
|
+
k_k = BlockedWell.__prop_array(perm_k_uuid, grid)[tuple_kji0]
|
2959
|
+
|
2960
|
+
return ntg_is_one, k_i, k_j, k_k
|
2961
|
+
|
2962
|
+
@staticmethod
|
2963
|
+
def __get_kh_for_interval(doing_kh, isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j,
|
2964
|
+
k_k, anglv, sine_anglv, cosine_anglv, sine_angla, cosine_angla, min_kh, pc, pc_titles,
|
2965
|
+
ci):
|
2966
|
+
"""Get the permeability-thickness value for the interval."""
|
2967
|
+
|
2968
|
+
skip_interval = False
|
2969
|
+
if doing_kh:
|
2970
|
+
kh = BlockedWell.__get_kh_if_doing_kh(isotropic_perm = isotropic_perm,
|
2971
|
+
ntg_is_one = ntg_is_one,
|
2972
|
+
length = length,
|
2973
|
+
perm_i_uuid = perm_i_uuid,
|
2974
|
+
grid = grid,
|
2975
|
+
tuple_kji0 = tuple_kji0,
|
2976
|
+
k_i = k_i,
|
2977
|
+
k_j = k_j,
|
2978
|
+
k_k = k_k,
|
2979
|
+
anglv = anglv,
|
2980
|
+
sine_anglv = sine_anglv,
|
2981
|
+
cosine_anglv = cosine_anglv,
|
2982
|
+
sine_angla = sine_angla,
|
2983
|
+
cosine_angla = cosine_angla)
|
2984
|
+
if min_kh is not None and kh < min_kh:
|
2985
|
+
skip_interval = True
|
2986
|
+
elif 'KH' in pc_titles:
|
2987
|
+
kh = pc.single_array_ref(citation_title = 'KH')[ci]
|
2988
|
+
else:
|
2989
|
+
kh = None
|
2990
|
+
return skip_interval, kh
|
2941
2991
|
|
2942
|
-
|
2992
|
+
@staticmethod
|
2993
|
+
def __get_kh_if_doing_kh(isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j, k_k, anglv,
|
2994
|
+
sine_anglv, cosine_anglv, sine_angla, cosine_angla):
|
2995
|
+
# note: this is believed to return required value even when grid crs has mixed xy & z units;
|
2996
|
+
# angles are true angles accounting for any mixed units
|
2997
|
+
if isotropic_perm and ntg_is_one:
|
2998
|
+
kh = length * BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
|
2999
|
+
else:
|
3000
|
+
if np.isnan(k_i) or np.isnan(k_j):
|
3001
|
+
kh = 0.0
|
3002
|
+
elif anglv == 0.0:
|
3003
|
+
kh = length * maths.sqrt(k_i * k_j)
|
3004
|
+
elif np.isnan(k_k):
|
3005
|
+
kh = 0.0
|
3006
|
+
else:
|
3007
|
+
k_e = maths.pow(k_i * k_j * k_k, 1.0 / 3.0)
|
3008
|
+
if k_e == 0.0:
|
3009
|
+
kh = 0.0
|
3010
|
+
else:
|
3011
|
+
l_i = length * maths.sqrt(k_e / k_i) * sine_anglv * cosine_angla
|
3012
|
+
l_j = length * maths.sqrt(k_e / k_j) * sine_anglv * sine_angla
|
3013
|
+
l_k = length * maths.sqrt(k_e / k_k) * cosine_anglv
|
3014
|
+
l_p = maths.sqrt(l_i * l_i + l_j * l_j + l_k * l_k)
|
3015
|
+
kh = k_e * l_p
|
3016
|
+
return kh
|
2943
3017
|
|
2944
|
-
|
2945
|
-
|
2946
|
-
|
2947
|
-
|
2948
|
-
perm_j_uuid = perm_j_uuid,
|
2949
|
-
perm_k_uuid = perm_k_uuid,
|
2950
|
-
satw_uuid = satw_uuid,
|
2951
|
-
sato_uuid = sato_uuid,
|
2952
|
-
satg_uuid = satg_uuid,
|
2953
|
-
region_uuid = region_uuid,
|
2954
|
-
radw = radw,
|
2955
|
-
skin = skin,
|
2956
|
-
stat = stat,
|
2957
|
-
active_only = active_only,
|
2958
|
-
min_k0 = min_k0,
|
2959
|
-
max_k0 = max_k0,
|
2960
|
-
k0_list = k0_list,
|
2961
|
-
min_length = min_length,
|
2962
|
-
min_kh = min_kh,
|
2963
|
-
max_depth = max_depth,
|
2964
|
-
max_satw = max_satw,
|
2965
|
-
min_sato = min_sato,
|
2966
|
-
max_satg = max_satg,
|
2967
|
-
perforation_list = perforation_list,
|
2968
|
-
region_list = region_list,
|
2969
|
-
depth_inc_down = depth_inc_down,
|
2970
|
-
set_k_face_intervals_vertical = set_k_face_intervals_vertical,
|
2971
|
-
anglv_ref = anglv_ref,
|
2972
|
-
angla_plane_ref = angla_plane_ref,
|
2973
|
-
length_mode = length_mode,
|
2974
|
-
length_uom = length_uom,
|
2975
|
-
preferential_perforation = preferential_perforation,
|
2976
|
-
use_properties = use_properties,
|
2977
|
-
property_time_index = property_time_index)
|
3018
|
+
@staticmethod
|
3019
|
+
def __get_pc_arrays_for_interval(pc, pc_timeless, pc_titles, ci, length, radw, skin, stat, length_uom, grid,
|
3020
|
+
traj_crs):
|
3021
|
+
"""Get the property collection arrays for the interval."""
|
2978
3022
|
|
2979
|
-
|
3023
|
+
def get_item(v, title, pc_titles, pc, pc_timeless, ci, uom):
|
2980
3024
|
|
2981
|
-
|
2982
|
-
|
2983
|
-
|
3025
|
+
def pk_for_title(title):
|
3026
|
+
d = {
|
3027
|
+
'RADW': 'wellbore radius',
|
3028
|
+
'RADB': 'block equivalent radius',
|
3029
|
+
'SKIN': 'skin',
|
3030
|
+
'STAT': 'well connection open'
|
3031
|
+
}
|
3032
|
+
return d.get(title)
|
2984
3033
|
|
2985
|
-
|
2986
|
-
|
2987
|
-
|
2988
|
-
|
2989
|
-
|
2990
|
-
|
3034
|
+
p = None
|
3035
|
+
pk = pk_for_title(title)
|
3036
|
+
pc_uom = None
|
3037
|
+
for try_pc in [pc, pc_timeless]:
|
3038
|
+
if try_pc is None:
|
3039
|
+
continue
|
3040
|
+
if title in pc_titles:
|
3041
|
+
p = try_pc.singleton(citation_title = title)
|
3042
|
+
if p is None and pk is not None:
|
3043
|
+
p = try_pc.singleton(property_kind = pk)
|
3044
|
+
if p is not None:
|
3045
|
+
v = try_pc.cached_part_array_ref(p)[ci]
|
3046
|
+
pc_uom = try_pc.uom_for_part(p)
|
3047
|
+
break
|
3048
|
+
if (title == 'STAT' or pk == 'well connection open') and v is not None and not isinstance(v, str):
|
3049
|
+
v = 'ON' if v else 'OFF'
|
3050
|
+
if pc_uom is not None and uom is not None and pc_uom != uom:
|
3051
|
+
v = wam.convert_lengths(v, pc_uom, uom)
|
3052
|
+
return v
|
2991
3053
|
|
2992
|
-
|
3054
|
+
if length_uom is None:
|
3055
|
+
l_uom = traj_crs.z_units
|
3056
|
+
r_uom = grid.crs.xy_units
|
3057
|
+
else:
|
3058
|
+
l_uom = length_uom
|
3059
|
+
r_uom = length_uom
|
3060
|
+
length = get_item(length, 'LENGTH', pc_titles, pc, pc_timeless, ci, l_uom)
|
3061
|
+
radw = get_item(radw, 'RADW', pc_titles, pc, pc_timeless, ci, r_uom)
|
3062
|
+
stat = get_item(stat, 'STAT', pc_titles, pc, pc_timeless, ci, None)
|
3063
|
+
assert radw is None or radw > 0.0 # todo: allow zero for inactive intervals?
|
3064
|
+
skin = get_item(skin, 'SKIN', pc_titles, pc, pc_timeless, ci, None)
|
3065
|
+
if skin is None:
|
3066
|
+
skin = get_item(None, 'skin', pc_titles, pc, pc_timeless, ci, None)
|
3067
|
+
radb = get_item(None, 'RADB', pc_titles, pc, pc_timeless, ci, r_uom)
|
3068
|
+
if radb is None:
|
3069
|
+
radb = get_item(None, 'block equivalent radius', pc_titles, pc, pc_timeless, ci, r_uom)
|
3070
|
+
wi = get_item(None, 'WI', pc_titles, pc, pc_timeless, ci, None)
|
3071
|
+
wbc = get_item(None, 'WBC', pc_titles, pc, pc_timeless, ci, None)
|
2993
3072
|
|
2994
|
-
|
3073
|
+
return length, radw, skin, radb, wi, wbc, stat
|
2995
3074
|
|
2996
|
-
|
2997
|
-
|
2998
|
-
|
2999
|
-
|
3000
|
-
for _ in range(trailing_blank_lines):
|
3001
|
-
fp.write('\n')
|
3075
|
+
@staticmethod
|
3076
|
+
def __get_well_inflow_parameters_for_interval(do_well_inflow, isotropic_perm, ntg_is_one, k_i, k_j, k_k, sine_anglv,
|
3077
|
+
cosine_anglv, sine_angla, cosine_angla, grid, cell_kji0, radw, radb,
|
3078
|
+
wi, wbc, skin, kh, length_uom, column_list):
|
3002
3079
|
|
3003
|
-
|
3080
|
+
if do_well_inflow:
|
3081
|
+
if not length_uom:
|
3082
|
+
length_uom = grid.crs.z_units
|
3083
|
+
k_ei, k_ej, k_ek, radw_e = BlockedWell.__calculate_ke_and_radw_e(isotropic_perm = isotropic_perm,
|
3084
|
+
ntg_is_one = ntg_is_one,
|
3085
|
+
radw = radw,
|
3086
|
+
k_i = k_i,
|
3087
|
+
k_j = k_j,
|
3088
|
+
k_k = k_k,
|
3089
|
+
sine_anglv = sine_anglv,
|
3090
|
+
cosine_anglv = cosine_anglv,
|
3091
|
+
sine_angla = sine_angla,
|
3092
|
+
cosine_angla = cosine_angla)
|
3004
3093
|
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3011
|
-
|
3012
|
-
|
3013
|
-
|
3014
|
-
|
3015
|
-
|
3016
|
-
|
3017
|
-
|
3094
|
+
cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
|
3095
|
+
wam.convert_lengths(cell_axial_vectors[..., :2], grid.crs.xy_units, length_uom)
|
3096
|
+
wam.convert_lengths(cell_axial_vectors[..., 2], grid.crs.z_units, length_uom)
|
3097
|
+
d2 = np.empty(3)
|
3098
|
+
for axis in range(3):
|
3099
|
+
d2[axis] = np.sum(cell_axial_vectors[axis] * cell_axial_vectors[axis])
|
3100
|
+
if radb is None:
|
3101
|
+
radb_e = BlockedWell.__calculate_radb_e(k_ei = k_ei,
|
3102
|
+
k_ej = k_ej,
|
3103
|
+
k_ek = k_ek,
|
3104
|
+
k_i = k_i,
|
3105
|
+
k_j = k_j,
|
3106
|
+
k_k = k_k,
|
3107
|
+
d2 = d2,
|
3108
|
+
sine_anglv = sine_anglv,
|
3109
|
+
cosine_anglv = cosine_anglv,
|
3110
|
+
sine_angla = sine_angla,
|
3111
|
+
cosine_angla = cosine_angla)
|
3112
|
+
radb = radw * radb_e / radw_e
|
3113
|
+
log.debug(f'RADB value calculated in BlockedWell dataframe method as: {radb}')
|
3114
|
+
if wi is None:
|
3115
|
+
wi = 0.0 if radb <= 0.0 else 2.0 * maths.pi / (maths.log(radb / radw) + skin)
|
3116
|
+
if 'WBC' in column_list and wbc is None:
|
3117
|
+
assert length_uom == 'm' or length_uom.startswith('ft'), \
|
3118
|
+
'WBC only calculable for length uom of m or ft*'
|
3119
|
+
conversion_constant = 8.5270171e-5 if length_uom == 'm' else 0.006328286
|
3120
|
+
wbc = conversion_constant * kh * wi # note: pperf aleady accounted for in kh
|
3018
3121
|
|
3019
|
-
|
3020
|
-
def __is_float_column(col_name):
|
3021
|
-
if col_name.upper() in [
|
3022
|
-
'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'RADB', 'PPERF'
|
3023
|
-
]:
|
3024
|
-
return True
|
3025
|
-
return False
|
3122
|
+
return radb, wi, wbc
|
3026
3123
|
|
3027
3124
|
@staticmethod
|
3028
|
-
def
|
3029
|
-
|
3030
|
-
return True
|
3031
|
-
return False
|
3125
|
+
def __calculate_ke_and_radw_e(isotropic_perm, ntg_is_one, radw, k_i, k_j, k_k, sine_anglv, cosine_anglv, sine_angla,
|
3126
|
+
cosine_angla):
|
3032
3127
|
|
3033
|
-
|
3034
|
-
|
3128
|
+
if isotropic_perm and ntg_is_one:
|
3129
|
+
k_ei = k_ej = k_ek = k_i
|
3130
|
+
radw_e = radw
|
3131
|
+
else:
|
3132
|
+
k_ei = maths.sqrt(k_j * k_k)
|
3133
|
+
k_ej = maths.sqrt(k_i * k_k)
|
3134
|
+
k_ek = maths.sqrt(k_i * k_j)
|
3135
|
+
r_wi = 0.0 if k_ei == 0.0 else 0.5 * radw * (maths.sqrt(k_ei / k_j) + maths.sqrt(k_ei / k_k))
|
3136
|
+
r_wj = 0.0 if k_ej == 0.0 else 0.5 * radw * (maths.sqrt(k_ej / k_i) + maths.sqrt(k_ej / k_k))
|
3137
|
+
r_wk = 0.0 if k_ek == 0.0 else 0.5 * radw * (maths.sqrt(k_ek / k_i) + maths.sqrt(k_ek / k_j))
|
3138
|
+
rwi = r_wi * sine_anglv * cosine_angla
|
3139
|
+
rwj = r_wj * sine_anglv * sine_angla
|
3140
|
+
rwk = r_wk * cosine_anglv
|
3141
|
+
radw_e = maths.sqrt(rwi * rwi + rwj * rwj + rwk * rwk)
|
3142
|
+
if radw_e == 0.0:
|
3143
|
+
radw_e = radw # no permeability in this situation anyway
|
3035
3144
|
|
3036
|
-
|
3037
|
-
if self.well_name:
|
3038
|
-
well_name = self.well_name
|
3039
|
-
elif self.root is not None:
|
3040
|
-
well_name = rqet.citation_title_for_node(self.root)
|
3041
|
-
elif self.wellbore_interpretation is not None:
|
3042
|
-
well_name = self.wellbore_interpretation.title
|
3043
|
-
elif self.trajectory is not None:
|
3044
|
-
well_name = self.trajectory.title
|
3045
|
-
if not well_name:
|
3046
|
-
log.warning('no well name identified for use in WELLSPEC')
|
3047
|
-
well_name = 'WELLNAME'
|
3048
|
-
well_name = BlockedWell.__tidy_well_name(well_name)
|
3145
|
+
return k_ei, k_ej, k_ek, radw_e
|
3049
3146
|
|
3050
|
-
|
3147
|
+
@staticmethod
|
3148
|
+
def __calculate_radb_e(k_ei, k_ej, k_ek, k_i, k_j, k_k, d2, sine_anglv, cosine_anglv, sine_angla, cosine_angla):
|
3051
3149
|
|
3052
|
-
|
3053
|
-
|
3054
|
-
|
3055
|
-
|
3150
|
+
r_bi = 0.0 if k_ei == 0.0 else 0.14 * maths.sqrt(k_ei * (d2[1] / k_j + d2[0] / k_k))
|
3151
|
+
r_bj = 0.0 if k_ej == 0.0 else 0.14 * maths.sqrt(k_ej * (d2[2] / k_i + d2[0] / k_k))
|
3152
|
+
r_bk = 0.0 if k_ek == 0.0 else 0.14 * maths.sqrt(k_ek * (d2[2] / k_i + d2[1] / k_j))
|
3153
|
+
rbi = r_bi * sine_anglv * cosine_angla
|
3154
|
+
rbj = r_bj * sine_anglv * sine_angla
|
3155
|
+
rbk = r_bk * cosine_anglv
|
3156
|
+
radb_e = maths.sqrt(rbi * rbi + rbj * rbj + rbk * rbk)
|
3056
3157
|
|
3057
|
-
|
3058
|
-
length_uom_system_list = ['METRIC', 'ENGLISH']
|
3059
|
-
length_uom_index = ['m', 'ft'].index(length_uom)
|
3060
|
-
fp.write(f'{length_uom_system_list[length_uom_index]}\n\n')
|
3158
|
+
return radb_e
|
3061
3159
|
|
3062
|
-
|
3063
|
-
|
3064
|
-
|
3065
|
-
fp.write('WELLSPEC ' + str(well_name) + '\n')
|
3160
|
+
def __get_xyz_for_interval(self, doing_xyz, length_mode, length_uom, md, traj_crs, depth_inc_down, traj_z_inc_down,
|
3161
|
+
entry_xyz, exit_xyz, ee_crs, pc, pc_titles, ci):
|
3162
|
+
"""Get the x, y and z location of the midpoint of the interval."""
|
3066
3163
|
|
3067
|
-
|
3068
|
-
|
3069
|
-
|
3070
|
-
|
3071
|
-
|
3072
|
-
|
3073
|
-
|
3074
|
-
|
3075
|
-
|
3076
|
-
|
3164
|
+
xyz = (np.nan, np.nan, np.nan)
|
3165
|
+
if doing_xyz:
|
3166
|
+
xyz = self.__get_xyz_if_doing_xyz(length_mode = length_mode,
|
3167
|
+
md = md,
|
3168
|
+
length_uom = length_uom,
|
3169
|
+
traj_crs = traj_crs,
|
3170
|
+
depth_inc_down = depth_inc_down,
|
3171
|
+
traj_z_inc_down = traj_z_inc_down,
|
3172
|
+
entry_xyz = entry_xyz,
|
3173
|
+
exit_xyz = exit_xyz,
|
3174
|
+
ee_crs = ee_crs)
|
3175
|
+
xyz = np.array(xyz)
|
3176
|
+
for i, col_header in enumerate(['X', 'Y', 'DEPTH']):
|
3177
|
+
if col_header in pc_titles:
|
3178
|
+
xyz[i] = pc.single_array_ref(citation_title = col_header)[ci]
|
3077
3179
|
|
3078
|
-
|
3079
|
-
def __write_wellspec_file_rows_from_dataframe(df, fp, col_width_dict, sep):
|
3080
|
-
"""Writes the non-blank lines of a Nexus WELLSPEC file from a BlockedWell dataframe."""
|
3180
|
+
return xyz
|
3081
3181
|
|
3082
|
-
|
3083
|
-
|
3084
|
-
for col_name in df.columns:
|
3085
|
-
try:
|
3086
|
-
if col_name in col_width_dict:
|
3087
|
-
width = col_width_dict[col_name]
|
3088
|
-
else:
|
3089
|
-
width = 10
|
3090
|
-
if BlockedWell.__is_float_column(col_name):
|
3091
|
-
form = '{0:>' + str(width) + '.3f}'
|
3092
|
-
value = row[col_name]
|
3093
|
-
if col_name == 'ANGLA' and (pd.isna(row[col_name]) or value is None or np.isnan(value)):
|
3094
|
-
value = 0.0
|
3095
|
-
fp.write(sep + form.format(float(value)))
|
3096
|
-
else:
|
3097
|
-
form = '{0:>' + str(width) + '}'
|
3098
|
-
if BlockedWell.__is_int_column(col_name):
|
3099
|
-
fp.write(sep + form.format(int(row[col_name])))
|
3100
|
-
elif col_name == 'STAT':
|
3101
|
-
fp.write(sep + form.format('OFF' if str(row['STAT']).upper() in ['0', 'OFF'] else 'ON'))
|
3102
|
-
else:
|
3103
|
-
fp.write(sep + form.format(str(row[col_name])))
|
3104
|
-
except Exception:
|
3105
|
-
fp.write(sep + str(row[col_name]))
|
3106
|
-
fp.write('\n')
|
3182
|
+
def __get_xyz_if_doing_xyz(self, length_mode, md, length_uom, traj_crs, depth_inc_down, traj_z_inc_down, exit_xyz,
|
3183
|
+
entry_xyz, ee_crs):
|
3107
3184
|
|
3108
|
-
|
3109
|
-
|
3185
|
+
if length_mode == 'MD' and self.trajectory is not None:
|
3186
|
+
xyz = self.trajectory.xyz_for_md(md)
|
3187
|
+
if length_uom is not None and length_uom != self.trajectory.md_uom:
|
3188
|
+
wam.convert_lengths(xyz, traj_crs.z_units, length_uom)
|
3189
|
+
if depth_inc_down and traj_z_inc_down is False:
|
3190
|
+
xyz[2] = -xyz[2]
|
3191
|
+
else:
|
3192
|
+
xyz = 0.5 * (np.array(exit_xyz) + np.array(entry_xyz))
|
3193
|
+
if length_uom is not None and length_uom != ee_crs.z_units:
|
3194
|
+
xyz[2] = wam.convert_lengths(xyz[2], ee_crs.z_units, length_uom)
|
3195
|
+
if depth_inc_down != ee_crs.z_inc_down:
|
3196
|
+
xyz[2] = -xyz[2]
|
3110
3197
|
|
3111
|
-
|
3112
|
-
if cells is None or grids is None or len(grids) == 0:
|
3113
|
-
return None, None, None, None
|
3114
|
-
return cells[0], grids[0].uuid
|
3198
|
+
return xyz
|
3115
3199
|
|
3116
|
-
def
|
3117
|
-
"""
|
3200
|
+
def __get_md_array_in_correct_units_for_interval(self, md, length_uom, pc, pc_titles, ci):
|
3201
|
+
"""Convert the measured depth to the correct units or get the measured depth from the property collection."""
|
3118
3202
|
|
3119
|
-
|
3120
|
-
|
3121
|
-
|
3122
|
-
|
3203
|
+
if 'MD' in pc_titles:
|
3204
|
+
md = pc.single_array_ref(citation_title = 'MD')[ci]
|
3205
|
+
elif length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
|
3206
|
+
md = wam.convert_lengths(md, self.trajectory.md_uom, length_uom)
|
3123
3207
|
|
3124
|
-
|
3125
|
-
if cells is None or grids is None or len(grids) == 0:
|
3126
|
-
return None, None
|
3127
|
-
node_index = 0
|
3128
|
-
while node_index < self.node_count - 1 and self.grid_indices[node_index] == -1:
|
3129
|
-
node_index += 1
|
3130
|
-
if node_index >= self.node_count - 1:
|
3131
|
-
return None, None
|
3132
|
-
md = 0.5 * (self.node_mds[node_index] + self.node_mds[node_index + 1])
|
3133
|
-
xyz = self.trajectory.xyz_for_md(md)
|
3134
|
-
return xyz, self.trajectory.crs_uuid
|
3208
|
+
return md
|
3135
3209
|
|
3136
|
-
|
3137
|
-
|
3210
|
+
@staticmethod
|
3211
|
+
def __append_interval_data_to_dataframe(df, grid_name, radw, skin, angla, anglv, length, kh, xyz, md, stat,
|
3212
|
+
part_perf_fraction, radb, wi, wbc, column_list, one_based, row_dict,
|
3213
|
+
cell_kji0, row_ci_list, ci):
|
3214
|
+
"""Append the row of data corresponding to the interval to the dataframe."""
|
3138
3215
|
|
3139
|
-
|
3140
|
-
|
3141
|
-
|
3142
|
-
|
3143
|
-
|
3144
|
-
|
3145
|
-
|
3146
|
-
|
3147
|
-
|
3148
|
-
title = 'WELL'
|
3149
|
-
if self.trajectory is not None:
|
3150
|
-
traj_interp_uuid = self.model.uuid(obj_type = 'WellboreInterpretation', related_uuid = self.trajectory.uuid)
|
3151
|
-
if traj_interp_uuid is not None:
|
3152
|
-
if shared_interpretation:
|
3153
|
-
self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
|
3154
|
-
uuid = traj_interp_uuid)
|
3155
|
-
traj_feature_uuid = self.model.uuid(obj_type = 'WellboreFeature', related_uuid = traj_interp_uuid)
|
3156
|
-
if traj_feature_uuid is not None:
|
3157
|
-
self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, uuid = traj_feature_uuid)
|
3158
|
-
if self.wellbore_feature is None:
|
3159
|
-
self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, feature_name = title)
|
3160
|
-
self.feature_to_be_written = True
|
3161
|
-
if self.wellbore_interpretation is None:
|
3162
|
-
title = title if not self.wellbore_feature.title else self.wellbore_feature.title
|
3163
|
-
self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
|
3164
|
-
title = title,
|
3165
|
-
wellbore_feature = self.wellbore_feature)
|
3166
|
-
if self.trajectory.wellbore_interpretation is None and shared_interpretation:
|
3167
|
-
self.trajectory.wellbore_interpretation = self.wellbore_interpretation
|
3168
|
-
self.interpretation_to_be_written = True
|
3216
|
+
column_names = [
|
3217
|
+
'GRID', 'RADW', 'SKIN', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'STAT', 'PPERF', 'RADB',
|
3218
|
+
'WI', 'WBC'
|
3219
|
+
]
|
3220
|
+
column_values = [
|
3221
|
+
grid_name, radw, skin, angla, anglv, length, kh, xyz[2], md, xyz[0], xyz[1], stat, part_perf_fraction, radb,
|
3222
|
+
wi, wbc
|
3223
|
+
]
|
3224
|
+
column_values_dict = dict(zip(column_names, column_values))
|
3169
3225
|
|
3170
|
-
|
3171
|
-
|
3172
|
-
|
3173
|
-
|
3174
|
-
|
3175
|
-
|
3176
|
-
|
3177
|
-
|
3178
|
-
|
3179
|
-
|
3226
|
+
data = df.to_dict()
|
3227
|
+
data = {k: list(v.values()) for k, v in data.items()}
|
3228
|
+
for col_index, col in enumerate(column_list):
|
3229
|
+
if col_index < 3:
|
3230
|
+
if one_based:
|
3231
|
+
row_dict[col] = [cell_kji0[2 - col_index] + 1]
|
3232
|
+
else:
|
3233
|
+
row_dict[col] = [cell_kji0[2 - col_index]]
|
3234
|
+
else:
|
3235
|
+
row_dict[col] = [column_values_dict[col]]
|
3180
3236
|
|
3181
|
-
|
3182
|
-
|
3183
|
-
|
3237
|
+
for col, vals in row_dict.items():
|
3238
|
+
if col in data:
|
3239
|
+
data[col].extend(vals)
|
3240
|
+
else:
|
3241
|
+
data[col] = vals
|
3242
|
+
df = pd.DataFrame(data)
|
3184
3243
|
|
3185
|
-
|
3186
|
-
well_name = self.title
|
3244
|
+
row_ci_list.append(ci)
|
3187
3245
|
|
3188
|
-
|
3189
|
-
datum_location = trajectory_points[0].copy()
|
3190
|
-
if set_depth_zero:
|
3191
|
-
datum_location[2] = 0.0
|
3192
|
-
datum = rqw.MdDatum(self.model,
|
3193
|
-
crs_uuid = grid.crs_uuid,
|
3194
|
-
location = datum_location,
|
3195
|
-
md_reference = 'mean sea level')
|
3246
|
+
return df
|
3196
3247
|
|
3197
|
-
|
3198
|
-
|
3199
|
-
|
3200
|
-
|
3201
|
-
|
3202
|
-
|
3203
|
-
|
3204
|
-
|
3205
|
-
})
|
3206
|
-
self.trajectory = rqw.Trajectory(self.model,
|
3207
|
-
md_datum = datum,
|
3208
|
-
data_frame = trajectory_df,
|
3209
|
-
length_uom = length_uom,
|
3210
|
-
well_name = well_name,
|
3211
|
-
set_tangent_vectors = set_tangent_vectors)
|
3212
|
-
self.trajectory_to_be_written = True
|
3248
|
+
def __add_as_properties(self,
|
3249
|
+
df,
|
3250
|
+
add_as_properties,
|
3251
|
+
extra_columns_list,
|
3252
|
+
length_uom,
|
3253
|
+
time_index = None,
|
3254
|
+
time_series_uuid = None):
|
3255
|
+
"""Adds property parts from df with columns listed in add_as_properties or extra_columns_list."""
|
3213
3256
|
|
3214
|
-
if
|
3215
|
-
|
3257
|
+
if add_as_properties:
|
3258
|
+
if isinstance(add_as_properties, list):
|
3259
|
+
for col in add_as_properties:
|
3260
|
+
assert col in extra_columns_list
|
3261
|
+
property_columns = add_as_properties
|
3262
|
+
else:
|
3263
|
+
property_columns = extra_columns_list
|
3264
|
+
self.add_df_properties(df,
|
3265
|
+
property_columns,
|
3266
|
+
length_uom = length_uom,
|
3267
|
+
time_index = time_index,
|
3268
|
+
time_series_uuid = time_series_uuid)
|
3216
3269
|
|
3217
|
-
def
|
3218
|
-
|
3219
|
-
create_for_trajectory_if_needed = True,
|
3220
|
-
add_as_part = True,
|
3221
|
-
add_relationships = True,
|
3222
|
-
title = None,
|
3223
|
-
originator = None):
|
3224
|
-
"""Create a blocked wellbore representation node from this BlockedWell object, optionally add as part.
|
3270
|
+
def _get_uom_pk_discrete_for_df_properties(self, extra, length_uom, temperature_uom = None):
|
3271
|
+
"""Set the property kind and unit of measure for all properties in the dataframe."""
|
3225
3272
|
|
3226
|
-
|
3227
|
-
|
3228
|
-
|
3273
|
+
# todo: this is horribly inefficient, building a whole dictionary for every call but only using one entry
|
3274
|
+
if length_uom not in ['m', 'ft']:
|
3275
|
+
raise ValueError(f"The length_uom {length_uom} must be either 'm' or 'ft'.")
|
3276
|
+
if extra == 'TEMP' and (temperature_uom is None or
|
3277
|
+
temperature_uom not in wam.valid_uoms('thermodynamic temperature')):
|
3278
|
+
raise ValueError(f"The temperature_uom must be in {wam.valid_uoms('thermodynamic temperature')}.")
|
3229
3279
|
|
3230
|
-
|
3231
|
-
|
3280
|
+
length_uom_pk_discrete = self._get_uom_pk_discrete_for_length_based_properties(length_uom = length_uom,
|
3281
|
+
extra = extra)
|
3282
|
+
uom_pk_discrete_dict = {
|
3283
|
+
'ANGLA': ('dega', 'azimuth', False),
|
3284
|
+
'ANGLV': ('dega', 'inclination', False),
|
3285
|
+
'KH': (f'mD.{length_uom}', 'permeability length', False),
|
3286
|
+
'PPERF': (f'{length_uom}/{length_uom}', 'perforation fraction', False),
|
3287
|
+
'STAT': (None, 'well connection open', True),
|
3288
|
+
'LENGTH': length_uom_pk_discrete,
|
3289
|
+
'MD': (length_uom, 'measured depth', False),
|
3290
|
+
'X': length_uom_pk_discrete,
|
3291
|
+
'Y': length_uom_pk_discrete,
|
3292
|
+
'DEPTH': (length_uom, 'depth', False),
|
3293
|
+
'RADW': (length_uom, 'wellbore radius', False),
|
3294
|
+
'RADB': (length_uom, 'block equivalent radius', False),
|
3295
|
+
'RADBP': length_uom_pk_discrete,
|
3296
|
+
'RADWP': length_uom_pk_discrete,
|
3297
|
+
'FM': (f'{length_uom}/{length_uom}', 'matrix fraction', False),
|
3298
|
+
'IRELPM': (None, 'relative permeability index', True), # TODO: change to 'region initialization' with facet
|
3299
|
+
'SECT': (None, 'wellbore section index', True),
|
3300
|
+
'LAYER': (None, 'layer index', True),
|
3301
|
+
'ANGLE': ('dega', 'plane angle', False),
|
3302
|
+
'TEMP': (temperature_uom, 'thermodynamic temperature', False),
|
3303
|
+
'MDCON': length_uom_pk_discrete,
|
3304
|
+
'K': ('mD', 'rock permeability', False),
|
3305
|
+
'DZ': (length_uom, 'cell length', False), # TODO: add direction facet
|
3306
|
+
'DTOP': (length_uom, 'depth', False),
|
3307
|
+
'DBOT': (length_uom, 'depth', False),
|
3308
|
+
'SKIN': ('Euc', 'skin', False),
|
3309
|
+
'WI': ('Euc', 'well connection index', False),
|
3310
|
+
}
|
3311
|
+
return uom_pk_discrete_dict.get(extra, ('Euc', 'generic continuous', False))
|
3232
3312
|
|
3233
|
-
|
3313
|
+
def _get_uom_pk_discrete_for_length_based_properties(self, length_uom, extra):
|
3314
|
+
if length_uom is None or length_uom == 'Euc':
|
3315
|
+
if extra in ['LENGTH', 'MD', 'MDCON']:
|
3316
|
+
uom = self.trajectory.md_uom
|
3317
|
+
elif extra in ['X', 'Y', 'RADW', 'RADB', 'RADBP', 'RADWP']:
|
3318
|
+
uom = self.grid_list[0].xy_units()
|
3319
|
+
else:
|
3320
|
+
uom = self.grid_list[0].z_units()
|
3321
|
+
else:
|
3322
|
+
uom = length_uom
|
3323
|
+
if extra == 'DEPTH':
|
3324
|
+
pk = 'depth'
|
3325
|
+
else:
|
3326
|
+
pk = 'length'
|
3327
|
+
return uom, pk, False
|
3234
3328
|
|
3235
|
-
|
3236
|
-
|
3329
|
+
@staticmethod
|
3330
|
+
def __tidy_well_name(well_name):
|
3331
|
+
nexus_friendly = ''
|
3332
|
+
previous_underscore = False
|
3333
|
+
for ch in well_name:
|
3334
|
+
if not 32 <= ord(ch) < 128 or ch in ' ,!*#':
|
3335
|
+
ch = '_'
|
3336
|
+
if not (previous_underscore and ch == '_'):
|
3337
|
+
nexus_friendly += ch
|
3338
|
+
previous_underscore = (ch == '_')
|
3339
|
+
if not nexus_friendly:
|
3340
|
+
well_name = 'WELL_X'
|
3341
|
+
return nexus_friendly
|
3237
3342
|
|
3238
|
-
|
3239
|
-
|
3240
|
-
if
|
3241
|
-
|
3242
|
-
|
3343
|
+
@staticmethod
|
3344
|
+
def __is_float_column(col_name):
|
3345
|
+
if col_name.upper() in [
|
3346
|
+
'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'RADB', 'PPERF'
|
3347
|
+
]:
|
3348
|
+
return True
|
3349
|
+
return False
|
3243
3350
|
|
3244
|
-
|
3245
|
-
|
3246
|
-
|
3351
|
+
@staticmethod
|
3352
|
+
def __is_int_column(col_name):
|
3353
|
+
if col_name.upper() in ['IW', 'JW', 'L']:
|
3354
|
+
return True
|
3355
|
+
return False
|
3247
3356
|
|
3248
|
-
|
3249
|
-
|
3250
|
-
add_relationships = add_relationships,
|
3251
|
-
originator = originator,
|
3252
|
-
ext_uuid = ext_uuid,
|
3253
|
-
title = title)
|
3357
|
+
def __get_well_name(self, well_name):
|
3358
|
+
"""Get the name of the well whose data is to be written to the Nexus WELLSPEC file."""
|
3254
3359
|
|
3255
|
-
|
3360
|
+
if not well_name:
|
3361
|
+
if self.well_name:
|
3362
|
+
well_name = self.well_name
|
3363
|
+
elif self.root is not None:
|
3364
|
+
well_name = rqet.citation_title_for_node(self.root)
|
3365
|
+
elif self.wellbore_interpretation is not None:
|
3366
|
+
well_name = self.wellbore_interpretation.title
|
3367
|
+
elif self.trajectory is not None:
|
3368
|
+
well_name = self.trajectory.title
|
3369
|
+
if not well_name:
|
3370
|
+
log.warning('no well name identified for use in WELLSPEC')
|
3371
|
+
well_name = 'WELLNAME'
|
3372
|
+
well_name = BlockedWell.__tidy_well_name(well_name)
|
3256
3373
|
|
3257
|
-
|
3374
|
+
return well_name
|
3258
3375
|
|
3259
|
-
|
3376
|
+
def __write_wellspec_file_units_metadata(self, write_nexus_units, fp, length_uom, length_uom_comment,
|
3377
|
+
extra_columns_list, well_name):
|
3378
|
+
# write the units of measure (uom) and system of measure for length in the WELLSPEC file
|
3379
|
+
# also write a comment on the length uom if necessary
|
3260
3380
|
|
3261
|
-
|
3262
|
-
|
3263
|
-
|
3381
|
+
if write_nexus_units:
|
3382
|
+
length_uom_system_list = ['METRIC', 'ENGLISH']
|
3383
|
+
length_uom_index = ['m', 'ft'].index(length_uom)
|
3384
|
+
fp.write(f'{length_uom_system_list[length_uom_index]}\n\n')
|
3264
3385
|
|
3265
|
-
self.
|
3266
|
-
|
3267
|
-
|
3268
|
-
|
3269
|
-
fis_values_node = fis_values_node)
|
3386
|
+
if length_uom_comment and self.trajectory is not None and ('LENGTH' in extra_columns_list or 'MD'
|
3387
|
+
in extra_columns_list or 'KH' in extra_columns_list):
|
3388
|
+
fp.write(f'! Length units along wellbore: {self.trajectory.md_uom if length_uom is None else length_uom}\n')
|
3389
|
+
fp.write('WELLSPEC ' + str(well_name) + '\n')
|
3270
3390
|
|
3271
|
-
|
3272
|
-
|
3391
|
+
@staticmethod
|
3392
|
+
def __write_wellspec_file_columns(df, fp, col_width_dict, sep):
|
3393
|
+
"""Write the column names to the WELLSPEC file."""
|
3394
|
+
for col_name in df.columns:
|
3395
|
+
if col_name in col_width_dict:
|
3396
|
+
width = col_width_dict[col_name]
|
3397
|
+
else:
|
3398
|
+
width = 10
|
3399
|
+
form = '{0:>' + str(width) + '}'
|
3400
|
+
fp.write(sep + form.format(col_name))
|
3273
3401
|
|
3274
|
-
|
3275
|
-
|
3276
|
-
|
3277
|
-
interp_root = interp_root,
|
3278
|
-
ext_uuid = ext_uuid)
|
3402
|
+
@staticmethod
|
3403
|
+
def __write_wellspec_file_rows_from_dataframe(df, fp, col_width_dict, sep):
|
3404
|
+
"""Writes the non-blank lines of a Nexus WELLSPEC file from a BlockedWell dataframe."""
|
3279
3405
|
|
3280
|
-
|
3406
|
+
for row_info in df.iterrows():
|
3407
|
+
row = row_info[1]
|
3408
|
+
for col_name in df.columns:
|
3409
|
+
try:
|
3410
|
+
if col_name in col_width_dict:
|
3411
|
+
width = col_width_dict[col_name]
|
3412
|
+
else:
|
3413
|
+
width = 10
|
3414
|
+
if BlockedWell.__is_float_column(col_name):
|
3415
|
+
form = '{0:>' + str(width) + '.3f}'
|
3416
|
+
value = row[col_name]
|
3417
|
+
if col_name == 'ANGLA' and (pd.isna(row[col_name]) or value is None or np.isnan(value)):
|
3418
|
+
value = 0.0
|
3419
|
+
fp.write(sep + form.format(float(value)))
|
3420
|
+
else:
|
3421
|
+
form = '{0:>' + str(width) + '}'
|
3422
|
+
if BlockedWell.__is_int_column(col_name):
|
3423
|
+
fp.write(sep + form.format(int(row[col_name])))
|
3424
|
+
elif col_name == 'STAT':
|
3425
|
+
fp.write(sep + form.format('OFF' if str(row['STAT']).upper() in ['0', 'OFF'] else 'ON'))
|
3426
|
+
else:
|
3427
|
+
fp.write(sep + form.format(str(row[col_name])))
|
3428
|
+
except Exception:
|
3429
|
+
fp.write(sep + str(row[col_name]))
|
3430
|
+
fp.write('\n')
|
3281
3431
|
|
3282
3432
|
def __create_wellbore_feature_and_interpretation_xml_if_needed(self, add_as_part, add_relationships, originator):
|
3283
3433
|
"""Create root node for WellboreFeature and WellboreInterpretation objects if necessary."""
|
@@ -3297,6 +3447,7 @@ class BlockedWell(BaseResqpy):
|
|
3297
3447
|
def __create_trajectory_xml_if_needed(self, create_for_trajectory_if_needed, add_as_part, add_relationships,
|
3298
3448
|
originator, ext_uuid, title):
|
3299
3449
|
"""Create root node for associated Trajectory object if necessary."""
|
3450
|
+
|
3300
3451
|
if create_for_trajectory_if_needed and self.trajectory_to_be_written and self.trajectory.root is None:
|
3301
3452
|
md_datum_root = self.trajectory.md_datum.create_xml(add_as_part = add_as_part,
|
3302
3453
|
add_relationships = add_relationships,
|
@@ -3311,6 +3462,7 @@ class BlockedWell(BaseResqpy):
|
|
3311
3462
|
|
3312
3463
|
def __create_bw_node_sub_elements(self, bw_node):
|
3313
3464
|
"""Append sub-elements to the BlockedWell object's root node."""
|
3465
|
+
|
3314
3466
|
nc_node = rqet.SubElement(bw_node, ns['resqml2'] + 'NodeCount')
|
3315
3467
|
nc_node.set(ns['xsi'] + 'type', ns['xsd'] + 'positiveInteger')
|
3316
3468
|
nc_node.text = str(self.node_count)
|
@@ -3369,7 +3521,8 @@ class BlockedWell(BaseResqpy):
|
|
3369
3521
|
fis_values_node.set(ns['xsi'] + 'type', ns['eml'] + 'Hdf5Dataset')
|
3370
3522
|
fis_values_node.text = rqet.null_xml_text
|
3371
3523
|
|
3372
|
-
return nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node,
|
3524
|
+
return (nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node,
|
3525
|
+
gnull_node, gis_values_node, fis_node, fnull_node, fis_values_node)
|
3373
3526
|
|
3374
3527
|
def __create_trajectory_grid_wellbore_interpretation_reference_nodes(self, bw_node):
|
3375
3528
|
"""Create nodes and add to BlockedWell object root node."""
|
@@ -3430,116 +3583,3 @@ class BlockedWell(BaseResqpy):
|
|
3430
3583
|
ext_node = self.model.root_for_part(ext_part)
|
3431
3584
|
self.model.create_reciprocal_relationship(bw_node, 'mlToExternalPartProxy', ext_node,
|
3432
3585
|
'externalPartProxyToMl')
|
3433
|
-
|
3434
|
-
def write_hdf5(self, file_name = None, mode = 'a', create_for_trajectory_if_needed = True):
|
3435
|
-
"""Create or append to an hdf5 file, writing datasets for the measured depths, grid, cell & face indices.
|
3436
|
-
|
3437
|
-
:meta common:
|
3438
|
-
"""
|
3439
|
-
|
3440
|
-
# NB: array data must all have been set up prior to calling this function
|
3441
|
-
|
3442
|
-
if self.uuid is None:
|
3443
|
-
self.uuid = bu.new_uuid()
|
3444
|
-
|
3445
|
-
h5_reg = rwh5.H5Register(self.model)
|
3446
|
-
|
3447
|
-
if create_for_trajectory_if_needed and self.trajectory_to_be_written:
|
3448
|
-
self.trajectory.write_hdf5(file_name, mode = mode)
|
3449
|
-
mode = 'a'
|
3450
|
-
|
3451
|
-
h5_reg.register_dataset(self.uuid, 'NodeMd', self.node_mds)
|
3452
|
-
h5_reg.register_dataset(self.uuid, 'CellIndices', self.cell_indices) # could use int32?
|
3453
|
-
h5_reg.register_dataset(self.uuid, 'GridIndices', self.grid_indices) # could use int32?
|
3454
|
-
# convert face index pairs from [axis, polarity] back to strange local face numbering
|
3455
|
-
mask = (self.face_pair_indices.flatten() == -1).reshape((-1, 2)) # 2nd axis is (axis, polarity)
|
3456
|
-
masked_face_indices = np.where(mask, 0, self.face_pair_indices.reshape((-1, 2))) # 2nd axis is (axis, polarity)
|
3457
|
-
# using flat array for raw_face_indices array
|
3458
|
-
# other resqml writing code might use an array with one int per entry point and one per exit point, with 2nd axis as (entry, exit)
|
3459
|
-
raw_face_indices = np.where(mask[:, 0], -1, self.face_index_map[masked_face_indices[:, 0],
|
3460
|
-
masked_face_indices[:,
|
3461
|
-
1]].flatten()).reshape(-1)
|
3462
|
-
|
3463
|
-
h5_reg.register_dataset(self.uuid, 'LocalFacePairPerCellIndices', raw_face_indices) # could use uint8?
|
3464
|
-
|
3465
|
-
h5_reg.write(file = file_name, mode = mode)
|
3466
|
-
|
3467
|
-
def add_grid_property_to_blocked_well(self, uuid_list):
|
3468
|
-
"""Add properties to blocked wells from a list of uuids for properties on the supporting grid."""
|
3469
|
-
|
3470
|
-
part_list = [self.model.part_for_uuid(uuid) for uuid in uuid_list]
|
3471
|
-
|
3472
|
-
assert len(self.grid_list) == 1, "only blocked wells with a single grid can be handled currently"
|
3473
|
-
grid = self.grid_list[0]
|
3474
|
-
# filter to only those properties on the grid
|
3475
|
-
parts = self.model.parts_list_filtered_by_supporting_uuid(part_list, grid.uuid)
|
3476
|
-
if len(parts) < len(uuid_list):
|
3477
|
-
log.warning(
|
3478
|
-
f"{len(uuid_list)-len(parts)} uuids ignored as they do not belong to the same grid as the blocked well")
|
3479
|
-
|
3480
|
-
gridpc = grid.extract_property_collection()
|
3481
|
-
# only 'cell' properties are handled
|
3482
|
-
cell_parts = [part for part in parts if gridpc.indexable_for_part(part) == 'cells']
|
3483
|
-
if len(cell_parts) < len(parts):
|
3484
|
-
log.warning(f"{len(parts)-len(cell_parts)} uuids ignored as they do not have indexable element of cells")
|
3485
|
-
|
3486
|
-
if len(cell_parts) > 0:
|
3487
|
-
bwpc = rqp.PropertyCollection(support = self)
|
3488
|
-
if len(gridpc.string_lookup_uuid_list()) > 0:
|
3489
|
-
sl_dict = {}
|
3490
|
-
for part in cell_parts:
|
3491
|
-
if gridpc.string_lookup_uuid_for_part(part) in sl_dict.keys():
|
3492
|
-
sl_dict[gridpc.string_lookup_uuid_for_part(part)] = \
|
3493
|
-
sl_dict[gridpc.string_lookup_uuid_for_part(part)] + [part]
|
3494
|
-
else:
|
3495
|
-
sl_dict[gridpc.string_lookup_uuid_for_part(part)] = [part]
|
3496
|
-
else:
|
3497
|
-
sl_dict = {None: cell_parts}
|
3498
|
-
|
3499
|
-
sl_ts_dict = {}
|
3500
|
-
for sl_uuid in sl_dict.keys():
|
3501
|
-
if len(gridpc.time_series_uuid_list()) > 0:
|
3502
|
-
# dictionary with keys for string_lookup uuids and None where missing
|
3503
|
-
# values for each key are a list of property parts associated with that lookup uuid, or None
|
3504
|
-
time_dict = {}
|
3505
|
-
for part in sl_dict[sl_uuid]:
|
3506
|
-
if gridpc.time_series_uuid_for_part(part) in time_dict.keys():
|
3507
|
-
time_dict[gridpc.time_series_uuid_for_part(part)] = \
|
3508
|
-
time_dict[gridpc.time_series_uuid_for_part(part)] + [part]
|
3509
|
-
else:
|
3510
|
-
time_dict[gridpc.time_series_uuid_for_part(part)] = [part]
|
3511
|
-
else:
|
3512
|
-
time_dict = {None: sl_dict[sl_uuid]}
|
3513
|
-
sl_ts_dict[sl_uuid] = time_dict
|
3514
|
-
|
3515
|
-
for sl_uuid in sl_ts_dict.keys():
|
3516
|
-
time_dict = sl_ts_dict[sl_uuid]
|
3517
|
-
for time_uuid in time_dict.keys():
|
3518
|
-
parts = time_dict[time_uuid]
|
3519
|
-
for part in parts:
|
3520
|
-
array = gridpc.cached_part_array_ref(part)
|
3521
|
-
indices = self.cell_indices_for_grid_uuid(grid.uuid)
|
3522
|
-
bwarray = np.empty(shape = (indices.shape[0],), dtype = array.dtype)
|
3523
|
-
for i, ind in enumerate(indices):
|
3524
|
-
bwarray[i] = array[tuple(ind)]
|
3525
|
-
bwpc.add_cached_array_to_imported_list(
|
3526
|
-
bwarray,
|
3527
|
-
source_info = f'property from grid {grid.title}',
|
3528
|
-
keyword = gridpc.citation_title_for_part(part),
|
3529
|
-
discrete = (not gridpc.continuous_for_part(part)),
|
3530
|
-
uom = gridpc.uom_for_part(part),
|
3531
|
-
time_index = gridpc.time_index_for_part(part),
|
3532
|
-
null_value = gridpc.null_value_for_part(part),
|
3533
|
-
property_kind = gridpc.property_kind_for_part(part),
|
3534
|
-
local_property_kind_uuid = gridpc.local_property_kind_uuid(part),
|
3535
|
-
facet_type = gridpc.facet_type_for_part(part),
|
3536
|
-
facet = gridpc.facet_for_part(part),
|
3537
|
-
realization = gridpc.realization_for_part(part),
|
3538
|
-
indexable_element = 'cells')
|
3539
|
-
bwpc.write_hdf5_for_imported_list(use_int32 = False)
|
3540
|
-
bwpc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_uuid,
|
3541
|
-
string_lookup_uuid = sl_uuid)
|
3542
|
-
else:
|
3543
|
-
log.debug(
|
3544
|
-
"no properties added - uuids either not 'cell' properties or blocked well is associated with multiple grids"
|
3545
|
-
)
|