resqpy 4.14.2__py3-none-any.whl → 5.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- resqpy/__init__.py +1 -1
- resqpy/fault/_gcs_functions.py +10 -10
- resqpy/fault/_grid_connection_set.py +277 -113
- resqpy/grid/__init__.py +2 -3
- resqpy/grid/_defined_geometry.py +3 -3
- resqpy/grid/_extract_functions.py +2 -1
- resqpy/grid/_grid.py +95 -12
- resqpy/grid/_grid_types.py +22 -7
- resqpy/grid/_points_functions.py +1 -1
- resqpy/grid/_regular_grid.py +6 -2
- resqpy/grid_surface/__init__.py +17 -38
- resqpy/grid_surface/_blocked_well_populate.py +5 -5
- resqpy/grid_surface/_find_faces.py +1349 -253
- resqpy/lines/_polyline.py +24 -33
- resqpy/model/_catalogue.py +9 -0
- resqpy/model/_forestry.py +18 -14
- resqpy/model/_hdf5.py +11 -3
- resqpy/model/_model.py +85 -10
- resqpy/model/_xml.py +38 -13
- resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
- resqpy/olio/read_nexus_fault.py +8 -2
- resqpy/olio/relperm.py +1 -1
- resqpy/olio/transmission.py +8 -8
- resqpy/olio/triangulation.py +36 -30
- resqpy/olio/vector_utilities.py +340 -6
- resqpy/olio/volume.py +0 -20
- resqpy/olio/wellspec_keywords.py +19 -13
- resqpy/olio/write_hdf5.py +1 -1
- resqpy/olio/xml_et.py +12 -0
- resqpy/property/__init__.py +6 -4
- resqpy/property/_collection_add_part.py +4 -3
- resqpy/property/_collection_create_xml.py +4 -2
- resqpy/property/_collection_get_attributes.py +4 -0
- resqpy/property/attribute_property_set.py +311 -0
- resqpy/property/grid_property_collection.py +11 -11
- resqpy/property/property_collection.py +79 -31
- resqpy/property/property_common.py +3 -8
- resqpy/rq_import/_add_surfaces.py +34 -14
- resqpy/rq_import/_grid_from_cp.py +2 -2
- resqpy/rq_import/_import_nexus.py +75 -48
- resqpy/rq_import/_import_vdb_all_grids.py +64 -52
- resqpy/rq_import/_import_vdb_ensemble.py +12 -13
- resqpy/surface/_mesh.py +4 -0
- resqpy/surface/_surface.py +593 -118
- resqpy/surface/_tri_mesh.py +13 -10
- resqpy/surface/_tri_mesh_stencil.py +4 -4
- resqpy/surface/_triangulated_patch.py +71 -51
- resqpy/time_series/_any_time_series.py +7 -4
- resqpy/time_series/_geologic_time_series.py +1 -1
- resqpy/unstructured/_hexa_grid.py +6 -2
- resqpy/unstructured/_prism_grid.py +13 -5
- resqpy/unstructured/_pyramid_grid.py +6 -2
- resqpy/unstructured/_tetra_grid.py +6 -2
- resqpy/unstructured/_unstructured_grid.py +6 -2
- resqpy/well/_blocked_well.py +1986 -1946
- resqpy/well/_deviation_survey.py +3 -3
- resqpy/well/_md_datum.py +11 -21
- resqpy/well/_trajectory.py +10 -5
- resqpy/well/_wellbore_frame.py +10 -2
- resqpy/well/blocked_well_frame.py +3 -3
- resqpy/well/well_object_funcs.py +7 -9
- resqpy/well/well_utils.py +33 -0
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/METADATA +8 -9
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
- resqpy/grid/_moved_functions.py +0 -15
- {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/LICENSE +0 -0
resqpy/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
|
-
)
|