Tutorial 4: Using the RTA Micro-service#

This tutorial builds upon Tutorial 3. Here, the filtered and decimated rates and pressure values will be uploaded to a Topaze document (already uploaded to the well)and a new model will be executed using the RTA micro-service.

This tutorial emphasizes the use of the micro-service, the extraction of the simulation resultsand the updating of model parameters.

The code for Tutorial 4 can be found in this zip file.

An additional input - Topaze file#

There is one additional input for this tutorial along with what is shown in Tutorial 3.

Listing 28 The Topaze file input is represented by the following YAML code#
    Topaze file:
      type: file
      readOnly: false
      mandatory: true
Click to view the full contents of tutorial_3.yaml
Listing 29 tutorial_3.yaml#
inputs:

    Rate:
      type: dataset
      mandatory: true
      dataType: qo
      isByStep: true

    Pressure:
      type: dataset
      mandatory: true
      dataType: BHP
      isByStep: false

    Window size:
      type:  int
      currentValue: 11
      mandatory: true

    Time decimation:
      type: double
      dimension: LargeTime
      currentValue: 168

    Topaze file:
      type: file
      readOnly: false
      mandatory: true

outputs:

    Filtered pressure:
      type: dataset
      dataType: BHP
      isByStep: false

    Decimated rates:
      type: dataset
      dataType: qo
      isByStep: true

User task script: Specifying the model#

The Topaze file is represented by a kappa_sdk.Document instance. The model input is described in xml format. The kappa_sdk.Document.set_model_xml() method accepts the xml file representing the model input (or optionally, only the input which is different in the Topaze model in the kappa_sdk.Document instance)and then runs a “Generate” or an “Improve”.

An example is shown in the code snippet in Listing 30. A kappa_sdk.ModelParser instance parses an xml and sets new parameter values using kappa_sdk.ModelParser.set_parameter_value(). The method kappa_sdk.ModelParser.export() creates the new xml, which is sent to the kappa_sdk.Document instance (“topaze_doc”) using kappa_sdk.Document.set_model_xml(). This method will run a “Generate” (by default) and give a return message if the execution is not successful.

Listing 30 Section of code showing the modification of a model xml and re-injection of that xml into a Topaze document.#
...
delta, k, xmf = x

parser = ModelParser(model_xml)
parser.set_parameter_value(topaze_doc.analyses[0].id, "KWKA_RES_PAR",
                           {"Type": "FRACTIONAL_DIMENSION", "ZoneIndexX": "1"}, str(delta))
k = np.power(10, k)
internal_perm = services.unit_converter.convert_to_internal(UnitEnum.perm_milli_darcy, k)
parser.set_parameter_value(topaze_doc.analyses[0].id, "KWKA_RES_PAR",
                           {"Type": "PERMEABILITY"}, str(internal_perm))
xmf = np.power(10, xmf)
internal_xmf = services.unit_converter.convert_to_internal(UnitEnum.length_feet, xmf)
parser.set_parameter_value(topaze_doc.analyses[0].id, "KWKA_WELL_PAR",
                           {"Type": "FRACTURE_XF"}, str(internal_xmf))

# get the updated xml
new_model_xml = parser.export()
ret = topaze_doc.set_model_xml(new_model_xml)

if not ret.is_success:
    services.log.error(ret.message)
...

The file “model_xml” can be retrieved preceding Listing 30 using kappa_sdk.Document.get_model_xml().

# Retrieve the model xml file
model_xml = topaze_doc.get_model_xml()

Now that we have our code to modify a model (via the xml), re-run the modeland extract the results, we can do something interesting such as run a least squares optimization using SciPy. After the import statements we have three functions: the first updates the model xml and runs the RTA micro-service, the second calculates the least squares residualand the third runs the least squares optimization. Following these function definitions, the Python code from Tutorial 2 is repeated (without the plotting). Finally, we have the code where the model xml is extracted and the optimization code is called.

Plotting the regression results#

We are going to plot the results of the least squares regression by extracting them from the model and not from the output of a user task. In this case, we must refresh the plot programmatically within the user task. The first step is to check if the plot has already been created. If so, then the plot can be retrieved, deleted and re-created. See Listing 31

Listing 31 Updating a plot#
this_task = services.user_task
plot_name = 'Regression Summary'
plot = next((x for x in this_task.plots if x.name == plot_name), None)
if plot is not None:
    plot.delete()

Then, we pass the data to the plot in the form of a list containing the x,y points.

Listing 32 Creating the data list#
#  Setting up data for plotting
data_measure = 'LiquidRateSurface'
historical_name = 'Decimated rates'
simulated_name = 'Simulated oil rate'
(data_times, data_values), (sim_times_init, sim_init) = run_topaze(x_init, model_xml, topaze_doc, data_measure, historical_name, simulated_name)
date_list = list()
for x in data_times:
    new_date = first_date + datetime.timedelta(hours=x)
    date_list.append(new_date)
