resqpy 4.18.11__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- resqpy/__init__.py +1 -1
- resqpy/grid/__init__.py +2 -3
- resqpy/grid/_grid.py +1 -7
- resqpy/grid_surface/_find_faces.py +3 -0
- resqpy/lines/_polyline.py +24 -33
- resqpy/model/_model.py +4 -2
- resqpy/multi_processing/wrappers/grid_surface_mp.py +1 -1
- resqpy/olio/triangulation.py +4 -3
- resqpy/olio/volume.py +0 -20
- resqpy/property/__init__.py +3 -2
- resqpy/rq_import/_grid_from_cp.py +2 -2
- resqpy/surface/_surface.py +223 -47
- resqpy/time_series/_any_time_series.py +5 -4
- resqpy/well/_blocked_well.py +1916 -1910
- resqpy/well/_md_datum.py +11 -21
- resqpy/well/_wellbore_frame.py +10 -2
- resqpy/well/well_utils.py +33 -0
- {resqpy-4.18.11.dist-info → resqpy-5.0.0.dist-info}/METADATA +7 -7
- {resqpy-4.18.11.dist-info → resqpy-5.0.0.dist-info}/RECORD +21 -22
- resqpy/grid/_moved_functions.py +0 -15
- {resqpy-4.18.11.dist-info → resqpy-5.0.0.dist-info}/LICENSE +0 -0
- {resqpy-4.18.11.dist-info → resqpy-5.0.0.dist-info}/WHEEL +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
|
@@ -142,7 +143,7 @@ class BlockedWell(BaseResqpy):
|
|
142
143
|
|
143
144
|
self.property_collection = None
|
144
145
|
#: all logs associated with the blockedwellbore; an instance of :class:`resqpy.property.WellIntervalPropertyCollection`
|
145
|
-
self.
|
146
|
+
self._logs = None # use of .logs is deprecated
|
146
147
|
self.cellind_null = -1
|
147
148
|
self.gridind_null = -1
|
148
149
|
self.facepair_null = -1
|
@@ -191,159 +192,13 @@ class BlockedWell(BaseResqpy):
|
|
191
192
|
self.title = well_name
|
192
193
|
# else an empty object is returned
|
193
194
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
grid_final = grid
|
202
|
-
return grid_final
|
203
|
-
|
204
|
-
def __check_cellio_init_okay(self, cellio_file, well_name, grid):
|
205
|
-
"""Checks if BlockedWell object initialization from a cellio file is okay."""
|
206
|
-
|
207
|
-
okay = self.import_from_rms_cellio(cellio_file, well_name, grid)
|
208
|
-
if not okay:
|
209
|
-
self.node_count = 0
|
210
|
-
|
211
|
-
def _load_from_xml(self):
|
212
|
-
"""Loads the blocked wellbore object from an xml node (and associated hdf5 data)."""
|
213
|
-
|
214
|
-
node = self.root
|
215
|
-
assert node is not None
|
216
|
-
|
217
|
-
self.__find_trajectory_uuid(node = node)
|
218
|
-
|
219
|
-
self.node_count = rqet.find_tag_int(node, 'NodeCount')
|
220
|
-
assert self.node_count is not None and self.node_count >= 2, 'problem with blocked well node count'
|
221
|
-
|
222
|
-
mds_node = rqet.find_tag(node, 'NodeMd')
|
223
|
-
assert mds_node is not None, 'blocked well node measured depths hdf5 reference not found in xml'
|
224
|
-
rqwu.load_hdf5_array(self, mds_node, 'node_mds')
|
225
|
-
|
226
|
-
# Statement below has no effect, is this a bug?
|
227
|
-
self.node_mds is not None and self.node_mds.ndim == 1 and self.node_mds.size == self.node_count
|
228
|
-
|
229
|
-
self.cell_count = rqet.find_tag_int(node, 'CellCount')
|
230
|
-
assert self.cell_count is not None and self.cell_count > 0
|
231
|
-
|
232
|
-
# todo: remove this if block once RMS export issue resolved
|
233
|
-
if self.cell_count == self.node_count:
|
234
|
-
extended_mds = np.empty((self.node_mds.size + 1,))
|
235
|
-
extended_mds[:-1] = self.node_mds
|
236
|
-
extended_mds[-1] = self.node_mds[-1] + 1.0
|
237
|
-
self.node_mds = extended_mds
|
238
|
-
self.node_count += 1
|
239
|
-
|
240
|
-
assert self.cell_count < self.node_count
|
241
|
-
|
242
|
-
unique_grid_indices = self.__find_gi_node_and_load_hdf5_array(node = node)
|
243
|
-
|
244
|
-
self.__find_grid_node(node = node, unique_grid_indices = unique_grid_indices)
|
245
|
-
|
246
|
-
self.__find_ci_node_and_load_hdf5_array(node = node)
|
247
|
-
|
248
|
-
self.__find_fi_node_and_load_hdf5_array(node)
|
249
|
-
|
250
|
-
interp_uuid = rqet.find_nested_tags_text(node, ['RepresentedInterpretation', 'UUID'])
|
251
|
-
if interp_uuid is None:
|
252
|
-
self.wellbore_interpretation = None
|
253
|
-
else:
|
254
|
-
self.wellbore_interpretation = rqo.WellboreInterpretation(self.model, uuid = interp_uuid)
|
255
|
-
|
256
|
-
# Create blocked well log collection of all log data
|
257
|
-
self.logs = rqp.WellIntervalPropertyCollection(frame = self)
|
258
|
-
|
259
|
-
# Set up matches between cell_indices and grid_indices
|
260
|
-
self.cell_grid_link = self.map_cell_and_grid_indices()
|
261
|
-
|
262
|
-
def __find_trajectory_uuid(self, node):
|
263
|
-
"""Find and verify the uuid of the trajectory associated with the BlockedWell object."""
|
264
|
-
|
265
|
-
trajectory_uuid = bu.uuid_from_string(rqet.find_nested_tags_text(node, ['Trajectory', 'UUID']))
|
266
|
-
assert trajectory_uuid is not None, 'blocked well trajectory reference not found in xml'
|
267
|
-
if self.trajectory is None:
|
268
|
-
self.trajectory = rqw.Trajectory(self.model, uuid = trajectory_uuid)
|
269
|
-
else:
|
270
|
-
assert bu.matching_uuids(self.trajectory.uuid, trajectory_uuid), 'blocked well trajectory uuid mismatch'
|
271
|
-
|
272
|
-
def __find_ci_node_and_load_hdf5_array(self, node):
|
273
|
-
"""Find the BlockedWell object's cell indices hdf5 reference node and load the array."""
|
274
|
-
|
275
|
-
ci_node = rqet.find_tag(node, 'CellIndices')
|
276
|
-
assert ci_node is not None, 'blocked well cell indices hdf5 reference not found in xml'
|
277
|
-
rqwu.load_hdf5_array(self, ci_node, 'cell_indices', dtype = self.cell_index_dtype)
|
278
|
-
assert (self.cell_indices is not None and self.cell_indices.ndim == 1 and
|
279
|
-
self.cell_indices.size == self.cell_count), 'mismatch in number of cell indices for blocked well'
|
280
|
-
self.cellind_null = rqet.find_tag_int(ci_node, 'NullValue')
|
281
|
-
if self.cellind_null is None:
|
282
|
-
self.cellind_null = -1 # if no Null found assume -1 default
|
283
|
-
|
284
|
-
def __find_fi_node_and_load_hdf5_array(self, node):
|
285
|
-
"""Find the BlockedWell object's face indices hdf5 reference node and load the array."""
|
286
|
-
|
287
|
-
fi_node = rqet.find_tag(node, 'LocalFacePairPerCellIndices')
|
288
|
-
assert fi_node is not None, 'blocked well face indices hdf5 reference not found in xml'
|
289
|
-
rqwu.load_hdf5_array(self, fi_node, 'raw_face_indices', dtype = np.int8)
|
290
|
-
assert self.raw_face_indices is not None, 'failed to load face indices for blocked well'
|
291
|
-
assert self.raw_face_indices.size == 2 * self.cell_count, 'mismatch in number of cell faces for blocked well'
|
292
|
-
if self.raw_face_indices.ndim > 1:
|
293
|
-
self.raw_face_indices = self.raw_face_indices.reshape((self.raw_face_indices.size,))
|
294
|
-
mask = np.where(self.raw_face_indices == -1)
|
295
|
-
self.raw_face_indices[mask] = 0
|
296
|
-
self.face_pair_indices = self.face_index_inverse_map[self.raw_face_indices]
|
297
|
-
self.face_pair_indices[mask] = (-1, -1)
|
298
|
-
self.face_pair_indices = self.face_pair_indices.reshape((-1, 2, 2))
|
299
|
-
del self.raw_face_indices
|
300
|
-
self.facepair_null = rqet.find_tag_int(fi_node, 'NullValue')
|
301
|
-
if self.facepair_null is None:
|
302
|
-
self.facepair_null = -1
|
303
|
-
|
304
|
-
def __find_gi_node_and_load_hdf5_array(self, node):
|
305
|
-
"""Find the BlockedWell object's grid indices hdf5 reference node and load the array."""
|
306
|
-
|
307
|
-
gi_node = rqet.find_tag(node, 'GridIndices')
|
308
|
-
assert gi_node is not None, 'blocked well grid indices hdf5 reference not found in xml'
|
309
|
-
rqwu.load_hdf5_array(self, gi_node, 'grid_indices', dtype = np.int32)
|
310
|
-
# assert self.grid_indices is not None and self.grid_indices.ndim == 1 and self.grid_indices.size == self.node_count - 1
|
311
|
-
# temporary code to handle blocked wells with incorrectly shaped grid indices wrt. nodes
|
312
|
-
assert self.grid_indices is not None and self.grid_indices.ndim == 1
|
313
|
-
if self.grid_indices.size != self.node_count - 1:
|
314
|
-
if self.grid_indices.size == self.cell_count and self.node_count == 2 * self.cell_count:
|
315
|
-
log.warning(f'handling node duplication or missing unblocked intervals in blocked well: {self.title}')
|
316
|
-
expanded_grid_indices = np.full(self.node_count - 1, -1, dtype = np.int32)
|
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
|
-
|
332
|
-
grid_node_list = rqet.list_of_tag(node, 'Grid')
|
333
|
-
assert len(grid_node_list) > 0, 'blocked well grid reference(s) not found in xml'
|
334
|
-
assert unique_grid_indices[0] >= -1 and unique_grid_indices[-1] < len(
|
335
|
-
grid_node_list), 'blocked well grid index out of range'
|
336
|
-
assert np.count_nonzero(
|
337
|
-
self.grid_indices >= 0) == self.cell_count, 'mismatch in number of blocked well intervals'
|
338
|
-
self.grid_list = []
|
339
|
-
for grid_ref_node in grid_node_list:
|
340
|
-
grid_node = self.model.referenced_node(grid_ref_node)
|
341
|
-
assert grid_node is not None, 'grid referenced in blocked well xml is not present in model'
|
342
|
-
grid_uuid = rqet.uuid_for_part_root(grid_node)
|
343
|
-
grid_obj = self.model.grid(uuid = grid_uuid, find_properties = False)
|
344
|
-
self.grid_list.append(grid_obj)
|
345
|
-
if grid_obj.is_big():
|
346
|
-
self.cell_index_dtype = np.int64
|
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
|
347
202
|
|
348
203
|
def extract_property_collection(self, refresh = False):
|
349
204
|
"""Returns a property collection for the blocked well."""
|
@@ -433,18 +288,6 @@ class BlockedWell(BaseResqpy):
|
|
433
288
|
interval = self.interval_for_cell(cell_index)
|
434
289
|
return (self.node_mds[interval], self.node_mds[interval + 1])
|
435
290
|
|
436
|
-
def _set_cell_interval_map(self):
|
437
|
-
"""Sets up an index mapping from blocked cell index to interval index, accounting for unblocked intervals."""
|
438
|
-
|
439
|
-
self.cell_interval_map = np.zeros(self.cell_count, dtype = np.int32)
|
440
|
-
ci = 0
|
441
|
-
for ii in range(self.node_count - 1):
|
442
|
-
if self.grid_indices[ii] < 0:
|
443
|
-
continue
|
444
|
-
self.cell_interval_map[ci] = ii
|
445
|
-
ci += 1
|
446
|
-
assert ci == self.cell_count
|
447
|
-
|
448
291
|
def cell_indices_kji0(self):
|
449
292
|
"""Returns a numpy int array of shape (N, 3) of cells visited by well, for a single grid situation.
|
450
293
|
|
@@ -723,17 +566,6 @@ class BlockedWell(BaseResqpy):
|
|
723
566
|
|
724
567
|
return self
|
725
568
|
|
726
|
-
def __derive_from_wellspec_check_well_name(self, well_name):
|
727
|
-
"""Set the well name to be used in the wellspec file."""
|
728
|
-
|
729
|
-
if well_name:
|
730
|
-
self.well_name = well_name
|
731
|
-
else:
|
732
|
-
well_name = self.well_name
|
733
|
-
if not self.title:
|
734
|
-
self.title = well_name
|
735
|
-
return well_name
|
736
|
-
|
737
569
|
def derive_from_cell_list(self, cell_kji0_list, well_name, grid, length_uom = None):
|
738
570
|
"""Populate empty blocked well from numpy int array of shape (N, 3) being list of cells."""
|
739
571
|
|
@@ -874,273 +706,53 @@ class BlockedWell(BaseResqpy):
|
|
874
706
|
|
875
707
|
return self
|
876
708
|
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
cell_kji0[:] -= 1
|
885
|
-
return cell_kji0
|
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.
|
886
716
|
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
log.warning('skipping perforation(s) in grid ' + other_grid + ' for well ' + str(well_name))
|
895
|
-
skipped_warning_grid = other_grid
|
896
|
-
skip_row = True
|
897
|
-
return skipped_warning_grid, skip_row
|
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
|
898
724
|
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
row = row, cp = cp, well_name = well_name, xy_units = xy_units, z_units = z_units)
|
725
|
+
returns:
|
726
|
+
self if successful; None otherwise
|
727
|
+
"""
|
728
|
+
|
729
|
+
if well_name:
|
730
|
+
self.well_name = well_name
|
906
731
|
else:
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
entry_axis, entry_polarity, exit_axis, exit_polarity = BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_indices(
|
911
|
-
df = df, i = i, cell_kji0 = cell_kji0, blocked_cells_kji0 = blocked_cells_kji0)
|
732
|
+
well_name = self.well_name
|
733
|
+
if not self.title:
|
734
|
+
self.title = well_name
|
912
735
|
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
exit_polarity = exit_polarity,
|
919
|
-
cp = cp)
|
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
|
920
741
|
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
if xy_units != z_units:
|
936
|
-
well_vector[2] = wam.convert_lengths(well_vector[2], xy_units, z_units)
|
937
|
-
well_vector = vec.unit_vector(well_vector) * 10000.0
|
938
|
-
# todo: the following might be producing NaN's when vector passes precisely through an edge
|
939
|
-
(entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz) = \
|
940
|
-
rqwu.find_entry_and_exit(cp, -well_vector, well_vector, well_name)
|
941
|
-
return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
|
942
|
-
|
943
|
-
def __calculate_entry_and_exit_axes_polarities_and_points_using_indices(df, i, cell_kji0, blocked_cells_kji0):
|
944
|
-
|
945
|
-
entry_axis, entry_polarity = BlockedWell.__fabricate_entry_axis_and_polarity_using_indices(
|
946
|
-
i, cell_kji0, blocked_cells_kji0)
|
947
|
-
exit_axis, exit_polarity = BlockedWell.__fabricate_exit_axis_and_polarity_using_indices(
|
948
|
-
i, cell_kji0, entry_axis, entry_polarity, df)
|
949
|
-
|
950
|
-
return entry_axis, entry_polarity, exit_axis, exit_polarity
|
951
|
-
|
952
|
-
@staticmethod
|
953
|
-
def __fabricate_entry_axis_and_polarity_using_indices(i, cell_kji0, blocked_cells_kji0):
|
954
|
-
"""Fabricate entry and exit axes and polarities based on indices alone.
|
955
|
-
|
956
|
-
note:
|
957
|
-
could use geometry but here a cheap rough-and-ready approach is used
|
958
|
-
"""
|
959
|
-
|
960
|
-
if i == 0:
|
961
|
-
entry_axis, entry_polarity = 0, 0 # K-
|
962
|
-
else:
|
963
|
-
entry_move = cell_kji0 - blocked_cells_kji0[-1]
|
964
|
-
log.debug(f'entry move: {entry_move}')
|
965
|
-
if entry_move[1] == 0 and entry_move[2] == 0: # K move
|
966
|
-
entry_axis = 0
|
967
|
-
entry_polarity = 0 if entry_move[0] >= 0 else 1
|
968
|
-
elif abs(entry_move[1]) > abs(entry_move[2]): # J dominant move
|
969
|
-
entry_axis = 1
|
970
|
-
entry_polarity = 0 if entry_move[1] >= 0 else 1
|
971
|
-
else: # I dominant move
|
972
|
-
entry_axis = 2
|
973
|
-
entry_polarity = 0 if entry_move[2] >= 0 else 1
|
974
|
-
return entry_axis, entry_polarity
|
975
|
-
|
976
|
-
@staticmethod
|
977
|
-
def __fabricate_exit_axis_and_polarity_using_indices(i, cell_kji0, entry_axis, entry_polarity, df):
|
978
|
-
if i == len(df) - 1:
|
979
|
-
exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
|
980
|
-
else:
|
981
|
-
next_cell_kji0 = BlockedWell.__cell_kji0_from_df(df, i + 1)
|
982
|
-
if next_cell_kji0 is None:
|
983
|
-
exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
|
984
|
-
else:
|
985
|
-
exit_move = next_cell_kji0 - cell_kji0
|
986
|
-
log.debug(f'exit move: {exit_move}')
|
987
|
-
if exit_move[1] == 0 and exit_move[2] == 0: # K move
|
988
|
-
exit_axis = 0
|
989
|
-
exit_polarity = 1 if exit_move[0] >= 0 else 0
|
990
|
-
elif abs(exit_move[1]) > abs(exit_move[2]): # J dominant move
|
991
|
-
exit_axis = 1
|
992
|
-
exit_polarity = 1 if exit_move[1] >= 0 else 0
|
993
|
-
else: # I dominant move
|
994
|
-
exit_axis = 2
|
995
|
-
exit_polarity = 1 if exit_move[2] >= 0 else 0
|
996
|
-
return exit_axis, exit_polarity
|
997
|
-
|
998
|
-
@staticmethod
|
999
|
-
def __override_vector_based_xyz_entry_and_exit_points_if_necessary(use_face_centres, entry_axis, exit_axis,
|
1000
|
-
entry_polarity, exit_polarity, cp):
|
1001
|
-
"""Override the vector based xyz entry and exit with face centres."""
|
1002
|
-
|
1003
|
-
if use_face_centres: # override the vector based xyz entry and exit points with face centres
|
1004
|
-
if entry_axis == 0:
|
1005
|
-
entry_xyz = np.mean(cp[entry_polarity, :, :], axis = (0, 1))
|
1006
|
-
elif entry_axis == 1:
|
1007
|
-
entry_xyz = np.mean(cp[:, entry_polarity, :], axis = (0, 1))
|
1008
|
-
else:
|
1009
|
-
entry_xyz = np.mean(cp[:, :, entry_polarity], axis = (0, 1)) # entry_axis == 2, ie. I
|
1010
|
-
if exit_axis == 0:
|
1011
|
-
exit_xyz = np.mean(cp[exit_polarity, :, :], axis = (0, 1))
|
1012
|
-
elif exit_axis == 1:
|
1013
|
-
exit_xyz = np.mean(cp[:, exit_polarity, :], axis = (0, 1))
|
1014
|
-
else:
|
1015
|
-
exit_xyz = np.mean(cp[:, :, exit_polarity], axis = (0, 1)) # exit_axis == 2, ie. I
|
1016
|
-
return entry_xyz, exit_xyz
|
1017
|
-
|
1018
|
-
@staticmethod
|
1019
|
-
def __add_interval(previous_xyz, entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz,
|
1020
|
-
cell_kji0, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0,
|
1021
|
-
blocked_face_pairs, xy_units, z_units, length_uom):
|
1022
|
-
if previous_xyz is None: # first entry
|
1023
|
-
log.debug('adding mean sea level trajectory start')
|
1024
|
-
previous_xyz = entry_xyz.copy()
|
1025
|
-
previous_xyz[2] = 0.0 # use depth zero as md datum
|
1026
|
-
trajectory_mds.append(0.0)
|
1027
|
-
trajectory_points.append(previous_xyz)
|
1028
|
-
if not vec.isclose(previous_xyz, entry_xyz, tolerance = 0.05): # add an unblocked interval
|
1029
|
-
log.debug('adding unblocked interval')
|
1030
|
-
trajectory_points.append(entry_xyz)
|
1031
|
-
new_md = trajectory_mds[-1] + BlockedWell._md_length(entry_xyz - previous_xyz, xy_units, z_units,
|
1032
|
-
length_uom)
|
1033
|
-
trajectory_mds.append(new_md)
|
1034
|
-
blocked_intervals.append(-1) # unblocked interval
|
1035
|
-
previous_xyz = entry_xyz
|
1036
|
-
log.debug('adding blocked interval for cell kji0: ' + str(cell_kji0))
|
1037
|
-
trajectory_points.append(exit_xyz)
|
1038
|
-
new_md = trajectory_mds[-1] + BlockedWell._md_length(exit_xyz - previous_xyz, xy_units, z_units, length_uom)
|
1039
|
-
trajectory_mds.append(new_md)
|
1040
|
-
blocked_intervals.append(0) # blocked interval
|
1041
|
-
previous_xyz = exit_xyz
|
1042
|
-
blocked_cells_kji0.append(cell_kji0)
|
1043
|
-
blocked_face_pairs.append(((entry_axis, entry_polarity), (exit_axis, exit_polarity)))
|
1044
|
-
|
1045
|
-
return previous_xyz, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0, blocked_face_pairs
|
1046
|
-
|
1047
|
-
@staticmethod
|
1048
|
-
def _md_length(xyz_vector, xy_units, z_units, length_uom):
|
1049
|
-
if length_uom == xy_units and length_uom == z_units:
|
1050
|
-
return vec.naive_length(xyz_vector)
|
1051
|
-
x = wam.convert_lengths(xyz_vector[0], xy_units, length_uom)
|
1052
|
-
y = wam.convert_lengths(xyz_vector[1], xy_units, length_uom)
|
1053
|
-
z = wam.convert_lengths(xyz_vector[2], z_units, length_uom)
|
1054
|
-
return vec.naive_length((x, y, z))
|
1055
|
-
|
1056
|
-
@staticmethod
|
1057
|
-
def __add_tail_to_trajectory_if_necessary(blocked_count, exit_axis, exit_polarity, cell_kji0, grid,
|
1058
|
-
trajectory_points, trajectory_mds):
|
1059
|
-
"""Add tail to trajcetory if last segment terminates at bottom face in bottom layer."""
|
1060
|
-
|
1061
|
-
if blocked_count > 0 and exit_axis == 0 and exit_polarity == 1 and cell_kji0[
|
1062
|
-
0] == grid.nk - 1 and grid.k_direction_is_down:
|
1063
|
-
tail_length = 10.0 # metres or feet
|
1064
|
-
tail_xyz = trajectory_points[-1].copy()
|
1065
|
-
tail_xyz[2] += tail_length * (1.0 if grid.z_inc_down() else -1.0)
|
1066
|
-
trajectory_points.append(tail_xyz)
|
1067
|
-
new_md = trajectory_mds[-1] + tail_length
|
1068
|
-
trajectory_mds.append(new_md)
|
1069
|
-
|
1070
|
-
return trajectory_points, trajectory_mds
|
1071
|
-
|
1072
|
-
def __add_as_properties_if_specified(self,
|
1073
|
-
add_as_properties,
|
1074
|
-
df,
|
1075
|
-
length_uom,
|
1076
|
-
time_index = None,
|
1077
|
-
time_series_uuid = None):
|
1078
|
-
# if add_as_properties is True and present as a list of wellspec column names, both the blocked well and
|
1079
|
-
# the properties will have their hdf5 data written, xml created and be added as parts to the model
|
1080
|
-
|
1081
|
-
if add_as_properties and len(df.columns) > 3:
|
1082
|
-
# NB: atypical writing of hdf5 data and xml creation in order to support related properties
|
1083
|
-
self.write_hdf5()
|
1084
|
-
self.create_xml()
|
1085
|
-
if isinstance(add_as_properties, list):
|
1086
|
-
for col in add_as_properties:
|
1087
|
-
assert col in df.columns[3:] # could just skip missing columns
|
1088
|
-
property_columns = add_as_properties
|
1089
|
-
else:
|
1090
|
-
property_columns = df.columns[3:]
|
1091
|
-
self.add_df_properties(df,
|
1092
|
-
property_columns,
|
1093
|
-
length_uom = length_uom,
|
1094
|
-
time_index = time_index,
|
1095
|
-
time_series_uuid = time_series_uuid)
|
1096
|
-
|
1097
|
-
def import_from_rms_cellio(self,
|
1098
|
-
cellio_file,
|
1099
|
-
well_name,
|
1100
|
-
grid,
|
1101
|
-
include_overburden_unblocked_interval = False,
|
1102
|
-
set_tangent_vectors = False):
|
1103
|
-
"""Populates empty blocked well from RMS cell I/O data; creates simulation trajectory and md datum.
|
1104
|
-
|
1105
|
-
arguments:
|
1106
|
-
cellio_file (string): path of RMS ascii export file holding blocked well cell I/O data; cell entry and
|
1107
|
-
exit points are expected
|
1108
|
-
well_name (string): the name of the well as used in the cell I/O file
|
1109
|
-
grid (grid.Grid object): the grid object which the cell indices in the cell I/O data relate to
|
1110
|
-
set_tangent_vectors (boolean, default False): if True, tangent vectors will be computed from the well
|
1111
|
-
trajectory's control points
|
1112
|
-
|
1113
|
-
returns:
|
1114
|
-
self if successful; None otherwise
|
1115
|
-
"""
|
1116
|
-
|
1117
|
-
if well_name:
|
1118
|
-
self.well_name = well_name
|
1119
|
-
else:
|
1120
|
-
well_name = self.well_name
|
1121
|
-
if not self.title:
|
1122
|
-
self.title = well_name
|
1123
|
-
|
1124
|
-
grid_name = rqet.citation_title_for_node(grid.root)
|
1125
|
-
length_uom = grid.z_units()
|
1126
|
-
grid_z_inc_down = crs.Crs(grid.model, uuid = grid.crs_uuid).z_inc_down
|
1127
|
-
log.debug('grid z increasing downwards: ' + str(grid_z_inc_down) + '(type: ' + str(type(grid_z_inc_down)) + ')')
|
1128
|
-
cellio_z_inc_down = None
|
1129
|
-
|
1130
|
-
try:
|
1131
|
-
assert ' ' not in well_name, 'cannot import for well name containing spaces'
|
1132
|
-
with open(cellio_file, 'r') as fp:
|
1133
|
-
BlockedWell.__verify_header_lines_in_cellio_file(fp = fp,
|
1134
|
-
well_name = well_name,
|
1135
|
-
cellio_file = cellio_file)
|
1136
|
-
previous_xyz = None
|
1137
|
-
trajectory_mds = []
|
1138
|
-
trajectory_points = [] # entries paired with trajectory_mds
|
1139
|
-
blocked_intervals = [
|
1140
|
-
] # will have one fewer entries than trajectory nodes; 0 = blocked, -1 = not blocked (for grid indices)
|
1141
|
-
blocked_cells_kji0 = [] # will have length equal to number of 0's in blocked intervals
|
1142
|
-
blocked_face_pairs = [
|
1143
|
-
] # same length as blocked_cells_kji0; each is ((entry axis, entry polarity), (exit axis, exit polarity))
|
742
|
+
try:
|
743
|
+
assert ' ' not in well_name, 'cannot import for well name containing spaces'
|
744
|
+
with open(cellio_file, 'r') as fp:
|
745
|
+
BlockedWell.__verify_header_lines_in_cellio_file(fp = fp,
|
746
|
+
well_name = well_name,
|
747
|
+
cellio_file = cellio_file)
|
748
|
+
previous_xyz = None
|
749
|
+
trajectory_mds = []
|
750
|
+
trajectory_points = [] # entries paired with trajectory_mds
|
751
|
+
blocked_intervals = [
|
752
|
+
] # will have one fewer entries than trajectory nodes; 0 = blocked, -1 = not blocked (for grid indices)
|
753
|
+
blocked_cells_kji0 = [] # will have length equal to number of 0's in blocked intervals
|
754
|
+
blocked_face_pairs = [
|
755
|
+
] # same length as blocked_cells_kji0; each is ((entry axis, entry polarity), (exit axis, exit polarity))
|
1144
756
|
|
1145
757
|
while not kf.blank_line(fp):
|
1146
758
|
line = fp.readline()
|
@@ -1217,71 +829,6 @@ class BlockedWell(BaseResqpy):
|
|
1217
829
|
|
1218
830
|
return self
|
1219
831
|
|
1220
|
-
@staticmethod
|
1221
|
-
def __verify_header_lines_in_cellio_file(fp, well_name, cellio_file):
|
1222
|
-
"""Find and verify the information in the header lines for the specified well in the RMS cellio file."""
|
1223
|
-
|
1224
|
-
while True:
|
1225
|
-
kf.skip_blank_lines_and_comments(fp)
|
1226
|
-
line = fp.readline() # file format version number?
|
1227
|
-
assert line, 'well ' + str(well_name) + ' not found in file ' + str(cellio_file)
|
1228
|
-
fp.readline() # 'Undefined'
|
1229
|
-
words = fp.readline().split()
|
1230
|
-
assert len(words), 'missing header info in cell I/O file'
|
1231
|
-
if words[0].upper() == well_name.upper():
|
1232
|
-
break
|
1233
|
-
while not kf.blank_line(fp):
|
1234
|
-
fp.readline() # skip to block of data for next well
|
1235
|
-
header_lines = int(fp.readline().strip())
|
1236
|
-
for _ in range(header_lines):
|
1237
|
-
fp.readline()
|
1238
|
-
|
1239
|
-
@staticmethod
|
1240
|
-
def __parse_non_blank_line_in_cellio_file(line, grid, cellio_z_inc_down, grid_z_inc_down):
|
1241
|
-
"""Parse each non-blank line in the RMS cellio file for the relevant parameters."""
|
1242
|
-
|
1243
|
-
words = line.split()
|
1244
|
-
assert len(words) >= 9, 'not enough items on data line in cell I/O file, minimum 9 expected'
|
1245
|
-
i1, j1, k1 = int(words[0]), int(words[1]), int(words[2])
|
1246
|
-
cell_kji0 = np.array((k1 - 1, j1 - 1, i1 - 1), dtype = np.int32)
|
1247
|
-
assert np.all(0 <= cell_kji0) and np.all(
|
1248
|
-
cell_kji0 < grid.extent_kji), 'cell I/O cell index not within grid extent'
|
1249
|
-
entry_xyz = np.array((float(words[3]), float(words[4]), float(words[5])))
|
1250
|
-
exit_xyz = np.array((float(words[6]), float(words[7]), float(words[8])))
|
1251
|
-
if cellio_z_inc_down is None:
|
1252
|
-
cellio_z_inc_down = bool(entry_xyz[2] + exit_xyz[2] > 0.0)
|
1253
|
-
if cellio_z_inc_down != grid_z_inc_down:
|
1254
|
-
entry_xyz[2] = -entry_xyz[2]
|
1255
|
-
exit_xyz[2] = -exit_xyz[2]
|
1256
|
-
return cell_kji0, entry_xyz, exit_xyz
|
1257
|
-
|
1258
|
-
@staticmethod
|
1259
|
-
def __calculate_cell_cp_center_and_vectors(grid, cell_kji0, entry_xyz, exit_xyz, well_name):
|
1260
|
-
# calculate the i,j,k coordinates that represent the corner points and center of a perforation cell
|
1261
|
-
# calculate the entry and exit vectors for the perforation cell
|
1262
|
-
|
1263
|
-
cp = grid.corner_points(cell_kji0 = cell_kji0, cache_resqml_array = False)
|
1264
|
-
assert not np.any(np.isnan(
|
1265
|
-
cp)), 'missing geometry for perforation cell(kji0) ' + str(cell_kji0) + ' for well ' + str(well_name)
|
1266
|
-
cell_centre = np.mean(cp, axis = (0, 1, 2))
|
1267
|
-
# let's hope everything is in the same coordinate reference system!
|
1268
|
-
entry_vector = 100.0 * (entry_xyz - cell_centre)
|
1269
|
-
exit_vector = 100.0 * (exit_xyz - cell_centre)
|
1270
|
-
return cp, cell_centre, entry_vector, exit_vector
|
1271
|
-
|
1272
|
-
@staticmethod
|
1273
|
-
def __check_number_of_blocked_well_intervals(blocked_cells_kji0, well_name, grid_name):
|
1274
|
-
"""Check that at least one interval is blocked for the specified well."""
|
1275
|
-
|
1276
|
-
blocked_count = len(blocked_cells_kji0)
|
1277
|
-
if blocked_count == 0:
|
1278
|
-
log.warning(f"No intervals blocked for well {well_name} in grid"
|
1279
|
-
f"{f' {grid_name}' if grid_name is not None else ''}.")
|
1280
|
-
return None
|
1281
|
-
else:
|
1282
|
-
log.info(f"{blocked_count} interval{rqwu._pl(blocked_count)} blocked for well {well_name} in"
|
1283
|
-
f" grid{f' {grid_name}' if grid_name is not None else ''}.")
|
1284
|
-
|
1285
832
|
def dataframe(self,
|
1286
833
|
i_col = 'IW',
|
1287
834
|
j_col = 'JW',
|
@@ -1832,1483 +1379,2055 @@ class BlockedWell(BaseResqpy):
|
|
1832
1379
|
set_perforation_fraction = set_perforation_fraction,
|
1833
1380
|
set_frame_interval = set_frame_interval)
|
1834
1381
|
|
1835
|
-
def
|
1836
|
-
|
1837
|
-
|
1838
|
-
|
1839
|
-
|
1840
|
-
|
1841
|
-
|
1842
|
-
|
1843
|
-
return interval_count
|
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.
|
1844
1390
|
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
1851
|
-
|
1852
|
-
|
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
|
1853
1401
|
|
1854
|
-
|
1855
|
-
|
1856
|
-
# returns unit vector with true direction, ie. accounts for differing xy & z units in grid's crs
|
1857
|
-
# gravity = np.array((0.0, 0.0, 1.0))
|
1858
|
-
if mode == 'normal well i+':
|
1859
|
-
return None # ANGLA only: option for no projection onto a plane
|
1860
|
-
ref_vector = None
|
1861
|
-
# options for anglv or angla reference: 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down'
|
1862
|
-
if mode == 'z+':
|
1863
|
-
ref_vector = np.array((0.0, 0.0, 1.0))
|
1864
|
-
elif mode == 'z down':
|
1865
|
-
if grid_crs.z_inc_down:
|
1866
|
-
ref_vector = np.array((0.0, 0.0, 1.0))
|
1867
|
-
else:
|
1868
|
-
ref_vector = np.array((0.0, 0.0, -1.0))
|
1869
|
-
else:
|
1870
|
-
cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
|
1871
|
-
if grid_crs.xy_units != grid_crs.z_units:
|
1872
|
-
wam.convert_lengths(cell_axial_vectors[..., 2], grid_crs.z_units, grid_crs.xy_units)
|
1873
|
-
if mode in ['k+', 'k down']:
|
1874
|
-
ref_vector = vec.unit_vector(cell_axial_vectors[0])
|
1875
|
-
if mode == 'k down' and not grid.k_direction_is_down:
|
1876
|
-
ref_vector = -ref_vector
|
1877
|
-
else: # normal to plane of ij axes
|
1878
|
-
ref_vector = vec.unit_vector(vec.cross_product(cell_axial_vectors[1], cell_axial_vectors[2]))
|
1879
|
-
if mode == 'normal ij down':
|
1880
|
-
if grid_crs.z_inc_down:
|
1881
|
-
if ref_vector[2] < 0.0:
|
1882
|
-
ref_vector = -ref_vector
|
1883
|
-
else:
|
1884
|
-
if ref_vector[2] > 0.0:
|
1885
|
-
ref_vector = -ref_vector
|
1886
|
-
if ref_vector is None or ref_vector[2] == 0.0:
|
1887
|
-
if grid_crs.z_inc_down:
|
1888
|
-
ref_vector = np.array((0.0, 0.0, 1.0))
|
1889
|
-
else:
|
1890
|
-
ref_vector = np.array((0.0, 0.0, -1.0))
|
1891
|
-
return ref_vector
|
1402
|
+
returns:
|
1403
|
+
None
|
1892
1404
|
|
1893
|
-
|
1894
|
-
|
1895
|
-
|
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
|
1410
|
+
"""
|
1896
1411
|
|
1897
|
-
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
1906
|
-
angla_plane_ref = 'z down'
|
1907
|
-
return anglv_ref, angla_plane_ref
|
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
|
1908
1421
|
|
1909
|
-
|
1910
|
-
|
1911
|
-
|
1912
|
-
|
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)
|
1913
1460
|
|
1914
|
-
|
1915
|
-
|
1916
|
-
|
1917
|
-
|
1918
|
-
|
1919
|
-
|
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).
|
1920
1490
|
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
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
|
+
"""
|
1924
1495
|
|
1925
|
-
|
1926
|
-
|
1927
|
-
|
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)
|
1928
1528
|
|
1929
|
-
return
|
1529
|
+
return sum(df['KH'])
|
1930
1530
|
|
1931
|
-
|
1932
|
-
|
1933
|
-
|
1934
|
-
|
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.
|
1576
|
+
|
1577
|
+
returns:
|
1578
|
+
pandas DataFrame containing data that has been written to the wellspec file
|
1935
1579
|
|
1936
1580
|
note:
|
1937
|
-
|
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
|
1938
1583
|
"""
|
1939
1584
|
|
1940
|
-
|
1941
|
-
for extra in extra_columns_list:
|
1942
|
-
assert extra.upper() in [
|
1943
|
-
'GRID', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'PPERF', 'RADB',
|
1944
|
-
'WI', 'WBC', 'STAT'
|
1945
|
-
]
|
1946
|
-
column_list.append(extra.upper())
|
1947
|
-
else:
|
1948
|
-
add_as_properties = use_properties = False
|
1949
|
-
assert not (add_as_properties and use_properties)
|
1950
|
-
|
1951
|
-
column_list, skin, stat, radw = BlockedWell.__check_skin_stat_radw_to_be_added_as_properties(
|
1952
|
-
skin = skin, stat = stat, radw = radw, column_list = column_list)
|
1953
|
-
|
1954
|
-
return column_list, add_as_properties, use_properties, skin, stat, radw
|
1585
|
+
assert wellspec_file, 'no output file specified to write WELLSPEC to'
|
1955
1586
|
|
1956
|
-
|
1957
|
-
|
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
|
+
}
|
1958
1604
|
|
1959
|
-
|
1960
|
-
is not None]):
|
1961
|
-
log.warning(
|
1962
|
-
'both LENGTH and PPERF will include effects of partial perforation; only one should be used in WELLSPEC'
|
1963
|
-
)
|
1964
|
-
elif all([
|
1965
|
-
perforation_list is not None, 'LENGTH' not in column_list, 'PPERF' not in column_list, 'KH'
|
1966
|
-
not in column_list, 'WBC' not in column_list
|
1967
|
-
]):
|
1968
|
-
log.warning('perforation list supplied but no use of LENGTH, KH, PPERF nor WBC')
|
1605
|
+
well_name = self.__get_well_name(well_name = well_name)
|
1969
1606
|
|
1970
|
-
|
1971
|
-
|
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)
|
1972
1641
|
|
1973
|
-
|
1974
|
-
def __check_skin_stat_radw_to_be_added_as_properties(skin, stat, radw, column_list):
|
1975
|
-
"""Verify whether skin should be added as a property in the dataframe."""
|
1642
|
+
sep = ' ' if space_instead_of_tab_separator else '\t'
|
1976
1643
|
|
1977
|
-
|
1978
|
-
|
1644
|
+
with open(wellspec_file, mode = mode) as fp:
|
1645
|
+
for _ in range(preceeding_blank_lines):
|
1646
|
+
fp.write('\n')
|
1979
1647
|
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
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)
|
1985
1654
|
|
1655
|
+
BlockedWell.__write_wellspec_file_columns(df = df, fp = fp, col_width_dict = col_width_dict, sep = sep)
|
1986
1656
|
|
1987
|
-
|
1988
|
-
# stat = 'ON'
|
1657
|
+
fp.write('\n')
|
1989
1658
|
|
1990
|
-
|
1991
|
-
|
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')
|
1992
1665
|
|
1993
|
-
return
|
1666
|
+
return df
|
1994
1667
|
|
1995
|
-
|
1996
|
-
|
1997
|
-
# Verify that the I direction permeability has been specified if well inflow properties are to be added
|
1998
|
-
# to the dataframe.
|
1668
|
+
def kji0_marker(self, active_only = True):
|
1669
|
+
"""Convenience method returning (k0, j0, i0), grid_uuid of first blocked interval."""
|
1999
1670
|
|
2000
|
-
|
2001
|
-
|
2002
|
-
|
2003
|
-
|
2004
|
-
assert perm_i_uuid is not None, 'WI, RADB or WBC requested without I direction permeabilty being specified'
|
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
|
2005
1675
|
|
2006
|
-
|
1676
|
+
def xyz_marker(self, active_only = True):
|
1677
|
+
"""Convenience method returning (x, y, z), crs_uuid of perforation in first blocked interval.
|
2007
1678
|
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
1679
|
+
notes:
|
1680
|
+
active_only argument not yet in use;
|
1681
|
+
returns None, None if no blocked interval found
|
1682
|
+
"""
|
2012
1683
|
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
|
2018
|
-
|
2019
|
-
if
|
2020
|
-
|
2021
|
-
|
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
|
2022
1695
|
|
2023
|
-
|
1696
|
+
def create_feature_and_interpretation(self, shared_interpretation = True):
|
1697
|
+
"""Instantiate new empty WellboreFeature and WellboreInterpretation objects.
|
2024
1698
|
|
2025
|
-
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
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
|
2029
1729
|
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
2034
|
-
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2038
|
-
|
2039
|
-
|
2040
|
-
isotropic_perm = (bu.matching_uuids(perm_i_uuid, perm_j_uuid) and
|
2041
|
-
bu.matching_uuids(perm_i_uuid, perm_k_uuid))
|
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.
|
2042
1740
|
|
2043
|
-
|
1741
|
+
note:
|
1742
|
+
not usually called directly; used by import methods
|
1743
|
+
"""
|
2044
1744
|
|
2045
|
-
|
2046
|
-
|
2047
|
-
# verify that the k layers to be included in the dataframe exist within the appropriate range
|
1745
|
+
if not well_name:
|
1746
|
+
well_name = self.title
|
2048
1747
|
|
2049
|
-
|
2050
|
-
|
2051
|
-
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
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')
|
2057
1756
|
|
2058
|
-
|
2059
|
-
|
2060
|
-
|
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
|
2061
1773
|
|
2062
|
-
|
2063
|
-
|
2064
|
-
doing_xyz = any([('X' in column_list and 'X' not in pc_titles), ('Y' in column_list and 'Y' not in pc_titles),
|
2065
|
-
('DEPTH' in column_list and 'DEPTH' not in pc_titles)])
|
2066
|
-
doing_entry_exit = any([(doing_angles),
|
2067
|
-
('LENGTH' in column_list and 'LENGTH' not in pc_titles and length_mode == 'straight')])
|
1774
|
+
if create_feature_and_interp:
|
1775
|
+
self.create_feature_and_interpretation()
|
2068
1776
|
|
2069
|
-
|
2070
|
-
|
2071
|
-
|
2072
|
-
|
2073
|
-
|
2074
|
-
|
2075
|
-
|
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.
|
2076
1785
|
|
2077
|
-
|
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
|
2078
1789
|
|
2079
|
-
|
2080
|
-
|
2081
|
-
# verify that each grid's crs units are consistent in all directions
|
1790
|
+
:meta common:
|
1791
|
+
"""
|
2082
1792
|
|
2083
|
-
|
2084
|
-
log.error('creating blocked well dataframe without GRID column for well that intersects more than one grid')
|
2085
|
-
grid_crs_list = []
|
2086
|
-
for grid in self.grid_list:
|
2087
|
-
grid_crs = crs.Crs(self.model, uuid = grid.crs_uuid)
|
2088
|
-
grid_crs_list.append(grid_crs)
|
2089
|
-
return grid_crs_list
|
1793
|
+
assert self.trajectory is not None, 'trajectory object missing'
|
2090
1794
|
|
2091
|
-
|
1795
|
+
if ext_uuid is None:
|
1796
|
+
ext_uuid = self.model.h5_uuid()
|
2092
1797
|
|
2093
|
-
if
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2098
|
-
traj_z_inc_down = traj_crs.z_inc_down
|
1798
|
+
if title:
|
1799
|
+
self.title = title
|
1800
|
+
if not self.title:
|
1801
|
+
self.title = self.well_name
|
1802
|
+
title = self.title
|
2099
1803
|
|
2100
|
-
|
1804
|
+
self.__create_wellbore_feature_and_interpretation_xml_if_needed(add_as_part = add_as_part,
|
1805
|
+
add_relationships = add_relationships,
|
1806
|
+
originator = originator)
|
2101
1807
|
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
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)
|
2105
1814
|
|
2106
|
-
|
2107
|
-
if max_depth is not None:
|
2108
|
-
cell_depth = grid.centre_point(cell_kji0)[2]
|
2109
|
-
if not grid_crs.z_inc_down:
|
2110
|
-
cell_depth = -cell_depth
|
2111
|
-
if cell_depth > max_depth:
|
2112
|
-
max_depth_exceeded = True
|
2113
|
-
return max_depth_exceeded
|
1815
|
+
assert self.trajectory.root is not None, 'trajectory xml not established'
|
2114
1816
|
|
2115
|
-
|
2116
|
-
def __skip_interval_check(max_depth, grid, cell_kji0, grid_crs, active_only, tuple_kji0, min_k0, max_k0, k0_list,
|
2117
|
-
region_list, region_uuid, max_satw, satw_uuid, min_sato, sato_uuid, max_satg, satg_uuid):
|
2118
|
-
"""Check whether any conditions are met that mean the interval should be skipped."""
|
1817
|
+
bw_node = super().create_xml(title = title, originator = originator, add_as_part = False)
|
2119
1818
|
|
2120
|
-
|
2121
|
-
grid = grid,
|
2122
|
-
cell_kji0 = cell_kji0,
|
2123
|
-
grid_crs = grid_crs)
|
2124
|
-
inactive_grid = active_only and grid.inactive is not None and grid.inactive[tuple_kji0]
|
2125
|
-
out_of_bounds_layer_1 = (min_k0 is not None and cell_kji0[0] < min_k0) or (max_k0 is not None and
|
2126
|
-
cell_kji0[0] > max_k0)
|
2127
|
-
out_of_bounds_layer_2 = k0_list is not None and cell_kji0[0] not in k0_list
|
2128
|
-
out_of_bounds_region = (region_list is not None and
|
2129
|
-
BlockedWell.__prop_array(region_uuid, grid)[tuple_kji0] not in region_list)
|
2130
|
-
saturation_limit_exceeded_1 = (max_satw is not None and
|
2131
|
-
BlockedWell.__prop_array(satw_uuid, grid)[tuple_kji0] > max_satw)
|
2132
|
-
saturation_limit_exceeded_2 = (min_sato is not None and
|
2133
|
-
BlockedWell.__prop_array(sato_uuid, grid)[tuple_kji0] < min_sato)
|
2134
|
-
saturation_limit_exceeded_3 = (max_satg is not None and
|
2135
|
-
BlockedWell.__prop_array(satg_uuid, grid)[tuple_kji0] > max_satg)
|
2136
|
-
skip_interval = any([
|
2137
|
-
max_depth_exceeded, inactive_grid, out_of_bounds_layer_1, out_of_bounds_layer_2, out_of_bounds_region,
|
2138
|
-
saturation_limit_exceeded_1, saturation_limit_exceeded_2, saturation_limit_exceeded_3
|
2139
|
-
])
|
1819
|
+
# wellbore frame elements
|
2140
1820
|
|
2141
|
-
|
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)
|
2142
1824
|
|
2143
|
-
|
2144
|
-
|
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)
|
2145
1830
|
|
2146
|
-
|
2147
|
-
|
2148
|
-
part_perf_fraction = pc.single_array_ref(citation_title = 'PPERF')[ci]
|
2149
|
-
else:
|
2150
|
-
part_perf_fraction = 1.0
|
2151
|
-
if perforation_list is not None:
|
2152
|
-
perf_length = 0.0
|
2153
|
-
for perf_start, perf_end in perforation_list:
|
2154
|
-
if perf_end <= self.node_mds[interval] or perf_start >= self.node_mds[interval + 1]:
|
2155
|
-
continue
|
2156
|
-
if perf_start <= self.node_mds[interval]:
|
2157
|
-
if perf_end >= self.node_mds[interval + 1]:
|
2158
|
-
perf_length += self.node_mds[interval + 1] - self.node_mds[interval]
|
2159
|
-
break
|
2160
|
-
else:
|
2161
|
-
perf_length += perf_end - self.node_mds[interval]
|
2162
|
-
else:
|
2163
|
-
if perf_end >= self.node_mds[interval + 1]:
|
2164
|
-
perf_length += self.node_mds[interval + 1] - perf_start
|
2165
|
-
else:
|
2166
|
-
perf_length += perf_end - perf_start
|
2167
|
-
if perf_length < length_tol:
|
2168
|
-
skip_interval = True
|
2169
|
-
perf_length = 0.0
|
2170
|
-
part_perf_fraction = min(1.0, perf_length / (self.node_mds[interval + 1] - self.node_mds[interval]))
|
1831
|
+
traj_root, grid_root, interp_root = self.__create_trajectory_grid_wellbore_interpretation_reference_nodes(
|
1832
|
+
bw_node = bw_node)
|
2171
1833
|
|
2172
|
-
|
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)
|
2173
1839
|
|
2174
|
-
|
2175
|
-
ci, grid_crs, traj_crs):
|
2176
|
-
# calculate the entry and exit points for the interval and set the entry and exit coordinate reference system
|
1840
|
+
return bw_node
|
2177
1841
|
|
2178
|
-
|
2179
|
-
|
2180
|
-
ee_crs = None
|
2181
|
-
if doing_entry_exit:
|
2182
|
-
assert self.trajectory is not None
|
2183
|
-
if use_face_centres:
|
2184
|
-
entry_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0], self.face_pair_indices[ci, 0,
|
2185
|
-
1])
|
2186
|
-
if self.face_pair_indices[ci, 1, 0] >= 0:
|
2187
|
-
exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 1, 0],
|
2188
|
-
self.face_pair_indices[ci, 1, 1])
|
2189
|
-
else:
|
2190
|
-
exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0],
|
2191
|
-
1 - self.face_pair_indices[ci, 0, 1])
|
2192
|
-
ee_crs = grid_crs
|
2193
|
-
else:
|
2194
|
-
entry_xyz = self.trajectory.xyz_for_md(self.node_mds[interval])
|
2195
|
-
exit_xyz = self.trajectory.xyz_for_md(self.node_mds[interval + 1])
|
2196
|
-
ee_crs = traj_crs
|
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.
|
2197
1844
|
|
2198
|
-
|
1845
|
+
:meta common:
|
1846
|
+
"""
|
2199
1847
|
|
2200
|
-
|
2201
|
-
part_perf_fraction, min_length):
|
2202
|
-
"""Calculate the length of the interval."""
|
1848
|
+
# NB: array data must all have been set up prior to calling this function
|
2203
1849
|
|
2204
|
-
|
2205
|
-
|
2206
|
-
length = self.node_mds[interval + 1] - self.node_mds[interval]
|
2207
|
-
if length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
|
2208
|
-
length = wam.convert_lengths(length, self.trajectory.md_uom, length_uom)
|
2209
|
-
else: # use straight line length between entry and exit
|
2210
|
-
entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
|
2211
|
-
length = vec.naive_length(exit_xyz - entry_xyz)
|
2212
|
-
if length_uom is not None:
|
2213
|
-
length = wam.convert_lengths(length, ee_crs.z_units, length_uom)
|
2214
|
-
elif self.trajectory is not None:
|
2215
|
-
length = wam.convert_lengths(length, ee_crs.z_units, self.trajectory.md_uom)
|
2216
|
-
if perforation_list is not None:
|
2217
|
-
length *= part_perf_fraction
|
2218
|
-
if min_length is not None and length < min_length:
|
2219
|
-
skip_interval = True
|
1850
|
+
if self.uuid is None:
|
1851
|
+
self.uuid = bu.new_uuid()
|
2220
1852
|
|
2221
|
-
|
1853
|
+
h5_reg = rwh5.H5Register(self.model)
|
2222
1854
|
|
2223
|
-
|
2224
|
-
|
2225
|
-
|
2226
|
-
return None
|
2227
|
-
xyz = np.array(xyz, dtype = float)
|
2228
|
-
if crs.xy_units != required_uom:
|
2229
|
-
xyz[0] = wam.convert_lengths(xyz[0], crs.xy_units, required_uom)
|
2230
|
-
xyz[1] = wam.convert_lengths(xyz[1], crs.xy_units, required_uom)
|
2231
|
-
if crs.z_units != required_uom:
|
2232
|
-
xyz[2] = wam.convert_lengths(xyz[2], crs.z_units, required_uom)
|
2233
|
-
return xyz
|
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'
|
2234
1858
|
|
2235
|
-
|
2236
|
-
|
2237
|
-
|
2238
|
-
|
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)
|
2239
1870
|
|
2240
|
-
|
2241
|
-
k_face_check_end, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs,
|
2242
|
-
cell_kji0, anglv_ref, angla_plane_ref):
|
2243
|
-
"""Calculate angla, anglv and related trigonometirc transforms for the interval."""
|
1871
|
+
h5_reg.register_dataset(self.uuid, 'LocalFacePairPerCellIndices', raw_face_indices) # could use uint8?
|
2244
1872
|
|
2245
|
-
|
2246
|
-
cosine_anglv = cosine_angla = 1.0
|
2247
|
-
anglv = pc.single_array_ref(citation_title = 'ANGLV')[ci] if 'ANGLV' in pc_titles else None
|
2248
|
-
angla = pc.single_array_ref(citation_title = 'ANGLA')[ci] if 'ANGLA' in pc_titles else None
|
1873
|
+
h5_reg.write(file = file_name, mode = mode)
|
2249
1874
|
|
2250
|
-
|
2251
|
-
|
2252
|
-
np.all(self.face_pair_indices[ci] == k_face_check_end))):
|
2253
|
-
anglv, sine_anglv, cosine_anglv, vector, a_ref_vector = BlockedWell.__get_anglv_for_interval(
|
2254
|
-
anglv = anglv,
|
2255
|
-
entry_xyz = entry_xyz,
|
2256
|
-
exit_xyz = exit_xyz,
|
2257
|
-
ee_crs = ee_crs,
|
2258
|
-
traj_z_inc_down = traj_z_inc_down,
|
2259
|
-
grid = grid,
|
2260
|
-
grid_crs = grid_crs,
|
2261
|
-
cell_kji0 = cell_kji0,
|
2262
|
-
anglv_ref = anglv_ref,
|
2263
|
-
angla_plane_ref = angla_plane_ref)
|
2264
|
-
if anglv != 0.0:
|
2265
|
-
angla, sine_angla, cosine_angla = BlockedWell.__get_angla_for_interval(angla = angla,
|
2266
|
-
grid = grid,
|
2267
|
-
cell_kji0 = cell_kji0,
|
2268
|
-
vector = vector,
|
2269
|
-
a_ref_vector = a_ref_vector)
|
2270
|
-
if angla is None:
|
2271
|
-
angla = 0.0
|
2272
|
-
if anglv is None:
|
2273
|
-
anglv = 0.0
|
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."""
|
2274
1877
|
|
2275
|
-
|
1878
|
+
part_list = [self.model.part_for_uuid(uuid) for uuid in uuid_list]
|
2276
1879
|
|
2277
|
-
|
2278
|
-
|
2279
|
-
|
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")
|
2280
1887
|
|
2281
|
-
|
2282
|
-
|
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")
|
2283
1893
|
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2288
|
-
|
2289
|
-
|
2290
|
-
|
2291
|
-
|
2292
|
-
|
2293
|
-
|
2294
|
-
|
2295
|
-
|
2296
|
-
if angla is not None:
|
2297
|
-
angla_rad = vec.radians_from_degrees(angla)
|
2298
|
-
cosine_angla = maths.cos(angla_rad)
|
2299
|
-
sine_angla = maths.sin(angla_rad)
|
2300
|
-
else:
|
2301
|
-
cosine_angla = min(max(vec.dot_product(vector, i_axis), -1.0), 1.0)
|
2302
|
-
angla_rad = maths.acos(cosine_angla)
|
2303
|
-
# negate angla if vector is 'clockwise from' i_axis when viewed from above, projected in the xy plane
|
2304
|
-
# todo: have discussion around angla sign under different ijk handedness (and z inc direction?)
|
2305
|
-
sine_angla = maths.sin(angla_rad)
|
2306
|
-
angla = vec.degrees_from_radians(angla_rad)
|
2307
|
-
if vec.clockwise((0.0, 0.0), i_axis, vector) > 0.0:
|
2308
|
-
angla = -angla
|
2309
|
-
angla_rad = -angla_rad ## as angle_rad before --> typo?
|
2310
|
-
sine_angla = -sine_angla
|
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}
|
2311
1906
|
|
2312
|
-
|
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
|
2313
1922
|
|
2314
|
-
|
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
|
+
)
|
2315
1954
|
|
2316
|
-
|
2317
|
-
|
2318
|
-
anglv_ref, angla_plane_ref):
|
2319
|
-
"""Get anglv and related trigonometric transforms for the interval."""
|
1955
|
+
def _set_cell_interval_map(self):
|
1956
|
+
"""Sets up an index mapping from blocked cell index to interval index, accounting for unblocked intervals."""
|
2320
1957
|
|
2321
|
-
|
2322
|
-
|
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
|
2323
1966
|
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
|
2329
|
-
unit_adjusted_vector = vector
|
2330
|
-
else:
|
2331
|
-
unit_adjusted_vector = vector.copy()
|
2332
|
-
unit_adjusted_vector[2] = wam.convert_lengths(unit_adjusted_vector[2], grid.crs.z_units, grid.crs.xy_units)
|
2333
|
-
v_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, anglv_ref)
|
2334
|
-
# log.debug('v ref vector: ' + str(v_ref_vector))
|
2335
|
-
if angla_plane_ref == anglv_ref:
|
2336
|
-
a_ref_vector = v_ref_vector
|
2337
|
-
else:
|
2338
|
-
a_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, angla_plane_ref)
|
2339
|
-
# log.debug('a ref vector: ' + str(a_ref_vector))
|
2340
|
-
if anglv is not None:
|
2341
|
-
anglv_rad = vec.radians_from_degrees(anglv)
|
2342
|
-
cosine_anglv = maths.cos(anglv_rad)
|
2343
|
-
sine_anglv = maths.sin(anglv_rad)
|
1967
|
+
def __derive_from_wellspec_check_well_name(self, well_name):
|
1968
|
+
"""Set the well name to be used in the wellspec file."""
|
1969
|
+
|
1970
|
+
if well_name:
|
1971
|
+
self.well_name = well_name
|
2344
1972
|
else:
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
2348
|
-
|
2349
|
-
# log.debug('anglv: ' + str(anglv))
|
1973
|
+
well_name = self.well_name
|
1974
|
+
if not self.title:
|
1975
|
+
self.title = well_name
|
1976
|
+
return well_name
|
2350
1977
|
|
2351
|
-
|
1978
|
+
@staticmethod
|
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
|
2352
1987
|
|
2353
1988
|
@staticmethod
|
2354
|
-
def
|
2355
|
-
|
2356
|
-
|
2357
|
-
|
2358
|
-
|
2359
|
-
|
2360
|
-
|
2361
|
-
|
2362
|
-
|
2363
|
-
|
2364
|
-
ntg_is_one = True
|
2365
|
-
else:
|
2366
|
-
ntg = BlockedWell.__prop_array(ntg_uuid, grid)[tuple_kji0]
|
2367
|
-
ntg_is_one = maths.isclose(ntg, 1.0, rel_tol = 0.001)
|
2368
|
-
if isotropic_perm and ntg_is_one:
|
2369
|
-
k_i = k_j = k_k = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
|
2370
|
-
else:
|
2371
|
-
if preferential_perforation and not ntg_is_one:
|
2372
|
-
if part_perf_fraction <= ntg:
|
2373
|
-
ntg = 1.0 # effective ntg when perforated intervals are in pay
|
2374
|
-
else:
|
2375
|
-
ntg /= part_perf_fraction # adjusted ntg when some perforations in non-pay
|
2376
|
-
# todo: check netgross facet type in property perm i & j parts: if set to gross then don't multiply by ntg below
|
2377
|
-
k_i = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0] * ntg
|
2378
|
-
k_j = BlockedWell.__prop_array(perm_j_uuid, grid)[tuple_kji0] * ntg
|
2379
|
-
k_k = BlockedWell.__prop_array(perm_k_uuid, grid)[tuple_kji0]
|
2380
|
-
|
2381
|
-
return ntg_is_one, k_i, k_j, k_k
|
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
|
2382
1999
|
|
2383
2000
|
@staticmethod
|
2384
|
-
def
|
2385
|
-
|
2386
|
-
|
2387
|
-
|
2388
|
-
|
2389
|
-
|
2390
|
-
if doing_kh:
|
2391
|
-
kh = BlockedWell.__get_kh_if_doing_kh(isotropic_perm = isotropic_perm,
|
2392
|
-
ntg_is_one = ntg_is_one,
|
2393
|
-
length = length,
|
2394
|
-
perm_i_uuid = perm_i_uuid,
|
2395
|
-
grid = grid,
|
2396
|
-
tuple_kji0 = tuple_kji0,
|
2397
|
-
k_i = k_i,
|
2398
|
-
k_j = k_j,
|
2399
|
-
k_k = k_k,
|
2400
|
-
anglv = anglv,
|
2401
|
-
sine_anglv = sine_anglv,
|
2402
|
-
cosine_anglv = cosine_anglv,
|
2403
|
-
sine_angla = sine_angla,
|
2404
|
-
cosine_angla = cosine_angla)
|
2405
|
-
if min_kh is not None and kh < min_kh:
|
2406
|
-
skip_interval = True
|
2407
|
-
elif 'KH' in pc_titles:
|
2408
|
-
kh = pc.single_array_ref(citation_title = 'KH')[ci]
|
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)
|
2409
2007
|
else:
|
2410
|
-
|
2411
|
-
|
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)
|
2412
2013
|
|
2413
|
-
|
2414
|
-
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2418
|
-
|
2419
|
-
|
2420
|
-
else:
|
2421
|
-
if np.isnan(k_i) or np.isnan(k_j):
|
2422
|
-
kh = 0.0
|
2423
|
-
elif anglv == 0.0:
|
2424
|
-
kh = length * maths.sqrt(k_i * k_j)
|
2425
|
-
elif np.isnan(k_k):
|
2426
|
-
kh = 0.0
|
2427
|
-
else:
|
2428
|
-
k_e = maths.pow(k_i * k_j * k_k, 1.0 / 3.0)
|
2429
|
-
if k_e == 0.0:
|
2430
|
-
kh = 0.0
|
2431
|
-
else:
|
2432
|
-
l_i = length * maths.sqrt(k_e / k_i) * sine_anglv * cosine_angla
|
2433
|
-
l_j = length * maths.sqrt(k_e / k_j) * sine_anglv * sine_angla
|
2434
|
-
l_k = length * maths.sqrt(k_e / k_k) * cosine_anglv
|
2435
|
-
l_p = maths.sqrt(l_i * l_i + l_j * l_j + l_k * l_k)
|
2436
|
-
kh = k_e * l_p
|
2437
|
-
return kh
|
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)
|
2438
2021
|
|
2439
|
-
|
2440
|
-
def __get_pc_arrays_for_interval(pc, pc_timeless, pc_titles, ci, length, radw, skin, stat, length_uom, grid,
|
2441
|
-
traj_crs):
|
2442
|
-
"""Get the property collection arrays for the interval."""
|
2022
|
+
return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
|
2443
2023
|
|
2444
|
-
|
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."""
|
2445
2027
|
|
2446
|
-
|
2447
|
-
|
2448
|
-
|
2449
|
-
|
2450
|
-
|
2451
|
-
|
2452
|
-
|
2453
|
-
|
2028
|
+
angla = row['ANGLA']
|
2029
|
+
inclination = row['ANGLV']
|
2030
|
+
if inclination < 0.001:
|
2031
|
+
azimuth = 0.0
|
2032
|
+
else:
|
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
|
2454
2043
|
|
2455
|
-
|
2456
|
-
pk = pk_for_title(title)
|
2457
|
-
pc_uom = None
|
2458
|
-
for try_pc in [pc, pc_timeless]:
|
2459
|
-
if try_pc is None:
|
2460
|
-
continue
|
2461
|
-
if title in pc_titles:
|
2462
|
-
p = try_pc.singleton(citation_title = title)
|
2463
|
-
if p is None and pk is not None:
|
2464
|
-
p = try_pc.singleton(property_kind = pk)
|
2465
|
-
if p is not None:
|
2466
|
-
v = try_pc.cached_part_array_ref(p)[ci]
|
2467
|
-
pc_uom = try_pc.uom_for_part(p)
|
2468
|
-
break
|
2469
|
-
if (title == 'STAT' or pk == 'well connection open') and v is not None and not isinstance(v, str):
|
2470
|
-
v = 'ON' if v else 'OFF'
|
2471
|
-
if pc_uom is not None and uom is not None and pc_uom != uom:
|
2472
|
-
v = wam.convert_lengths(v, pc_uom, uom)
|
2473
|
-
return v
|
2044
|
+
def __calculate_entry_and_exit_axes_polarities_and_points_using_indices(df, i, cell_kji0, blocked_cells_kji0):
|
2474
2045
|
|
2475
|
-
|
2476
|
-
|
2477
|
-
|
2478
|
-
|
2479
|
-
l_uom = length_uom
|
2480
|
-
r_uom = length_uom
|
2481
|
-
length = get_item(length, 'LENGTH', pc_titles, pc, pc_timeless, ci, l_uom)
|
2482
|
-
radw = get_item(radw, 'RADW', pc_titles, pc, pc_timeless, ci, r_uom)
|
2483
|
-
stat = get_item(stat, 'STAT', pc_titles, pc, pc_timeless, ci, None)
|
2484
|
-
assert radw is None or radw > 0.0 # todo: allow zero for inactive intervals?
|
2485
|
-
skin = get_item(skin, 'SKIN', pc_titles, pc, pc_timeless, ci, None)
|
2486
|
-
if skin is None:
|
2487
|
-
skin = get_item(None, 'skin', pc_titles, pc, pc_timeless, ci, None)
|
2488
|
-
radb = get_item(None, 'RADB', pc_titles, pc, pc_timeless, ci, r_uom)
|
2489
|
-
if radb is None:
|
2490
|
-
radb = get_item(None, 'block equivalent radius', pc_titles, pc, pc_timeless, ci, r_uom)
|
2491
|
-
wi = get_item(None, 'WI', pc_titles, pc, pc_timeless, ci, None)
|
2492
|
-
wbc = get_item(None, 'WBC', pc_titles, pc, pc_timeless, ci, None)
|
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)
|
2493
2050
|
|
2494
|
-
return
|
2051
|
+
return entry_axis, entry_polarity, exit_axis, exit_polarity
|
2495
2052
|
|
2496
2053
|
@staticmethod
|
2497
|
-
def
|
2498
|
-
|
2499
|
-
wi, wbc, skin, kh, length_uom, column_list):
|
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.
|
2500
2056
|
|
2501
|
-
|
2502
|
-
|
2503
|
-
|
2504
|
-
k_ei, k_ej, k_ek, radw_e = BlockedWell.__calculate_ke_and_radw_e(isotropic_perm = isotropic_perm,
|
2505
|
-
ntg_is_one = ntg_is_one,
|
2506
|
-
radw = radw,
|
2507
|
-
k_i = k_i,
|
2508
|
-
k_j = k_j,
|
2509
|
-
k_k = k_k,
|
2510
|
-
sine_anglv = sine_anglv,
|
2511
|
-
cosine_anglv = cosine_anglv,
|
2512
|
-
sine_angla = sine_angla,
|
2513
|
-
cosine_angla = cosine_angla)
|
2057
|
+
note:
|
2058
|
+
could use geometry but here a cheap rough-and-ready approach is used
|
2059
|
+
"""
|
2514
2060
|
|
2515
|
-
|
2516
|
-
|
2517
|
-
|
2518
|
-
|
2519
|
-
|
2520
|
-
|
2521
|
-
|
2522
|
-
|
2523
|
-
|
2524
|
-
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
|
2530
|
-
cosine_anglv = cosine_anglv,
|
2531
|
-
sine_angla = sine_angla,
|
2532
|
-
cosine_angla = cosine_angla)
|
2533
|
-
radb = radw * radb_e / radw_e
|
2534
|
-
log.debug(f'RADB value calculated in BlockedWell dataframe method as: {radb}')
|
2535
|
-
if wi is None:
|
2536
|
-
wi = 0.0 if radb <= 0.0 else 2.0 * maths.pi / (maths.log(radb / radw) + skin)
|
2537
|
-
if 'WBC' in column_list and wbc is None:
|
2538
|
-
assert length_uom == 'm' or length_uom.startswith('ft'), \
|
2539
|
-
'WBC only calculable for length uom of m or ft*'
|
2540
|
-
conversion_constant = 8.5270171e-5 if length_uom == 'm' else 0.006328286
|
2541
|
-
wbc = conversion_constant * kh * wi # note: pperf aleady accounted for in kh
|
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
|
2542
2076
|
|
2543
|
-
|
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
|
2085
|
+
else:
|
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
|
2544
2098
|
|
2545
2099
|
@staticmethod
|
2546
|
-
def
|
2547
|
-
|
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."""
|
2548
2103
|
|
2549
|
-
if
|
2550
|
-
|
2551
|
-
|
2552
|
-
|
2553
|
-
|
2554
|
-
|
2555
|
-
|
2556
|
-
|
2557
|
-
|
2558
|
-
|
2559
|
-
|
2560
|
-
|
2561
|
-
|
2562
|
-
|
2563
|
-
if radw_e == 0.0:
|
2564
|
-
radw_e = radw # no permeability in this situation anyway
|
2565
|
-
|
2566
|
-
return k_ei, k_ej, k_ek, radw_e
|
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
|
2567
2118
|
|
2568
2119
|
@staticmethod
|
2569
|
-
def
|
2570
|
-
|
2571
|
-
|
2572
|
-
|
2573
|
-
|
2574
|
-
|
2575
|
-
|
2576
|
-
|
2577
|
-
|
2578
|
-
|
2579
|
-
|
2580
|
-
|
2581
|
-
|
2582
|
-
|
2583
|
-
|
2584
|
-
|
2585
|
-
|
2586
|
-
|
2587
|
-
|
2588
|
-
|
2589
|
-
|
2590
|
-
|
2591
|
-
|
2592
|
-
|
2593
|
-
|
2594
|
-
exit_xyz = exit_xyz,
|
2595
|
-
ee_crs = ee_crs)
|
2596
|
-
xyz = np.array(xyz)
|
2597
|
-
for i, col_header in enumerate(['X', 'Y', 'DEPTH']):
|
2598
|
-
if col_header in pc_titles:
|
2599
|
-
xyz[i] = pc.single_array_ref(citation_title = col_header)[ci]
|
2600
|
-
|
2601
|
-
return xyz
|
2602
|
-
|
2603
|
-
def __get_xyz_if_doing_xyz(self, length_mode, md, length_uom, traj_crs, depth_inc_down, traj_z_inc_down, exit_xyz,
|
2604
|
-
entry_xyz, ee_crs):
|
2605
|
-
|
2606
|
-
if length_mode == 'MD' and self.trajectory is not None:
|
2607
|
-
xyz = self.trajectory.xyz_for_md(md)
|
2608
|
-
if length_uom is not None and length_uom != self.trajectory.md_uom:
|
2609
|
-
wam.convert_lengths(xyz, traj_crs.z_units, length_uom)
|
2610
|
-
if depth_inc_down and traj_z_inc_down is False:
|
2611
|
-
xyz[2] = -xyz[2]
|
2612
|
-
else:
|
2613
|
-
xyz = 0.5 * (np.array(exit_xyz) + np.array(entry_xyz))
|
2614
|
-
if length_uom is not None and length_uom != ee_crs.z_units:
|
2615
|
-
xyz[2] = wam.convert_lengths(xyz[2], ee_crs.z_units, length_uom)
|
2616
|
-
if depth_inc_down != ee_crs.z_inc_down:
|
2617
|
-
xyz[2] = -xyz[2]
|
2618
|
-
|
2619
|
-
return xyz
|
2620
|
-
|
2621
|
-
def __get_md_array_in_correct_units_for_interval(self, md, length_uom, pc, pc_titles, ci):
|
2622
|
-
"""Convert the measured depth to the correct units or get the measured depth from the property collection."""
|
2623
|
-
|
2624
|
-
if 'MD' in pc_titles:
|
2625
|
-
md = pc.single_array_ref(citation_title = 'MD')[ci]
|
2626
|
-
elif length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
|
2627
|
-
md = wam.convert_lengths(md, self.trajectory.md_uom, length_uom)
|
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)))
|
2628
2145
|
|
2629
|
-
return
|
2146
|
+
return previous_xyz, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0, blocked_face_pairs
|
2630
2147
|
|
2631
2148
|
@staticmethod
|
2632
|
-
def
|
2633
|
-
|
2634
|
-
|
2635
|
-
|
2636
|
-
|
2637
|
-
|
2638
|
-
|
2639
|
-
'WI', 'WBC'
|
2640
|
-
]
|
2641
|
-
column_values = [
|
2642
|
-
grid_name, radw, skin, angla, anglv, length, kh, xyz[2], md, xyz[0], xyz[1], stat, part_perf_fraction, radb,
|
2643
|
-
wi, wbc
|
2644
|
-
]
|
2645
|
-
column_values_dict = dict(zip(column_names, column_values))
|
2646
|
-
|
2647
|
-
data = df.to_dict()
|
2648
|
-
data = {k: list(v.values()) for k, v in data.items()}
|
2649
|
-
for col_index, col in enumerate(column_list):
|
2650
|
-
if col_index < 3:
|
2651
|
-
if one_based:
|
2652
|
-
row_dict[col] = [cell_kji0[2 - col_index] + 1]
|
2653
|
-
else:
|
2654
|
-
row_dict[col] = [cell_kji0[2 - col_index]]
|
2655
|
-
else:
|
2656
|
-
row_dict[col] = [column_values_dict[col]]
|
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))
|
2657
2156
|
|
2658
|
-
|
2659
|
-
|
2660
|
-
|
2661
|
-
|
2662
|
-
data[col] = vals
|
2663
|
-
df = pd.DataFrame(data)
|
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."""
|
2664
2161
|
|
2665
|
-
|
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)
|
2666
2170
|
|
2667
|
-
return
|
2171
|
+
return trajectory_points, trajectory_mds
|
2668
2172
|
|
2669
|
-
def
|
2670
|
-
|
2671
|
-
|
2672
|
-
|
2673
|
-
|
2674
|
-
|
2675
|
-
|
2676
|
-
|
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
|
2677
2181
|
|
2678
|
-
if add_as_properties:
|
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()
|
2679
2186
|
if isinstance(add_as_properties, list):
|
2680
2187
|
for col in add_as_properties:
|
2681
|
-
assert col in
|
2188
|
+
assert col in df.columns[3:] # could just skip missing columns
|
2682
2189
|
property_columns = add_as_properties
|
2683
2190
|
else:
|
2684
|
-
property_columns =
|
2191
|
+
property_columns = df.columns[3:]
|
2685
2192
|
self.add_df_properties(df,
|
2686
2193
|
property_columns,
|
2687
2194
|
length_uom = length_uom,
|
2688
2195
|
time_index = time_index,
|
2689
2196
|
time_series_uuid = time_series_uuid)
|
2690
2197
|
|
2691
|
-
def
|
2692
|
-
|
2693
|
-
columns,
|
2694
|
-
length_uom = None,
|
2695
|
-
time_index = None,
|
2696
|
-
time_series_uuid = None,
|
2697
|
-
realization = None):
|
2698
|
-
"""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."""
|
2699
2200
|
|
2700
|
-
|
2701
|
-
|
2702
|
-
|
2703
|
-
|
2704
|
-
|
2705
|
-
|
2706
|
-
time_series_uuid (uuid.UUID, optional): if adding a timestamp to the property, this is
|
2707
|
-
the uuid of the TimeSeries object
|
2708
|
-
realization (int, optional): if present, is used as the realization number for all the
|
2709
|
-
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
|
2710
2207
|
|
2711
|
-
|
2712
|
-
|
2208
|
+
def __check_cellio_init_okay(self, cellio_file, well_name, grid):
|
2209
|
+
"""Checks if BlockedWell object initialization from a cellio file is okay."""
|
2713
2210
|
|
2714
|
-
|
2715
|
-
|
2716
|
-
|
2717
|
-
this method currently only handles single grid situations;
|
2718
|
-
dataframe rows must be in the same order as the cells in the blocked well
|
2719
|
-
"""
|
2211
|
+
okay = self.import_from_rms_cellio(cellio_file, well_name, grid)
|
2212
|
+
if not okay:
|
2213
|
+
self.node_count = 0
|
2720
2214
|
|
2721
|
-
|
2722
|
-
|
2723
|
-
if columns is None or len(columns) == 0 or len(df) == 0:
|
2724
|
-
return
|
2725
|
-
if length_uom is None:
|
2726
|
-
length_uom = self.trajectory.md_uom
|
2727
|
-
extra_pc = rqp.PropertyCollection()
|
2728
|
-
extra_pc.set_support(support = self)
|
2729
|
-
assert len(df) == self.cell_count
|
2215
|
+
def _load_from_xml(self):
|
2216
|
+
"""Loads the blocked wellbore object from an xml node (and associated hdf5 data)."""
|
2730
2217
|
|
2731
|
-
|
2732
|
-
|
2733
|
-
uom, pk, discrete = self._get_uom_pk_discrete_for_df_properties(extra = extra, length_uom = length_uom)
|
2734
|
-
if discrete:
|
2735
|
-
null_value = -1
|
2736
|
-
na_value = -1
|
2737
|
-
dtype = np.int32
|
2738
|
-
else:
|
2739
|
-
null_value = None
|
2740
|
-
na_value = np.nan
|
2741
|
-
dtype = float
|
2742
|
-
# 'SKIN': use defaults for now; todo: create local property kind for skin
|
2743
|
-
if column == 'STAT':
|
2744
|
-
col_as_list = list(df[column])
|
2745
|
-
expanded = np.array([(0 if (str(st).upper() in ['OFF', '0', 'FALSE']) else 1) for st in col_as_list],
|
2746
|
-
dtype = np.int8)
|
2747
|
-
dtype = np.int8
|
2748
|
-
else:
|
2749
|
-
expanded = df[column].to_numpy(dtype = dtype, copy = True, na_value = na_value)
|
2750
|
-
extra_pc.add_cached_array_to_imported_list(
|
2751
|
-
expanded,
|
2752
|
-
'blocked well dataframe',
|
2753
|
-
extra,
|
2754
|
-
discrete = discrete,
|
2755
|
-
uom = uom,
|
2756
|
-
property_kind = pk,
|
2757
|
-
local_property_kind_uuid = None,
|
2758
|
-
facet_type = None,
|
2759
|
-
facet = None,
|
2760
|
-
realization = realization,
|
2761
|
-
indexable_element = 'cells',
|
2762
|
-
count = 1,
|
2763
|
-
time_index = time_index,
|
2764
|
-
null_value = null_value,
|
2765
|
-
)
|
2766
|
-
extra_pc.write_hdf5_for_imported_list()
|
2767
|
-
extra_pc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_series_uuid,
|
2768
|
-
find_local_property_kinds = True)
|
2218
|
+
node = self.root
|
2219
|
+
assert node is not None
|
2769
2220
|
|
2770
|
-
|
2771
|
-
"""Set the property kind and unit of measure for all properties in the dataframe."""
|
2221
|
+
self.__find_trajectory_uuid(node = node)
|
2772
2222
|
|
2773
|
-
|
2774
|
-
|
2775
|
-
raise ValueError(f"The length_uom {length_uom} must be either 'm' or 'ft'.")
|
2776
|
-
if extra == 'TEMP' and (temperature_uom is None or
|
2777
|
-
temperature_uom not in wam.valid_uoms('thermodynamic temperature')):
|
2778
|
-
raise ValueError(f"The temperature_uom must be in {wam.valid_uoms('thermodynamic temperature')}.")
|
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'
|
2779
2225
|
|
2780
|
-
|
2781
|
-
|
2782
|
-
|
2783
|
-
'ANGLA': ('dega', 'azimuth', False),
|
2784
|
-
'ANGLV': ('dega', 'inclination', False),
|
2785
|
-
'KH': (f'mD.{length_uom}', 'permeability length', False),
|
2786
|
-
'PPERF': (f'{length_uom}/{length_uom}', 'perforation fraction', False),
|
2787
|
-
'STAT': (None, 'well connection open', True),
|
2788
|
-
'LENGTH': length_uom_pk_discrete,
|
2789
|
-
'MD': (length_uom, 'measured depth', False),
|
2790
|
-
'X': length_uom_pk_discrete,
|
2791
|
-
'Y': length_uom_pk_discrete,
|
2792
|
-
'DEPTH': (length_uom, 'depth', False),
|
2793
|
-
'RADW': (length_uom, 'wellbore radius', False),
|
2794
|
-
'RADB': (length_uom, 'block equivalent radius', False),
|
2795
|
-
'RADBP': length_uom_pk_discrete,
|
2796
|
-
'RADWP': length_uom_pk_discrete,
|
2797
|
-
'FM': (f'{length_uom}/{length_uom}', 'matrix fraction', False),
|
2798
|
-
'IRELPM': (None, 'relative permeability index', True), # TODO: change to 'region initialization' with facet
|
2799
|
-
'SECT': (None, 'wellbore section index', True),
|
2800
|
-
'LAYER': (None, 'layer index', True),
|
2801
|
-
'ANGLE': ('dega', 'plane angle', False),
|
2802
|
-
'TEMP': (temperature_uom, 'thermodynamic temperature', False),
|
2803
|
-
'MDCON': length_uom_pk_discrete,
|
2804
|
-
'K': ('mD', 'rock permeability', False),
|
2805
|
-
'DZ': (length_uom, 'cell length', False), # TODO: add direction facet
|
2806
|
-
'DTOP': (length_uom, 'depth', False),
|
2807
|
-
'DBOT': (length_uom, 'depth', False),
|
2808
|
-
'SKIN': ('Euc', 'skin', False),
|
2809
|
-
'WI': ('Euc', 'well connection index', False),
|
2810
|
-
}
|
2811
|
-
return uom_pk_discrete_dict.get(extra, ('Euc', 'generic continuous', False))
|
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')
|
2812
2229
|
|
2813
|
-
|
2814
|
-
|
2815
|
-
if extra in ['LENGTH', 'MD', 'MDCON']:
|
2816
|
-
uom = self.trajectory.md_uom
|
2817
|
-
elif extra in ['X', 'Y', 'RADW', 'RADB', 'RADBP', 'RADWP']:
|
2818
|
-
uom = self.grid_list[0].xy_units()
|
2819
|
-
else:
|
2820
|
-
uom = self.grid_list[0].z_units()
|
2821
|
-
else:
|
2822
|
-
uom = length_uom
|
2823
|
-
if extra == 'DEPTH':
|
2824
|
-
pk = 'depth'
|
2825
|
-
else:
|
2826
|
-
pk = 'length'
|
2827
|
-
return uom, pk, False
|
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
|
2828
2232
|
|
2829
|
-
|
2830
|
-
|
2831
|
-
perm_i_uuid = None,
|
2832
|
-
perm_j_uuid = None,
|
2833
|
-
perm_k_uuid = None,
|
2834
|
-
satw_uuid = None,
|
2835
|
-
sato_uuid = None,
|
2836
|
-
satg_uuid = None,
|
2837
|
-
region_uuid = None,
|
2838
|
-
active_only = False,
|
2839
|
-
min_k0 = None,
|
2840
|
-
max_k0 = None,
|
2841
|
-
k0_list = None,
|
2842
|
-
min_length = None,
|
2843
|
-
min_kh = None,
|
2844
|
-
max_depth = None,
|
2845
|
-
max_satw = None,
|
2846
|
-
min_sato = None,
|
2847
|
-
max_satg = None,
|
2848
|
-
perforation_list = None,
|
2849
|
-
region_list = None,
|
2850
|
-
set_k_face_intervals_vertical = False,
|
2851
|
-
anglv_ref = 'gravity',
|
2852
|
-
angla_plane_ref = None,
|
2853
|
-
length_mode = 'MD',
|
2854
|
-
length_uom = None,
|
2855
|
-
use_face_centres = False,
|
2856
|
-
preferential_perforation = True):
|
2857
|
-
"""Returns the total static K.H (permeability x height).
|
2233
|
+
self.cell_count = rqet.find_tag_int(node, 'CellCount')
|
2234
|
+
assert self.cell_count is not None and self.cell_count > 0
|
2858
2235
|
|
2859
|
-
|
2860
|
-
|
2861
|
-
|
2862
|
-
|
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
|
2863
2243
|
|
2864
|
-
|
2865
|
-
j_col = 'J',
|
2866
|
-
k_col = 'K',
|
2867
|
-
one_based = False,
|
2868
|
-
extra_columns_list = ['KH'],
|
2869
|
-
ntg_uuid = ntg_uuid,
|
2870
|
-
perm_i_uuid = perm_i_uuid,
|
2871
|
-
perm_j_uuid = perm_j_uuid,
|
2872
|
-
perm_k_uuid = perm_k_uuid,
|
2873
|
-
satw_uuid = satw_uuid,
|
2874
|
-
sato_uuid = sato_uuid,
|
2875
|
-
satg_uuid = satg_uuid,
|
2876
|
-
region_uuid = region_uuid,
|
2877
|
-
active_only = active_only,
|
2878
|
-
min_k0 = min_k0,
|
2879
|
-
max_k0 = max_k0,
|
2880
|
-
k0_list = k0_list,
|
2881
|
-
min_length = min_length,
|
2882
|
-
min_kh = min_kh,
|
2883
|
-
max_depth = max_depth,
|
2884
|
-
max_satw = max_satw,
|
2885
|
-
min_sato = min_sato,
|
2886
|
-
max_satg = max_satg,
|
2887
|
-
perforation_list = perforation_list,
|
2888
|
-
region_list = region_list,
|
2889
|
-
set_k_face_intervals_vertical = set_k_face_intervals_vertical,
|
2890
|
-
anglv_ref = anglv_ref,
|
2891
|
-
angla_plane_ref = angla_plane_ref,
|
2892
|
-
length_mode = length_mode,
|
2893
|
-
length_uom = length_uom,
|
2894
|
-
use_face_centres = use_face_centres,
|
2895
|
-
preferential_perforation = preferential_perforation)
|
2244
|
+
assert self.cell_count < self.node_count
|
2896
2245
|
|
2897
|
-
|
2246
|
+
unique_grid_indices = self.__find_gi_node_and_load_hdf5_array(node = node)
|
2898
2247
|
|
2899
|
-
|
2900
|
-
wellspec_file,
|
2901
|
-
well_name = None,
|
2902
|
-
mode = 'a',
|
2903
|
-
extra_columns_list = [],
|
2904
|
-
ntg_uuid = None,
|
2905
|
-
perm_i_uuid = None,
|
2906
|
-
perm_j_uuid = None,
|
2907
|
-
perm_k_uuid = None,
|
2908
|
-
satw_uuid = None,
|
2909
|
-
sato_uuid = None,
|
2910
|
-
satg_uuid = None,
|
2911
|
-
region_uuid = None,
|
2912
|
-
radw = None,
|
2913
|
-
skin = None,
|
2914
|
-
stat = None,
|
2915
|
-
active_only = False,
|
2916
|
-
min_k0 = None,
|
2917
|
-
max_k0 = None,
|
2918
|
-
k0_list = None,
|
2919
|
-
min_length = None,
|
2920
|
-
min_kh = None,
|
2921
|
-
max_depth = None,
|
2922
|
-
max_satw = None,
|
2923
|
-
min_sato = None,
|
2924
|
-
max_satg = None,
|
2925
|
-
perforation_list = None,
|
2926
|
-
region_list = None,
|
2927
|
-
set_k_face_intervals_vertical = False,
|
2928
|
-
depth_inc_down = True,
|
2929
|
-
anglv_ref = 'gravity',
|
2930
|
-
angla_plane_ref = None,
|
2931
|
-
length_mode = 'MD',
|
2932
|
-
length_uom = None,
|
2933
|
-
preferential_perforation = True,
|
2934
|
-
space_instead_of_tab_separator = True,
|
2935
|
-
align_columns = True,
|
2936
|
-
preceeding_blank_lines = 0,
|
2937
|
-
trailing_blank_lines = 0,
|
2938
|
-
length_uom_comment = False,
|
2939
|
-
write_nexus_units = True,
|
2940
|
-
float_format = '5.3',
|
2941
|
-
use_properties = False,
|
2942
|
-
property_time_index = None):
|
2943
|
-
"""Writes Nexus WELLSPEC keyword to an ascii file.
|
2248
|
+
self.__find_grid_node(node = node, unique_grid_indices = unique_grid_indices)
|
2944
2249
|
|
2945
|
-
|
2946
|
-
pandas DataFrame containing data that has been written to the wellspec file
|
2250
|
+
self.__find_ci_node_and_load_hdf5_array(node = node)
|
2947
2251
|
|
2948
|
-
|
2949
|
-
see doc string for dataframe() method for most of the argument descriptions;
|
2950
|
-
align_columns and float_format arguments are deprecated and no longer used
|
2951
|
-
"""
|
2252
|
+
self.__find_fi_node_and_load_hdf5_array(node)
|
2952
2253
|
|
2953
|
-
|
2254
|
+
interp_uuid = rqet.find_nested_tags_text(node, ['RepresentedInterpretation', 'UUID'])
|
2255
|
+
if interp_uuid is None:
|
2256
|
+
self.wellbore_interpretation = None
|
2257
|
+
else:
|
2258
|
+
self.wellbore_interpretation = rqo.WellboreInterpretation(self.model, uuid = interp_uuid)
|
2954
2259
|
|
2955
|
-
|
2956
|
-
|
2957
|
-
'JW': 4,
|
2958
|
-
'L': 4,
|
2959
|
-
'ANGLA': 8,
|
2960
|
-
'ANGLV': 8,
|
2961
|
-
'LENGTH': 8,
|
2962
|
-
'KH': 10,
|
2963
|
-
'DEPTH': 10,
|
2964
|
-
'MD': 10,
|
2965
|
-
'X': 8,
|
2966
|
-
'Y': 12,
|
2967
|
-
'SKIN': 7,
|
2968
|
-
'RADW': 5,
|
2969
|
-
'RADB': 8,
|
2970
|
-
'PPERF': 5
|
2971
|
-
}
|
2260
|
+
# Set up matches between cell_indices and grid_indices
|
2261
|
+
self.cell_grid_link = self.map_cell_and_grid_indices()
|
2972
2262
|
|
2973
|
-
|
2263
|
+
def __find_trajectory_uuid(self, node):
|
2264
|
+
"""Find and verify the uuid of the trajectory associated with the BlockedWell object."""
|
2974
2265
|
|
2975
|
-
|
2976
|
-
|
2977
|
-
|
2978
|
-
|
2979
|
-
|
2980
|
-
|
2981
|
-
|
2982
|
-
|
2983
|
-
|
2984
|
-
|
2985
|
-
|
2986
|
-
|
2987
|
-
|
2988
|
-
|
2989
|
-
|
2990
|
-
|
2991
|
-
|
2992
|
-
|
2993
|
-
|
2994
|
-
|
2995
|
-
|
2996
|
-
|
2997
|
-
|
2998
|
-
|
2999
|
-
|
3000
|
-
|
3001
|
-
|
3002
|
-
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
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)
|
2270
|
+
else:
|
2271
|
+
assert bu.matching_uuids(self.trajectory.uuid, trajectory_uuid), 'blocked well trajectory uuid mismatch'
|
2272
|
+
|
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."""
|
2275
|
+
|
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
|
2284
|
+
|
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."""
|
2287
|
+
|
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
|
2304
|
+
|
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."""
|
2307
|
+
|
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
|
2329
|
+
|
2330
|
+
def __find_grid_node(self, node, unique_grid_indices):
|
2331
|
+
"""Find the BlockedWell object's grid reference node(s)."""
|
2332
|
+
|
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
|
2348
|
+
|
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
|
3009
2991
|
|
3010
|
-
|
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
|
3011
3017
|
|
3012
|
-
|
3013
|
-
|
3014
|
-
|
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."""
|
3015
3022
|
|
3016
|
-
|
3017
|
-
fp = fp,
|
3018
|
-
length_uom = length_uom,
|
3019
|
-
length_uom_comment = length_uom_comment,
|
3020
|
-
extra_columns_list = extra_columns_list,
|
3021
|
-
well_name = well_name)
|
3023
|
+
def get_item(v, title, pc_titles, pc, pc_timeless, ci, uom):
|
3022
3024
|
|
3023
|
-
|
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)
|
3024
3033
|
|
3025
|
-
|
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
|
3026
3053
|
|
3027
|
-
|
3028
|
-
|
3029
|
-
|
3030
|
-
|
3031
|
-
|
3032
|
-
|
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)
|
3033
3072
|
|
3034
|
-
return
|
3073
|
+
return length, radw, skin, radb, wi, wbc, stat
|
3035
3074
|
|
3036
3075
|
@staticmethod
|
3037
|
-
def
|
3038
|
-
|
3039
|
-
|
3040
|
-
for ch in well_name:
|
3041
|
-
if not 32 <= ord(ch) < 128 or ch in ' ,!*#':
|
3042
|
-
ch = '_'
|
3043
|
-
if not (previous_underscore and ch == '_'):
|
3044
|
-
nexus_friendly += ch
|
3045
|
-
previous_underscore = (ch == '_')
|
3046
|
-
if not nexus_friendly:
|
3047
|
-
well_name = 'WELL_X'
|
3048
|
-
return nexus_friendly
|
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):
|
3049
3079
|
|
3050
|
-
|
3051
|
-
|
3052
|
-
|
3053
|
-
|
3054
|
-
|
3055
|
-
|
3056
|
-
|
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)
|
3093
|
+
|
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
|
3121
|
+
|
3122
|
+
return radb, wi, wbc
|
3057
3123
|
|
3058
3124
|
@staticmethod
|
3059
|
-
def
|
3060
|
-
|
3061
|
-
return True
|
3062
|
-
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):
|
3063
3127
|
|
3064
|
-
|
3065
|
-
|
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
|
3066
3144
|
|
3067
|
-
|
3068
|
-
if self.well_name:
|
3069
|
-
well_name = self.well_name
|
3070
|
-
elif self.root is not None:
|
3071
|
-
well_name = rqet.citation_title_for_node(self.root)
|
3072
|
-
elif self.wellbore_interpretation is not None:
|
3073
|
-
well_name = self.wellbore_interpretation.title
|
3074
|
-
elif self.trajectory is not None:
|
3075
|
-
well_name = self.trajectory.title
|
3076
|
-
if not well_name:
|
3077
|
-
log.warning('no well name identified for use in WELLSPEC')
|
3078
|
-
well_name = 'WELLNAME'
|
3079
|
-
well_name = BlockedWell.__tidy_well_name(well_name)
|
3145
|
+
return k_ei, k_ej, k_ek, radw_e
|
3080
3146
|
|
3081
|
-
|
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):
|
3082
3149
|
|
3083
|
-
|
3084
|
-
|
3085
|
-
|
3086
|
-
|
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)
|
3087
3157
|
|
3088
|
-
|
3089
|
-
length_uom_system_list = ['METRIC', 'ENGLISH']
|
3090
|
-
length_uom_index = ['m', 'ft'].index(length_uom)
|
3091
|
-
fp.write(f'{length_uom_system_list[length_uom_index]}\n\n')
|
3158
|
+
return radb_e
|
3092
3159
|
|
3093
|
-
|
3094
|
-
|
3095
|
-
|
3096
|
-
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."""
|
3097
3163
|
|
3098
|
-
|
3099
|
-
|
3100
|
-
|
3101
|
-
|
3102
|
-
|
3103
|
-
|
3104
|
-
|
3105
|
-
|
3106
|
-
|
3107
|
-
|
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]
|
3108
3179
|
|
3109
|
-
|
3110
|
-
def __write_wellspec_file_rows_from_dataframe(df, fp, col_width_dict, sep):
|
3111
|
-
"""Writes the non-blank lines of a Nexus WELLSPEC file from a BlockedWell dataframe."""
|
3180
|
+
return xyz
|
3112
3181
|
|
3113
|
-
|
3114
|
-
|
3115
|
-
for col_name in df.columns:
|
3116
|
-
try:
|
3117
|
-
if col_name in col_width_dict:
|
3118
|
-
width = col_width_dict[col_name]
|
3119
|
-
else:
|
3120
|
-
width = 10
|
3121
|
-
if BlockedWell.__is_float_column(col_name):
|
3122
|
-
form = '{0:>' + str(width) + '.3f}'
|
3123
|
-
value = row[col_name]
|
3124
|
-
if col_name == 'ANGLA' and (pd.isna(row[col_name]) or value is None or np.isnan(value)):
|
3125
|
-
value = 0.0
|
3126
|
-
fp.write(sep + form.format(float(value)))
|
3127
|
-
else:
|
3128
|
-
form = '{0:>' + str(width) + '}'
|
3129
|
-
if BlockedWell.__is_int_column(col_name):
|
3130
|
-
fp.write(sep + form.format(int(row[col_name])))
|
3131
|
-
elif col_name == 'STAT':
|
3132
|
-
fp.write(sep + form.format('OFF' if str(row['STAT']).upper() in ['0', 'OFF'] else 'ON'))
|
3133
|
-
else:
|
3134
|
-
fp.write(sep + form.format(str(row[col_name])))
|
3135
|
-
except Exception:
|
3136
|
-
fp.write(sep + str(row[col_name]))
|
3137
|
-
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):
|
3138
3184
|
|
3139
|
-
|
3140
|
-
|
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]
|
3141
3197
|
|
3142
|
-
|
3143
|
-
if cells is None or grids is None or len(grids) == 0:
|
3144
|
-
return None, None, None, None
|
3145
|
-
return cells[0], grids[0].uuid
|
3198
|
+
return xyz
|
3146
3199
|
|
3147
|
-
def
|
3148
|
-
"""
|
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."""
|
3149
3202
|
|
3150
|
-
|
3151
|
-
|
3152
|
-
|
3153
|
-
|
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)
|
3154
3207
|
|
3155
|
-
|
3156
|
-
if cells is None or grids is None or len(grids) == 0:
|
3157
|
-
return None, None
|
3158
|
-
node_index = 0
|
3159
|
-
while node_index < self.node_count - 1 and self.grid_indices[node_index] == -1:
|
3160
|
-
node_index += 1
|
3161
|
-
if node_index >= self.node_count - 1:
|
3162
|
-
return None, None
|
3163
|
-
md = 0.5 * (self.node_mds[node_index] + self.node_mds[node_index + 1])
|
3164
|
-
xyz = self.trajectory.xyz_for_md(md)
|
3165
|
-
return xyz, self.trajectory.crs_uuid
|
3208
|
+
return md
|
3166
3209
|
|
3167
|
-
|
3168
|
-
|
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."""
|
3169
3215
|
|
3170
|
-
|
3171
|
-
|
3172
|
-
|
3173
|
-
|
3174
|
-
|
3175
|
-
|
3176
|
-
|
3177
|
-
|
3178
|
-
|
3179
|
-
title = 'WELL'
|
3180
|
-
if self.trajectory is not None:
|
3181
|
-
traj_interp_uuid = self.model.uuid(obj_type = 'WellboreInterpretation', related_uuid = self.trajectory.uuid)
|
3182
|
-
if traj_interp_uuid is not None:
|
3183
|
-
if shared_interpretation:
|
3184
|
-
self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
|
3185
|
-
uuid = traj_interp_uuid)
|
3186
|
-
traj_feature_uuid = self.model.uuid(obj_type = 'WellboreFeature', related_uuid = traj_interp_uuid)
|
3187
|
-
if traj_feature_uuid is not None:
|
3188
|
-
self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, uuid = traj_feature_uuid)
|
3189
|
-
if self.wellbore_feature is None:
|
3190
|
-
self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, feature_name = title)
|
3191
|
-
self.feature_to_be_written = True
|
3192
|
-
if self.wellbore_interpretation is None:
|
3193
|
-
title = title if not self.wellbore_feature.title else self.wellbore_feature.title
|
3194
|
-
self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
|
3195
|
-
title = title,
|
3196
|
-
wellbore_feature = self.wellbore_feature)
|
3197
|
-
if self.trajectory.wellbore_interpretation is None and shared_interpretation:
|
3198
|
-
self.trajectory.wellbore_interpretation = self.wellbore_interpretation
|
3199
|
-
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))
|
3200
3225
|
|
3201
|
-
|
3202
|
-
|
3203
|
-
|
3204
|
-
|
3205
|
-
|
3206
|
-
|
3207
|
-
|
3208
|
-
|
3209
|
-
|
3210
|
-
|
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]]
|
3211
3236
|
|
3212
|
-
|
3213
|
-
|
3214
|
-
|
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)
|
3215
3243
|
|
3216
|
-
|
3217
|
-
well_name = self.title
|
3244
|
+
row_ci_list.append(ci)
|
3218
3245
|
|
3219
|
-
|
3220
|
-
datum_location = trajectory_points[0].copy()
|
3221
|
-
if set_depth_zero:
|
3222
|
-
datum_location[2] = 0.0
|
3223
|
-
datum = rqw.MdDatum(self.model,
|
3224
|
-
crs_uuid = grid.crs_uuid,
|
3225
|
-
location = datum_location,
|
3226
|
-
md_reference = 'mean sea level')
|
3246
|
+
return df
|
3227
3247
|
|
3228
|
-
|
3229
|
-
|
3230
|
-
|
3231
|
-
|
3232
|
-
|
3233
|
-
|
3234
|
-
|
3235
|
-
|
3236
|
-
})
|
3237
|
-
self.trajectory = rqw.Trajectory(self.model,
|
3238
|
-
md_datum = datum,
|
3239
|
-
data_frame = trajectory_df,
|
3240
|
-
length_uom = length_uom,
|
3241
|
-
well_name = well_name,
|
3242
|
-
set_tangent_vectors = set_tangent_vectors)
|
3243
|
-
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."""
|
3244
3256
|
|
3245
|
-
if
|
3246
|
-
|
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)
|
3247
3269
|
|
3248
|
-
def
|
3249
|
-
|
3250
|
-
create_for_trajectory_if_needed = True,
|
3251
|
-
add_as_part = True,
|
3252
|
-
add_relationships = True,
|
3253
|
-
title = None,
|
3254
|
-
originator = None):
|
3255
|
-
"""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."""
|
3256
3272
|
|
3257
|
-
|
3258
|
-
|
3259
|
-
|
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')}.")
|
3260
3279
|
|
3261
|
-
|
3262
|
-
|
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))
|
3263
3312
|
|
3264
|
-
|
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
|
3265
3328
|
|
3266
|
-
|
3267
|
-
|
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
|
3268
3342
|
|
3269
|
-
|
3270
|
-
|
3271
|
-
if
|
3272
|
-
|
3273
|
-
|
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
|
3274
3350
|
|
3275
|
-
|
3276
|
-
|
3277
|
-
|
3351
|
+
@staticmethod
|
3352
|
+
def __is_int_column(col_name):
|
3353
|
+
if col_name.upper() in ['IW', 'JW', 'L']:
|
3354
|
+
return True
|
3355
|
+
return False
|
3278
3356
|
|
3279
|
-
|
3280
|
-
|
3281
|
-
add_relationships = add_relationships,
|
3282
|
-
originator = originator,
|
3283
|
-
ext_uuid = ext_uuid,
|
3284
|
-
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."""
|
3285
3359
|
|
3286
|
-
|
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)
|
3287
3373
|
|
3288
|
-
|
3374
|
+
return well_name
|
3289
3375
|
|
3290
|
-
|
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
|
3291
3380
|
|
3292
|
-
|
3293
|
-
|
3294
|
-
|
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')
|
3295
3385
|
|
3296
|
-
self.
|
3297
|
-
|
3298
|
-
|
3299
|
-
|
3300
|
-
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')
|
3301
3390
|
|
3302
|
-
|
3303
|
-
|
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))
|
3304
3401
|
|
3305
|
-
|
3306
|
-
|
3307
|
-
|
3308
|
-
interp_root = interp_root,
|
3309
|
-
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."""
|
3310
3405
|
|
3311
|
-
|
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')
|
3312
3431
|
|
3313
3432
|
def __create_wellbore_feature_and_interpretation_xml_if_needed(self, add_as_part, add_relationships, originator):
|
3314
3433
|
"""Create root node for WellboreFeature and WellboreInterpretation objects if necessary."""
|
@@ -3464,116 +3583,3 @@ class BlockedWell(BaseResqpy):
|
|
3464
3583
|
ext_node = self.model.root_for_part(ext_part)
|
3465
3584
|
self.model.create_reciprocal_relationship(bw_node, 'mlToExternalPartProxy', ext_node,
|
3466
3585
|
'externalPartProxyToMl')
|
3467
|
-
|
3468
|
-
def write_hdf5(self, file_name = None, mode = 'a', create_for_trajectory_if_needed = True):
|
3469
|
-
"""Create or append to an hdf5 file, writing datasets for the measured depths, grid, cell & face indices.
|
3470
|
-
|
3471
|
-
:meta common:
|
3472
|
-
"""
|
3473
|
-
|
3474
|
-
# NB: array data must all have been set up prior to calling this function
|
3475
|
-
|
3476
|
-
if self.uuid is None:
|
3477
|
-
self.uuid = bu.new_uuid()
|
3478
|
-
|
3479
|
-
h5_reg = rwh5.H5Register(self.model)
|
3480
|
-
|
3481
|
-
if create_for_trajectory_if_needed and self.trajectory_to_be_written:
|
3482
|
-
self.trajectory.write_hdf5(file_name, mode = mode)
|
3483
|
-
mode = 'a'
|
3484
|
-
|
3485
|
-
h5_reg.register_dataset(self.uuid, 'NodeMd', self.node_mds)
|
3486
|
-
h5_reg.register_dataset(self.uuid, 'CellIndices', self.cell_indices) # could use int32?
|
3487
|
-
h5_reg.register_dataset(self.uuid, 'GridIndices', self.grid_indices) # could use int32?
|
3488
|
-
# convert face index pairs from [axis, polarity] back to strange local face numbering
|
3489
|
-
mask = (self.face_pair_indices.flatten() == -1).reshape((-1, 2)) # 2nd axis is (axis, polarity)
|
3490
|
-
masked_face_indices = np.where(mask, 0, self.face_pair_indices.reshape((-1, 2))) # 2nd axis is (axis, polarity)
|
3491
|
-
# using flat array for raw_face_indices array
|
3492
|
-
# 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)
|
3493
|
-
raw_face_indices = np.where(mask[:, 0], -1, self.face_index_map[masked_face_indices[:, 0],
|
3494
|
-
masked_face_indices[:,
|
3495
|
-
1]].flatten()).reshape(-1)
|
3496
|
-
|
3497
|
-
h5_reg.register_dataset(self.uuid, 'LocalFacePairPerCellIndices', raw_face_indices) # could use uint8?
|
3498
|
-
|
3499
|
-
h5_reg.write(file = file_name, mode = mode)
|
3500
|
-
|
3501
|
-
def add_grid_property_to_blocked_well(self, uuid_list):
|
3502
|
-
"""Add properties to blocked wells from a list of uuids for properties on the supporting grid."""
|
3503
|
-
|
3504
|
-
part_list = [self.model.part_for_uuid(uuid) for uuid in uuid_list]
|
3505
|
-
|
3506
|
-
assert len(self.grid_list) == 1, "only blocked wells with a single grid can be handled currently"
|
3507
|
-
grid = self.grid_list[0]
|
3508
|
-
# filter to only those properties on the grid
|
3509
|
-
parts = self.model.parts_list_filtered_by_supporting_uuid(part_list, grid.uuid)
|
3510
|
-
if len(parts) < len(uuid_list):
|
3511
|
-
log.warning(
|
3512
|
-
f"{len(uuid_list)-len(parts)} uuids ignored as they do not belong to the same grid as the blocked well")
|
3513
|
-
|
3514
|
-
gridpc = grid.extract_property_collection()
|
3515
|
-
# only 'cell' properties are handled
|
3516
|
-
cell_parts = [part for part in parts if gridpc.indexable_for_part(part) == 'cells']
|
3517
|
-
if len(cell_parts) < len(parts):
|
3518
|
-
log.warning(f"{len(parts)-len(cell_parts)} uuids ignored as they do not have indexable element of cells")
|
3519
|
-
|
3520
|
-
if len(cell_parts) > 0:
|
3521
|
-
bwpc = rqp.PropertyCollection(support = self)
|
3522
|
-
if len(gridpc.string_lookup_uuid_list()) > 0:
|
3523
|
-
sl_dict = {}
|
3524
|
-
for part in cell_parts:
|
3525
|
-
if gridpc.string_lookup_uuid_for_part(part) in sl_dict.keys():
|
3526
|
-
sl_dict[gridpc.string_lookup_uuid_for_part(part)] = \
|
3527
|
-
sl_dict[gridpc.string_lookup_uuid_for_part(part)] + [part]
|
3528
|
-
else:
|
3529
|
-
sl_dict[gridpc.string_lookup_uuid_for_part(part)] = [part]
|
3530
|
-
else:
|
3531
|
-
sl_dict = {None: cell_parts}
|
3532
|
-
|
3533
|
-
sl_ts_dict = {}
|
3534
|
-
for sl_uuid in sl_dict.keys():
|
3535
|
-
if len(gridpc.time_series_uuid_list()) > 0:
|
3536
|
-
# dictionary with keys for string_lookup uuids and None where missing
|
3537
|
-
# values for each key are a list of property parts associated with that lookup uuid, or None
|
3538
|
-
time_dict = {}
|
3539
|
-
for part in sl_dict[sl_uuid]:
|
3540
|
-
if gridpc.time_series_uuid_for_part(part) in time_dict.keys():
|
3541
|
-
time_dict[gridpc.time_series_uuid_for_part(part)] = \
|
3542
|
-
time_dict[gridpc.time_series_uuid_for_part(part)] + [part]
|
3543
|
-
else:
|
3544
|
-
time_dict[gridpc.time_series_uuid_for_part(part)] = [part]
|
3545
|
-
else:
|
3546
|
-
time_dict = {None: sl_dict[sl_uuid]}
|
3547
|
-
sl_ts_dict[sl_uuid] = time_dict
|
3548
|
-
|
3549
|
-
for sl_uuid in sl_ts_dict.keys():
|
3550
|
-
time_dict = sl_ts_dict[sl_uuid]
|
3551
|
-
for time_uuid in time_dict.keys():
|
3552
|
-
parts = time_dict[time_uuid]
|
3553
|
-
for part in parts:
|
3554
|
-
array = gridpc.cached_part_array_ref(part)
|
3555
|
-
indices = self.cell_indices_for_grid_uuid(grid.uuid)
|
3556
|
-
bwarray = np.empty(shape = (indices.shape[0],), dtype = array.dtype)
|
3557
|
-
for i, ind in enumerate(indices):
|
3558
|
-
bwarray[i] = array[tuple(ind)]
|
3559
|
-
bwpc.add_cached_array_to_imported_list(
|
3560
|
-
bwarray,
|
3561
|
-
source_info = f'property from grid {grid.title}',
|
3562
|
-
keyword = gridpc.citation_title_for_part(part),
|
3563
|
-
discrete = (not gridpc.continuous_for_part(part)),
|
3564
|
-
uom = gridpc.uom_for_part(part),
|
3565
|
-
time_index = gridpc.time_index_for_part(part),
|
3566
|
-
null_value = gridpc.null_value_for_part(part),
|
3567
|
-
property_kind = gridpc.property_kind_for_part(part),
|
3568
|
-
local_property_kind_uuid = gridpc.local_property_kind_uuid(part),
|
3569
|
-
facet_type = gridpc.facet_type_for_part(part),
|
3570
|
-
facet = gridpc.facet_for_part(part),
|
3571
|
-
realization = gridpc.realization_for_part(part),
|
3572
|
-
indexable_element = 'cells')
|
3573
|
-
bwpc.write_hdf5_for_imported_list(use_int32 = False)
|
3574
|
-
bwpc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_uuid,
|
3575
|
-
string_lookup_uuid = sl_uuid)
|
3576
|
-
else:
|
3577
|
-
log.debug(
|
3578
|
-
"no properties added - uuids either not 'cell' properties or blocked well is associated with multiple grids"
|
3579
|
-
)
|