"""Module holding functions relating to the mapping of welbore frame properties onto blocked wells."""
import logging
log = logging.getLogger(__name__)
import math as maths
import numpy as np
import resqpy.property as rqp
import resqpy.well as rqw
[docs]def blocked_well_frame_contributions_list(bw, wbf):
"""Returns wellbore frame contributions to each cell of a blocked well.
arguments:
bw (BlockedWell): the blocked well to map the wellbore frame onto
wbf (WellboreFrame): the wellbore frame to map to the blocked well
returns:
list of list of (int, float, float) with one entry per blocked well cell, and each
entry being a list of (wellbore frame interval index,
fraction of wellbore frame interval in cell,
fraction of cell's wellbore interval in wellbore frame interval)
"""
wb_fraction_of_wbf = []
for wb_ii in range(bw.node_count - 1):
if bw.grid_indices[wb_ii] < 0:
continue
cell_entry_md = bw.node_mds[wb_ii]
cell_exit_md = bw.node_mds[wb_ii + 1]
cell_dmd = cell_exit_md - cell_entry_md # wellbore interval length within cell
entry_wbf_i, entry_wbf_fr = wbf.interval_for_md(bw.node_mds[wb_ii])
exit_wbf_i, exit_wbf_fr = wbf.interval_for_md(bw.node_mds[wb_ii + 1])
if (exit_wbf_i == -1 and exit_wbf_fr < 0.5) or (entry_wbf_i == -1 and entry_wbf_fr > 0.5):
wb_fraction_of_wbf.append([]) # cell is above or below all frame intervals
continue
if entry_wbf_i == -1:
assert entry_wbf_fr < 0.5
entry_wbf_i = 0
entry_wbf_fr = 0.0
if exit_wbf_i == -1:
assert exit_wbf_fr > 0.5
exit_wbf_i = wbf.node_count - 2
exit_wbf_fr = 1.0
assert exit_wbf_i >= entry_wbf_i
if exit_wbf_i == entry_wbf_i: # single frame interval contributing to this cell
assert exit_wbf_fr >= entry_wbf_fr
if exit_wbf_fr == entry_wbf_fr: # kissing point
wb_fraction_of_wbf.append([])
continue
cell_fr = (min(cell_exit_md, wbf.node_mds[exit_wbf_i + 1]) -
max(cell_entry_md, wbf.node_mds[entry_wbf_i])) / cell_dmd
assert 0.0 <= cell_fr <= 1.0 + 1.0e-6
wb_fraction_of_wbf.append([(entry_wbf_i, exit_wbf_fr - entry_wbf_fr, cell_fr)])
continue
# more than one frame interval contributes to this cell
cell_fr = (wbf.node_mds[entry_wbf_i + 1] - max(cell_entry_md, wbf.node_mds[entry_wbf_i])) / cell_dmd
assert 0.0 <= cell_fr <= 1.0
contrib_list = [(entry_wbf_i, 1.0 - entry_wbf_fr, cell_fr)]
for wbf_i in range(entry_wbf_i + 1, exit_wbf_i): # these frame intervals entirely within cell
contrib_list.append((wbf_i, 1.0, (wbf.node_mds[wbf_i + 1] - wbf.node_mds[wbf_i]) / cell_dmd))
cell_fr = (min(cell_exit_md, wbf.node_mds[exit_wbf_i + 1]) - wbf.node_mds[exit_wbf_i]) / cell_dmd
assert 0.0 <= cell_fr <= 1.0
contrib_list.append((exit_wbf_i, exit_wbf_fr, cell_fr))
wb_fraction_of_wbf.append(contrib_list)
return wb_fraction_of_wbf
[docs]def add_blocked_well_properties_from_wellbore_frame(bw,
frame_uuid = None,
property_kinds_list = None,
realization = None,
set_length = None,
set_perforation_fraction = None,
set_frame_interval = False):
"""Add properties to this blocked well by derivation from wellbore frame intervals properties.
arguments:
bw (BlockedWell): the blocked well to add properties to
frame_uuid (UUID, optional): the uuid of the wellbore frame to source properties from; if None, a
solitary wellbore frame relating to the same trajectory as the blocked well will be used
property_kinds_list (list of str, optional): if present, a list of handled property kinds which
are to be set from the wellbore frame properties; if None, any handled property kinds that
are present for the wellbore frame will be used
realization (int, optional): if present, wellbore frame properties will be filtered by this
realization number and it will be assigned to the blocked well properties that are created
set_length (bool, optional): if True, a length property will be generated based on active
measured depth intervals; if None, will be set True if length is in list of property kinds
being processed
set_perforation_fraction (bool, optional): if True, a perforation fraction property will be
created based on the fraction of the measured depth within a blocked well cell that is
flagged as active, ie. perforated at some time; if None, it will be created only if length
and permeability thickness are both absent
set_frame_interval (bool, default False): if True, a static discrete property holding the index
of the dominant active wellbore frame interval (per blocked well cell) is created
returns:
list of uuids of created property parts (does not include any copied time series object)
notes:
this method is designed to set up some blocked well properties based on similar properties
already established on a special wellbore frame, mainly for perforations and open hole completions;
frame_uuid should be specified if there are well logs in the dataset, or other wellbore frames;
if a permeability thickness property is being set based on a wellbore frame property, the value
is divided between blocked well cells based solely on measured depth interval lengths, without
reference to grid properties such as net to gross ratio or permeability;
titles will be the same as those used in the frame properties, and 'PPERF' for partial
perforation;
if set_frame_interval is True, the resulting property will be given a soft relationship with the
wellbore frame (in addition to its supporting representation reference relationship with the
blocked well); a null value of -1 is used where no active frame interval is present in a cell;
units of measure will also be the same as those in the wellbore frame;
this method only supports single grid blocked wells at present;
blocked well and wellbore frame must be in the same model
"""
# note: 'active' ia a static property kind used to indicate that a cell is perforated or otherwise
# open to flow at some time, so needs to appear somewhere in well specification data for flow simulation;
# 'well connection open' is a dynamic indicator used to identify time periods when a connection is open
handled_property_kinds = [
'active', 'well connection open', 'length', 'permeability length', 'skin', 'wellbore radius',
'nonDarcy flow coefficient'
]
def pk_pc_and_ts(pc, pk):
"""Returns a selective property collection based on property kind and a time series uuid if used."""
pk_pc = rqp.selective_version_of_collection(pc, property_kind = pk)
ts_uuids = pk_pc.time_series_uuid_list(sort_list = False)
assert 0 <= len(ts_uuids) <= 1
ts_uuid = None if len(ts_uuids) == 0 else ts_uuids[0]
assert ts_uuid is not None or pk_pc.number_of_parts() == 1
return pk_pc, ts_uuid
assert len(bw.grid_list) == 1, \
'blocked well references more than one grid; not supported for frame properties'
if frame_uuid is None: # note: there could be multiple wellbore frames if well logs are in the dataset
frame_uuid = bw.model.uuid(obj_type = 'WellboreFrameRepresentation', related_uuid = bw.trajectory.uuid)
assert frame_uuid is not None, f'wellbore frame neither specified nor found for trajectory: {bw.trajectory.title}'
if property_kinds_list is not None:
for pk in property_kinds_list:
assert pk in handled_property_kinds, f'blocked well from frame property kind not supported: {pk}'
# establish a property collection for this blocked well
wb_pc = bw.extract_property_collection()
# load wellbore frame and its properties
wbf = rqw.WellboreFrame(bw.model, uuid = frame_uuid)
wbf_pc = wbf.extract_property_collection()
wbf_pc = rqp.selective_version_of_collection(wbf_pc, indexable = 'intervals', realization = realization)
if wbf_pc.number_of_parts() == 0:
log.warning(f'no intervals properties found for wellbore frame: {wbf.title}; realization: {realization}')
return []
# determine the actual property kinds list that will be worked with
pk_list = []
wbf_pk_list = wbf_pc.property_kind_list(sort_list = False)
for pk in wbf_pk_list:
if ((property_kinds_list is None and pk in handled_property_kinds) or
(property_kinds_list is not None and pk in property_kinds_list)):
pk_list.append(pk)
if len(pk_list) == 0:
log.warning('no appropriate property kinds found for wellbore frame: {wbf.title}')
if set_length is None:
set_length = ('length' in pk_list)
if set_perforation_fraction is None:
set_perforation_fraction = (not set_length and 'permeability length' not in pk_list)
log.debug(f'working list of property kinds for: {wbf.title}: {pk_list}')
# find mapping of wellbpre frame onto blocked well cells, based on measured depths
# list of entry per cell:
# entry is list of triplets of (frame interval index, fraction of frame interval, fraction of cell md interval)
wb_fraction_of_wbf = bw.frame_contributions_list(wbf)
assert len(wb_fraction_of_wbf) == bw.cell_count
# inherit static properties from wellbore frame to blocked well; resampling mehtod depends on property kind
# TODO: make length and perforation fraction properties dynamic
wb_active_array = None
if 'active' in pk_list or set_length or set_perforation_fraction or set_frame_interval:
wbf_p = wbf_pc.singleton(property_kind = 'active')
assert wbf_p is not None, 'problem with active wellbore frame property'
wbf_a = wbf_pc.cached_part_array_ref(wbf_p)
wb_a = np.zeros(bw.cell_count, dtype = bool)
length = np.zeros(bw.cell_count, dtype = float)
pperf = np.zeros(bw.cell_count, dtype = float)
dominant_wbf_interval = np.full(bw.cell_count, -1, dtype = np.int32)
ci = 0
for wb_ii in range(bw.node_count - 1):
if bw.grid_indices[wb_ii] < 0:
continue
i = ci
ci += 1
wbf_contributions = wb_fraction_of_wbf[i]
max_active_cell_fr = 0.0
max_active_wbf_ii = None
for (wbf_ii, _, cell_fr) in wbf_contributions:
if wbf_a[wbf_ii]:
wb_a[i] = True # any contributing frame interval active makes cell active
pperf[i] += cell_fr
if cell_fr > max_active_cell_fr:
max_active_cell_fr = cell_fr
max_active_wbf_ii = wbf_ii
if pperf[i] > 1.0:
pperf[i] = 1.0
if max_active_wbf_ii is not None:
dominant_wbf_interval[i] = max_active_wbf_ii
length[i] = pperf[i] * (bw.node_mds[wb_ii + 1] - bw.node_mds[wb_ii])
wb_active_array = wb_a.copy()
if set_frame_interval:
# position this first to make it easy to get its uuid, for adding a soft relationship
wb_pc.add_cached_array_to_imported_list(dominant_wbf_interval,
f'derived from wellbore frame {wbf.title}',
'wellbore frame interval',
discrete = True,
property_kind = 'wellbore frame interval',
null_value = -1,
realization = realization,
indexable_element = 'cells')
if 'active' in pk_list:
wb_pc.add_cached_array_to_imported_list(wb_a,
f'derived from wellbore frame {wbf.title}',
wbf_pc.citation_title_for_part(wbf_p),
discrete = True,
property_kind = 'active',
realization = realization,
indexable_element = 'cells')
if set_length:
md_uom = bw.trajectory.md_uom
wb_pc.add_cached_array_to_imported_list(length,
f'derived from wellbore frame {wbf.title}',
'LENGTH',
discrete = False,
uom = f'{md_uom}',
property_kind = 'length',
realization = realization,
indexable_element = 'cells')
if set_perforation_fraction:
md_uom = bw.trajectory.md_uom
md_uom = 'ft' if md_uom.startswith('ft') else 'm'
wb_pc.add_cached_array_to_imported_list(pperf,
f'derived from wellbore frame {wbf.title}',
'PPERF',
discrete = False,
uom = f'{md_uom}/{md_uom}',
property_kind = 'perforation fraction',
realization = realization,
indexable_element = 'cells')
if 'wellbore radius' in pk_list:
wbf_p = wbf_pc.singleton(property_kind = 'wellbore radius')
assert wbf_p is not None, 'problem with wellbore radius wellbore frame property'
wbf_a = wbf_pc.cached_part_array_ref(wbf_p)
wb_a = np.full(bw.cell_count, np.nan, dtype = float)
for i, wbf_contributions in enumerate(wb_fraction_of_wbf):
if len(wbf_contributions) == 0:
continue # todo: could try to inherit wellbore radius from above or below?
if np.all(np.isnan(wbf_a[wbf_contributions[0][0]:wbf_contributions[-1][0] + 1])):
continue
# use a maximum wellbore radius for the whole cell; over-optimistic in case of variation
max_r = np.nanmax(wbf_a[wbf_contributions[0][0]:wbf_contributions[-1][0] + 1])
if np.nanmin(wbf_a[wbf_contributions[0][0]:wbf_contributions[-1][0] + 1]) < max_r - 1.0e-6:
log.warning(
f'radius of wellbore for {wbf.title} changes within cell {i} for blocked wellbore {bw.title}')
# todo: find modal average or measured length weighted mean?; for now, use topmost (largest) radius
wb_a[i] = max_r
wb_pc.add_cached_array_to_imported_list(wb_a,
f'derived from wellbore frame {wbf.title}',
wbf_pc.citation_title_for_part(wbf_p),
discrete = False,
uom = wbf_pc.uom_for_part(wbf_p),
property_kind = 'wellbore radius',
realization = realization,
indexable_element = 'cells')
# if any dynamic, copy time series (possibly reusing an existing one, as likely to be shared with other wells)
if 'well connection open' in pk_list:
wco_pc, ts_uuid = pk_pc_and_ts(wbf_pc, 'well connection open')
for wco_part in wco_pc.parts():
wbf_wco = wco_pc.cached_part_array_ref(wco_part)
assert wbf_wco is not None
ti = wco_pc.time_index_for_part(wco_part)
wb_wco = np.zeros(bw.cell_count, dtype = bool)
for i, wbf_contributions in enumerate(wb_fraction_of_wbf):
if len(wbf_contributions) == 0:
continue
some_active_closed = False
for (wbf_ii, _, _) in wbf_contributions:
if wbf_wco[wbf_ii]:
wb_wco[i] = True # any contributing frame interval active makes cell active
elif wb_active_array is not None and wb_active_array[i]:
some_active_closed = True
if wb_wco[i] and some_active_closed:
log.error('unsyncronised opening of active intervals ' +
f'at time index {ti} within cell {i} in blocked well {bw.title}')
wb_pc.add_cached_array_to_imported_list(wb_wco,
f'derived from wellbore frame {wbf.title}',
wco_pc.citation_title_for_part(wco_part),
discrete = True,
property_kind = 'well connection open',
time_index = ti,
time_series_uuid = ts_uuid,
realization = realization,
indexable_element = 'cells')
if 'permeability length' in pk_list:
kh_pc, ts_uuid = pk_pc_and_ts(wbf_pc, 'permeability length')
kh_uom = None
for kh_part in kh_pc.parts():
wbf_kh = kh_pc.cached_part_array_ref(kh_part)
assert wbf_kh is not None
if kh_uom is None:
kh_uom = kh_pc.uom_for_part(kh_part)
else:
assert kh_pc.uom_for_part(kh_part) == kh_uom, 'mixed permeability length units of measure'
wb_kh = np.zeros(bw.cell_count, dtype = float)
for i, wbf_contributions in enumerate(wb_fraction_of_wbf):
if len(wbf_contributions) == 0:
continue
for (wbf_ii, wbf_fr, _) in wbf_contributions:
if not np.isnan(wbf_kh[wbf_ii]):
wb_kh[i] += wbf_kh[wbf_ii] * wbf_fr
wb_pc.add_cached_array_to_imported_list(wb_kh,
f'derived from wellbore frame {wbf.title}',
kh_pc.citation_title_for_part(kh_part),
discrete = False,
uom = kh_uom,
property_kind = 'permeability length',
time_index = kh_pc.time_index_for_part(kh_part),
time_series_uuid = ts_uuid,
realization = realization,
indexable_element = 'cells')
if 'skin' in pk_list:
# note: here the skin contributions are weighted by fraction of measured length within a cell
# this will not give a technically correct result if the permeability varies between frame intervals
skin_pc, ts_uuid = pk_pc_and_ts(wbf_pc, 'skin')
skin_uom = None # probably always Euc
for skin_part in skin_pc.parts():
wbf_skin = skin_pc.cached_part_array_ref(skin_part)
assert wbf_skin is not None
if skin_uom is None:
skin_uom = skin_pc.uom_for_part(skin_part)
else:
assert skin_pc.uom_for_part(skin_part) == skin_uom, 'mixed skin units of measure'
wb_skin = np.full(bw.cell_count, np.nan, dtype = float)
for i, wbf_contributions in enumerate(wb_fraction_of_wbf):
if len(wbf_contributions) == 0:
continue
fr_sum = 0.0
for (wbf_ii, _, cell_fr) in wbf_contributions:
if np.isnan(wbf_skin[wbf_ii]):
continue
if np.isnan(wb_skin[i]):
wb_skin[i] = 0.0
wb_skin[i] += wbf_skin[wbf_ii] * cell_fr
fr_sum += cell_fr
if fr_sum > 0.0:
wb_skin[i] /= fr_sum
wb_pc.add_cached_array_to_imported_list(wb_skin,
f'derived from wellbore frame {wbf.title}',
skin_pc.citation_title_for_part(skin_part),
discrete = False,
uom = skin_uom,
property_kind = 'skin',
time_index = skin_pc.time_index_for_part(skin_part),
time_series_uuid = ts_uuid,
realization = realization,
indexable_element = 'cells')
# TODO: non Darcy flow coefficient
# D factor is complicated: it appears in the inflow equation as a rate dependent adjustment to skin
wb_pc.write_hdf5_for_imported_list()
uuids = wb_pc.create_xml_for_imported_list_and_add_parts_to_model()
if set_frame_interval:
bw.model.create_reciprocal_relationship_uuids(uuids[0], 'sourceObject', frame_uuid, 'destinationObject')
return uuids