sim_init_list = list()
for x in sim_times_init:
    new_date = first_date + datetime.timedelta(hours=x)
    sim_init_list.append(new_date)

my_data_type = PlotDataTypesEnum.oil_rate_surface
data_series = summary_plot.add_embedded_data(Vector(date_list, data_values, first_date), 'Data', pane_name, show_symbols=False,
                                             first_x=first_date, show_lines=True, data_type=my_data_type, is_by_step=True, use_elapsed=False)
data_series.set_lines_aspect(style=LineAspectEnum.dash)
init_series = summary_plot.add_embedded_data(Vector(sim_init_list, sim_init, first_date), 'Initial', pane_name, show_symbols=False,
                                             first_x=first_date, show_lines=True, data_type=my_data_type, is_by_step=True, use_elapsed=False)
init_series.set_lines_aspect(style=LineAspectEnum.dot)

The plot is then updated in the usual fashion by kappa_sdk.Plot.update_plot().

The full Python script#

The full user task script is shown below in Listing 33.

Note

Running a least squares optimization in this manner will be much slower than running an “Improve” within the RTA micro-service. This example is given for demonstration purposes only.

Listing 33 tutorial_4.py#
  1from kappa_sdk.user_tasks.user_task_environment import services
  2import tutorial_4_utility as utility
  3import datetime
  4from kappa_sdk import KWModuleEnum, UnitEnum
  5from kappa_sdk import ModelParser, Document
  6import numpy as np
  7import numpy.typing as npt
  8from scipy.interpolate import interp1d
  9from scipy import optimize
 10import math
 11from kappa_sdk import Plot, PlotDataTypesEnum, LineAspectEnum, Vector
 12from typing import Tuple, List, cast
 13from uuid import UUID
 14
 15
 16def run_topaze(x: Tuple[float, float, float], model_xml: str, topaze_doc: Document, data_measure: str, historical_name: str, simulated_name: str) \
 17        -> Tuple[Tuple[List[float], List[float]], Tuple[List[float], List[float]]]:
 18    delta, k, xmf = x
 19
 20    parser = ModelParser(model_xml)
 21    parser.set_parameter_value(topaze_doc.analyses[0].id, "KWKA_RES_PAR",
 22                               {"Type": "FRACTIONAL_DIMENSION", "ZoneIndexX": "1"}, str(delta))
 23    k = np.power(10, k)
 24    internal_perm = services.unit_converter.convert_to_internal(UnitEnum.perm_milli_darcy, k)
 25    parser.set_parameter_value(topaze_doc.analyses[0].id, "KWKA_RES_PAR",
 26                               {"Type": "PERMEABILITY"}, str(internal_perm))
 27    xmf = np.power(10, xmf)
 28    internal_xmf = services.unit_converter.convert_to_internal(UnitEnum.length_feet, xmf)
 29    parser.set_parameter_value(topaze_doc.analyses[0].id, "KWKA_WELL_PAR",
 30                               {"Type": "FRACTURE_XF"}, str(internal_xmf))
 31
 32    # get the updated xml
 33    new_model_xml = parser.export()
 34    result = topaze_doc.set_model_xml(new_model_xml)
 35
 36    if not result.is_success:
 37        services.log.error(result.message)
 38        raise ValueError(f"Document model update has failed: {result.message}")
 39
 40    # retrieve generated data from the document
 41    vectors = topaze_doc.analyses[0].get_plot_data('History')  # <-- need to document plot types
 42    #  NOTE:  dates coming from vectors is not correct because the reference time is often not initialized correctly
 43    for v in vectors:
 44        if v.data_measure == data_measure and v.data_name == historical_name:
 45            data = v.values
 46            data_times = v.elapsed_times
 47        elif v.data_measure == data_measure and v.data_name == simulated_name:
 48            sim = v.values
 49            sim_times = v.elapsed_times
 50
 51    if data is None or sim is None:
 52        raise Exception('No data or simulated data found in the document')
 53
 54    return (data_times, data), (sim_times, sim)
 55
 56
 57def get_ls_residual(x: Tuple[float, float, float], model_xml: str, topaze_doc: Document, data_measure: str, historical_name: str, simulated_name: str) -> npt.NDArray[np.float64]:
 58
 59    (data_times, data), (sim_times, sim) = run_topaze(x, model_xml, topaze_doc, data_measure, historical_name, simulated_name)
 60    sim_interpolator = interp1d(sim_times, sim, bounds_error=False, fill_value='extrapolate')
 61    data_times_array = np.array(data_times)
 62    data_array = np.array(data)
 63    start_match_time = 1000  # started simulation at 1000 hours
 64    diff: npt.NDArray[np.float64] = sim_interpolator(data_times_array[data_times_array > start_match_time]) - data_array[data_times_array > start_match_time]
 65    return diff
 66
 67
 68def perform_least_squares(model_xml: str, topaze_doc: Document, x_init: Tuple[float, float, float], min_vals: npt.NDArray[np.float64], max_vals: npt.NDArray[np.float64], data_measure: str, historical_name: str, simulated_name: str) -> optimize.OptimizeResult:
 69
 70    args_least_squares = [model_xml, topaze_doc, data_measure, historical_name, simulated_name]
 71    result = optimize.least_squares(get_ls_residual, x_init, bounds=(min_vals, max_vals),
 72                                    args=args_least_squares, method='dogbox')
 73    return result
 74
 75
 76services.log.info('[Tutorial 3] Start of user task')
 77data = services.data_enumerator.to_enumerable([cast(UUID, services.parameters.input['Rate']), cast(UUID, services.parameters.input['Pressure'])])
 78services.log.info('[Tutorial 3] Data read by data_enumerator')
 79
 80dates = list()
 81rates = list()
 82pressures = list()
 83times = list()
 84first_date = data[0].date
 85for point in data:
 86    dates.append(point.date)
 87    elapsed = (point.date - first_date).total_seconds() / 60.0 / 60.0
 88    times.append(elapsed)
 89    rates.append(point.values[0])
 90    pressures.append(point.values[1])
 91
 92#  Size of window for smoothing
 93window_size = cast(int, services.parameters.input['Window size'])
 94services.log.info('[Tutorial 4 Smoothing pressure signal for non-zero rate periods')
 95filtered_pressures = utility.filter_positive_values(rates, pressures, window_size)
 96
 97#  Time period for rate decimation
 98delta_t_max = cast(float, services.parameters.input['Time decimation'])
 99
