From 3c0498e55fc5646dec0882541180010d4d7e562d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Wed, 18 May 2022 14:41:13 +0200 Subject: [PATCH] Dynamically update schedule from Python Adds some test cases that show how injection properties in Opm::Schedule can be dynamically modified from Python. --- CMakeLists.txt | 28 ++++++++++ python/{ => opm2}/simulators/__init__.py | 0 python/simulators/CMakeLists.txt | 30 +++++++---- python/test/test_schedule.py | 66 +++++++++++++++++++++--- 4 files changed, 106 insertions(+), 18 deletions(-) rename python/{ => opm2}/simulators/__init__.py (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 77407f322..98952b035 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,11 +25,39 @@ option(BUILD_FLOW_VARIANTS "Build the variants for flow by default?" OFF) option(BUILD_FLOW_POLY_GRID "Build flow blackoil with polyhedral grid" OFF) option(OPM_ENABLE_PYTHON "Enable python bindings?" OFF) option(OPM_ENABLE_PYTHON_TESTS "Enable tests for the python bindings?" ON) +option(OPM_INSTALL_PYTHON "Install python bindings?" ON) option(ENABLE_FPGA "Enable FPGA kernels integration?" OFF) option(USE_CHOW_PATEL_ILU "Use the iterative ILU by Chow and Patel?" OFF) option(USE_CHOW_PATEL_ILU_GPU "Run iterative ILU decomposition on GPU? Requires USE_CHOW_PATEL_ILU" OFF) option(USE_CHOW_PATEL_ILU_GPU_PARALLEL "Try to use more parallelism on the GPU during the iterative ILU decomposition? Requires USE_CHOW_PATEL_ILU_GPU" OFF) +# The following was copied from CMakeLists.txt in opm-common. +# TODO: factor out the common parts in opm-common and opm-simulator as a cmake module +if (OPM_ENABLE_PYTHON) + # We need to be compatible with older CMake versions + # that do not offer FindPython3 + # e.g. Ubuntu LTS 18.04 uses cmake 3.10 + if(${CMAKE_VERSION} VERSION_LESS "3.12.0") + find_package(PythonInterp REQUIRED) + if(PYTHON_VERSION_MAJOR LESS 3) + message(SEND_ERROR "OPM requires version 3 of Python but only version ${PYTHON_VERSION_STRING} was found") + endif() + set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE}) + set(Python3_LIBRARIES ${PYTHON_LIBRARIES}) + else() + # Be backwards compatible. + if(PYTHON_EXECUTABLE AND NOT Python3_EXECUTABLE) + set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE}) + endif() + find_package(Python3 REQUIRED COMPONENTS Interpreter) + endif() + + # Compatibility settings for PythonInterp and PythonLibs + # used e.g. in FindCwrap, pybind11 + set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) +endif() + + if(SIBLING_SEARCH AND NOT opm-common_DIR) # guess the sibling dir get_filename_component(_leaf_dir_name ${PROJECT_BINARY_DIR} NAME) diff --git a/python/simulators/__init__.py b/python/opm2/simulators/__init__.py similarity index 100% rename from python/simulators/__init__.py rename to python/opm2/simulators/__init__.py diff --git a/python/simulators/CMakeLists.txt b/python/simulators/CMakeLists.txt index be05e49c4..9fcc79ed7 100644 --- a/python/simulators/CMakeLists.txt +++ b/python/simulators/CMakeLists.txt @@ -39,17 +39,6 @@ if (PYTHON_SITE_PACKAGES_PATH MATCHES ".*/dist-packages/?" AND else() set(PYTHON_PACKAGE_PATH "site-packages") endif() -set(PYTHON_INSTALL_PREFIX "lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/${PYTHON_PACKAGE_PATH}" CACHE STRING "Subdirectory to install Python modules in") - -install(TARGETS simulators DESTINATION ${DEST_PREFIX}${CMAKE_INSTALL_PREFIX}/${PYTHON_INSTALL_PREFIX}/opm) - -file( COPY ${PROJECT_SOURCE_DIR}/python/test - DESTINATION ${PROJECT_BINARY_DIR}/python) -file( COPY ${PROJECT_SOURCE_DIR}/python/test_data - DESTINATION ${PROJECT_BINARY_DIR}/python) -file( MAKE_DIRECTORY ${PYTHON_OPM_SIMULATORS_PACKAGE_PATH} ) -file( COPY ${PROJECT_SOURCE_DIR}/python/simulators/__init__.py - DESTINATION ${PYTHON_OPM_SIMULATORS_PACKAGE_PATH}) if(OPM_ENABLE_PYTHON_TESTS) if(Python3_EXECUTABLE AND NOT PYTHON_EXECUTABLE) @@ -71,3 +60,22 @@ if(OPM_ENABLE_PYTHON_TESTS) -E env PYTHONPATH=${PYTHON_PATH} ${PYTHON_EXECUTABLE} -m unittest test/test_schedule.py) endif() + +# NOTE: instead of using file( COPY ...) which copies the files at configure time (not at build time) +# we should add copying of the files at build time such that running "make" or "make all" will +# update the files if they have been modified. We use the install.py script in opm-common, see also +# CMakeLists.txt in opm-common +add_custom_target(copy_python ALL + COMMAND ${PYTHON_EXECUTABLE} ${opm-common_DIR}/python/install.py + ${PROJECT_SOURCE_DIR}/python ${PROJECT_BINARY_DIR} 0) + +# Since the installation of Python code is nonstandard it is protected by an +# extra cmake switch, OPM_INSTALL_PYTHON. If you prefer you can still invoke +# setup.py install manually - optionally with the generated script +# setup-install.sh - and completely bypass cmake in the installation phase. +if (OPM_INSTALL_PYTHON) + set(PYTHON_INSTALL_PREFIX "lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/${PYTHON_PACKAGE_PATH}" CACHE STRING "Subdirectory to install Python modules in") + install(TARGETS simulators DESTINATION ${DEST_PREFIX}${CMAKE_INSTALL_PREFIX}/${PYTHON_INSTALL_PREFIX}/opm) + include(PyInstallPrefix) # from opm-common + install( CODE "execute_process(COMMAND ${PYTHON_EXECUTABLE} ${opm-common_DIR}/python/install.py ${PROJECT_BINARY_DIR}/python/opm2 ${DEST_PREFIX}${CMAKE_INSTALL_PREFIX}/${PYTHON_INSTALL_PREFIX} 1)") +endif() diff --git a/python/test/test_schedule.py b/python/test/test_schedule.py index f0232364a..058c942d8 100755 --- a/python/test/test_schedule.py +++ b/python/test/test_schedule.py @@ -3,6 +3,7 @@ import unittest from contextlib import contextmanager import datetime as dt from pathlib import Path +import re from opm.simulators import BlackOilSimulator from opm.io.parser import Parser from opm.io.ecl_state import EclipseState @@ -36,6 +37,7 @@ class TestBasic(unittest.TestCase): self.schedule = Schedule( self.deck, state ) summary_config = SummaryConfig(self.deck, state, self.schedule) self.unit_system = self.deck.active_unit_system() + self.assertTrue('PROD' in self.schedule) self.assertTrue('INJ' in self.schedule) self.assertEqual(dt.datetime(2015, 1, 1), self.schedule.start) @@ -55,13 +57,32 @@ class TestBasic(unittest.TestCase): #schedule.shut_well("PROD", 3) #prod = schedule.get_well("PROD", 3) #self.assertEqual(prod.status(), "SHUT") - self.subtest_modify_schedule_dynamically(well_name, report_step) + self.subtest_modify_prod_weltarg_dynamically(well_name, report_step) self.sim.step() + report_step = self.sim.current_step() + well_name = "INJ" + self.subtest_modify_inj_weltarg_dynamically(well_name, report_step) self.sim.advance(report_step=last_step) self.sim.step_cleanup() + def subtest_modify_inj_weltarg_dynamically(self, well_name, report_step): + prop = self.schedule.get_injection_properties(well_name, report_step) + self.assertEqual(prop['surf_inj_rate'], 100000.0) # Mscf/day + self.assertEqual(prop['resv_inj_rate'], 0.0) # rb/day + self.assertEqual(prop['bhp_target'], 9014.0) # psi + self.assertEqual(prop['thp_target'], 0.0) + new_grat_target = prop['surf_inj_rate'] - 100 # stb/day + self.update_inj_grat_target_wconinje(well_name, new_grat_target) + self.sim.step() + prop2 = self.schedule.get_injection_properties(well_name, report_step+1) + self.assertEqual(prop2['surf_inj_rate'], new_grat_target) + new_grat_target += 200 + self.update_inj_grat_target_weltarg(well_name, new_grat_target) + self.sim.step() + prop3 = self.schedule.get_injection_properties(well_name, report_step+2) + self.assertEqual(prop3['surf_inj_rate'], new_grat_target) - def subtest_modify_schedule_dynamically(self, well_name, report_step): + def subtest_modify_prod_weltarg_dynamically(self, well_name, report_step): prop = self.schedule.get_production_properties(well_name, report_step) self.assertEqual(prop['alq_value'], 0.0) self.assertEqual(prop['bhp_target'], 1000.0) @@ -72,13 +93,43 @@ class TestBasic(unittest.TestCase): self.assertEqual(prop['thp_target'], 0.0) self.assertEqual(prop['water_rate'], 0.0) new_oil_target = prop['oil_rate'] + 10000 # stb/day - #self.update_oil_target_wconprod(well_name, new_oil_target) - self.update_oil_target_weltarg(well_name, new_oil_target) + self.update_prod_orat_target_wconprod(well_name, new_oil_target) self.sim.step() prop2 = self.schedule.get_production_properties(well_name, report_step+1) - self.assertEqual(prop2['oil_rate'], 30000.0) + self.assertEqual(prop2['oil_rate'], new_oil_target) + new_oil_target += 1000 + self.update_prod_orat_target_weltarg(well_name, new_oil_target) + self.sim.step() + prop3 = self.schedule.get_production_properties(well_name, report_step+2) + self.assertEqual(prop3['oil_rate'], new_oil_target) - def update_oil_target_weltarg(self, well_name, oil_target): + # This is an alternative to using WELTARG + def update_inj_grat_target_wconinje(self, well_name, new_surf_flow_rate): + data = self.deck["WCONINJE"] + # assumes data looks like this: + # WCONINJE + # 'INJ' 'GAS' 'OPEN' 'RATE' 100000 1* 9014 / + # / + # The initial rate can also be obtained from data[0][4].get_uda(0).get_double() + data = re.sub(pattern='100000', repl=str(new_surf_flow_rate), string=str(data), count=1) + report_step = self.sim.current_step() + self.schedule.insert_keywords( + data, step=report_step, unit_system=self.unit_system) + + # This is an alternative to using WCONINJE to modify injection properties + def update_inj_grat_target_weltarg(self, well_name, net_surf_flow_rate): + data = """ +WELTARG + '{}' GRAT {} / +/ + """.format(well_name, net_surf_flow_rate) + report_step = self.sim.current_step() + self.schedule.insert_keywords( + data, step=report_step, unit_system=self.unit_system) + + + # This is an alternative to using WCONPROD to modify production properties + def update_prod_orat_target_weltarg(self, well_name, oil_target): data = """ WELTARG '{}' ORAT {} / @@ -88,7 +139,8 @@ WELTARG self.schedule.insert_keywords( data, step=report_step, unit_system=self.unit_system) - def update_oil_target_wconprod(self, well_name, oil_target): + # This is an alternative to using WELTARG to modify production properties + def update_prod_orat_target_wconprod(self, well_name, oil_target): well_status = "OPEN" control_mode = "ORAT" bhp_limit = 1000 # psia