100# # Decimate the rate values by delta_t_max (preserving cumulative production)
101services.log.info('[Tutorial 4] Decimate the rate values by {}'.format(delta_t_max))
102decimated_times, decimated_rates = utility.get_decimated_times_rates(times, rates, delta_t_max)
103
104decimated_dates = list()
105for t in decimated_times:
106    new_date = first_date + datetime.timedelta(hours=t)
107    decimated_dates.append(new_date)
108
109services.log.info('[Tutorial 4] Interpolate pressure')
110pressure_interpolator = interp1d(times, filtered_pressures)
111filtered_pressures = (pressure_interpolator(decimated_times))
112services.data_command_service.replace_data(str(services.parameters.output['Filtered pressure']), decimated_dates, filtered_pressures.tolist())
113
114#  Step data requires the set_first_x method
115services.data_command_service.set_first_x(str(services.parameters.output['Decimated rates']), first_date)
116services.data_command_service.replace_data(str(services.parameters.output['Decimated rates']), decimated_dates[1:], decimated_rates[1:])
117
118services.log.info('[Tutorial 4] Getting Topaze document')
119file_id = services.parameters.input['Topaze file']
120topaze_doc = next(x for x in services.well.documents if (x.type == KWModuleEnum.topaze and x.file_id == str(file_id)))
121
122# Set new pressure and rate values
123ret = topaze_doc.reset_pressure(str(services.parameters.output['Filtered pressure']))
124if not ret.is_success:
125    services.log.error(ret.message)
126ret = topaze_doc.reset_rate(str(services.parameters.output['Decimated rates']), 'OilRate')
127if not ret.is_success:
128    services.log.error(ret.message)
129
130services.log.info('[Tutorial 4] Retrieving model xml')
131# get the model XML once and re-use it on each iteration
132model_xml = topaze_doc.get_model_xml()
133
134services.log.info('[Tutorial 4] Reset delta, k, xmf')
135#  Initial values and min/max bounds
136delta = 0.45
137k = math.log10(1E-4)  # Working in log(k) space
138xmf = math.log10(5000)  # Working in log(Xmf) space
139x_init = (delta, k, xmf)
140min_vals = np.array([0.2, -5, 3])
141max_vals = np.array([0.7, -3, 5])
142
143this_task = services.user_task
144plot_name = 'Regression Summary'
145plot = next((x for x in this_task.plots if x.name == plot_name), None)
146if plot is not None:
147    plot.delete()
148
149pane_name = 'Init vs Final Match'
150summary_plot: Plot = services.user_task.create_plot(plot_name, pane_name)
151
152#  Setting up data for plotting
153data_measure = 'LiquidRateSurface'
154historical_name = 'Decimated rates'
155simulated_name = 'Simulated oil rate'
156(data_times, data_values), (sim_times_init, sim_init) = run_topaze(x_init, model_xml, topaze_doc, data_measure, historical_name, simulated_name)
157date_list = list()
158for x in data_times:
159    new_date = first_date + datetime.timedelta(hours=x)
160    date_list.append(new_date)
161sim_init_list = list()
162for x in sim_times_init:
163    new_date = first_date + datetime.timedelta(hours=x)
164    sim_init_list.append(new_date)
165
166my_data_type = PlotDataTypesEnum.oil_rate_surface
167data_series = summary_plot.add_embedded_data(Vector(date_list, data_values, first_date), 'Data', pane_name, show_symbols=False,
168                                             first_x=first_date, show_lines=True, data_type=my_data_type, is_by_step=True, use_elapsed=False)
169data_series.set_lines_aspect(style=LineAspectEnum.dash)
170init_series = summary_plot.add_embedded_data(Vector(sim_init_list, sim_init, first_date), 'Initial', pane_name, show_symbols=False,
171                                             first_x=first_date, show_lines=True, data_type=my_data_type, is_by_step=True, use_elapsed=False)
172init_series.set_lines_aspect(style=LineAspectEnum.dot)
173
174# # Least squares optimization
175services.log.info('[Tutorial 4] Performing least squares')
176result = perform_least_squares(model_xml, topaze_doc, x_init, min_vals, max_vals, data_measure, historical_name, simulated_name)
177services.log.info('[Tutorial 4] Converged to {}'.format(result.x))
178services.log.info('[Tutorial 4] Rerunning model at optimal point')
179
180# Run final result and add that to plot
181(data_times, data_values), (sim_times_final, sim_final) = run_topaze(result.x, model_xml, topaze_doc, data_measure, historical_name, simulated_name)
182sim_final_list = list()
183for x in sim_times_final:
184    new_date = first_date + datetime.timedelta(hours=x)
185    sim_final_list.append(new_date)
186
187new_final_vector = Vector(sim_final_list, sim_final, first_date)
188final_series = summary_plot.add_embedded_data(new_final_vector, 'Final', pane_name, show_symbols=False, show_lines=True,
189                                              first_x=first_date, data_type=my_data_type, is_by_step=True, use_elapsed=False)
190final_series.set_lines_aspect('Purple', 4, LineAspectEnum.solid)
191
192summary_plot.update_plot()

We can see the results of the user task in the before (Fig. 26) and after (Fig. 27) images below.

../_images/tutorial_4_before.png

Fig. 26 The original mismatch of the oil (“Kite - qo”, green dashed line) and simulation results (“Simulated oil rate”, solid green line) before the execution of the user task.#

../_images/tutorial_4_after.png

Fig. 27 The final mismatch of the oil (“Kite - qo”, green dashed line) and simulation results (“Simulated oil rate”, solid green line) after the execution of the user task.#

The simulation file is given below. It is similar to what is found in Tutorial 2, with additional code to initialize the input for the Topaze document.

Click to view the full contents of tutorial_4_simulation.py
Listing 34 tutorial_4_simulation.py#
 1from kappa_sdk import Connection, KWModuleEnum
 2from kappa_sdk.user_tasks import Context
 3from kappa_sdk.user_tasks.simulation import UserTaskInstance
 4
 5
 6ka_server_address = 'https://your-ka-instance'
 7print('Connecting to {}'.format(ka_server_address))
 8connection = Connection(ka_server_address, verify_ssl=False)
 9
10field_name = 'SPE RTA Demo'
11print("Reading [{}] field data...".format(field_name))
12field = next(x for x in connection.get_fields() if x.name == field_name)
13
14well_name = 'KITE'
15print("Reading [{}] well...".format(well_name))
16well = next(x for x in field.wells if x.name == well_name)
17
18context = Context()
19context.field_id = field.id
20context.well_id = well.id
21
22task = UserTaskInstance(connection, context)
23print("  [->] Well Name = {}".format(task.well.name))
24
25print('[Tutorial 4] Binding parameters..')
26rate_data_type = 'qo'
27rate_name_prefix = 'OIL - '
28rate_data = next(x for x in task.well.data if x.data_type == rate_data_type and x.name.startswith(rate_name_prefix))
29print("  [->] Rate Name = {}".format(rate_data.name))
30task.inputs['Rate'] = rate_data.vector_id
31
32pressure_data_type = 'BHP'
33pressure_name_prefix = 'Calculated'
34pressure_data = next(x for x in task.well.data if x.data_type == pressure_data_type and x.name.startswith(pressure_name_prefix))
35print("  [->] Pressure Name = {}".format(pressure_data.name))
36task.inputs['Pressure'] = pressure_data.vector_id
37
38task.inputs['Window size'] = 21
39task.inputs['Time decimation'] = 168
40
41document_name = 'UR_example.kt5'
42document = next(x for x in task.well.documents if x.type == KWModuleEnum.topaze and (document_name is None or document_name in x.name))
43print('Found {}'.format(document.name))
44task.inputs['Topaze file'] = document.file_id
45
46task.run()