def test_swatinit_less_than_1_below_contact(simulator, tmp_path): """SWATINIT below the contact is ignored, and SWAT is set based on the input SWOF table. In water-wet system (pc>0), this always yields SWAT=1 """ os.chdir(tmp_path) model = PillarModel(cells=1, apex=1000, owc=[900], swatinit=[0.7], swl=[0.1]) qc_frame = run_reservoir_simulator(simulator, model) qc_vols = qc_volumes(qc_frame) assert qc_frame["QC_FLAG"][0] == __HC_BELOW_FWL__ assert np.isclose(qc_frame["SWAT"][0], 1) assert np.isclose(qc_vols[__HC_BELOW_FWL__], (1 - 0.7) * qc_frame["PORV"][0]) if "flow" in simulator: assert np.isclose(qc_frame["PPCW"][0], 3.0) assert np.isclose(qc_frame["PC_SCALING"][0], 1.0) assert np.isclose(qc_frame["PC"], 0) else: # E100 will not report a PPCW in this case, libecl gives -1e20, # which becomes a NaN through ecl2df and then NaN columns are dropped. if "PPCW" in qc_frame: assert pd.isnull(qc_frame["PPCW"][0]) if "PC_SCALING" in qc_frame: assert pd.isnull(qc_frame["PC_SCALING"][0]) if "PC" in qc_frame: assert pd.isnull(qc_frame["PC"][0])
def test_volplot_manynegative(): qc_frame = pd.DataFrame([ { "EQLNUM": 1, "Z": 1000, "SWATINIT": 0.9, "PORV": 100000, "SWAT": 0.2, "VOLUME": 80, "QC_FLAG": __SWL_TRUNC__, "SATNUM": 1, "PCOW_MAX": 2, "PPCW": 4, "PC_SCALING": 2, "OIP_INIT": 0, }, { "EQLNUM": 1, "Z": 1000, "SWATINIT": 0.9, "PORV": 100000, "SWAT": 0.2, "VOLUME": 80, "QC_FLAG": __SWATINIT_1__, "SATNUM": 1, "PCOW_MAX": 2, "PPCW": 4, "PC_SCALING": 2, "OIP_INIT": 0, }, ]) wvol_waterfall(qc_volumes(qc_frame)) print("Verify that y limits in particular are correct") pyplot.show()
def test_swat_higher_than_swatinit_via_swl_above_contact(simulator, tmp_path): """If SWL is set higher than SWATINIT, both Eclipse and flow truncates SWAT to SWL. QC category is "Water gained" """ os.chdir(tmp_path) model = PillarModel(cells=1, apex=1000, owc=[2000], swatinit=[0.3], swl=[0.5]) qc_frame = run_reservoir_simulator(simulator, model) assert qc_frame["QC_FLAG"][0] == __SWL_TRUNC__ assert np.isclose(qc_frame["SWAT"][0], 0.5) assert np.isclose(qc_frame["SWATINIT"][0], 0.3) qc_vols = qc_volumes(qc_frame) assert np.isclose(qc_vols[__SWL_TRUNC__], (0.5 - 0.3) * qc_frame["PORV"][0]) if "flow" in simulator: expected_ppcw = 12.650095 assert np.isclose(qc_frame["PPCW"][0], expected_ppcw) # oip_init=0 else: assert np.isclose(qc_frame["PPCW"][0], 3.0) # When SWL is truncated, we cannot trust PC_SCALING to be used to # compute PC, so it is removed from the dataframe. assert pd.isnull(qc_frame["PC_SCALING"][0]) assert pd.isnull(qc_frame["PC"][0])
def test_swatinit_almost1_slightly_above_contact(simulator, tmp_path): """The result is discontinuous close to swatinit=1 for Eclipse100, because at swatinit = 1 - epsilon, Eclipse will try to scale the capillary pressure, and is only limited by PPCWMAX (but not in this test). flow is not discontinuous in SWAT as a function of SWATINIT, but in PPCW as a function of SWATINIT. """ os.chdir(tmp_path) if "flow" in simulator: p_cap = 0.37392 else: p_cap = 0.3738366 model = PillarModel(cells=1, apex=1000, owc=[1030], swatinit=[0.999], swl=[0.1]) qc_frame = run_reservoir_simulator(simulator, model) assert qc_frame["QC_FLAG"][0] == __PC_SCALED__ assert np.isclose(qc_frame["SWAT"][0], 0.999) assert np.isclose(qc_frame["PC"], p_cap, atol=0.001) needed_scaling = p_cap / model.evaluate_pc(0.999) # Worryingly inaccurate? assert np.isclose(qc_frame["PPCW"][0], needed_scaling * 3.0, atol=1) qc_vols = qc_volumes(qc_frame) assert np.isclose(qc_vols[__PC_SCALED__], 0.0, atol=0.0003)
def test_volplot_negative_bars(): """Test the volumetrics waterfall chart with negative values, giving negative bars, interaticve plot test""" qc_frame = pd.DataFrame([ # This dataframe is a minimum dataset for check_swatinit # to run. { "EQLNUM": 1, "Z": 1000, "SWATINIT": 0.9, "PORV": 100, "SWAT": 0.8, "VOLUME": 80, "QC_FLAG": __SWL_TRUNC__, "SATNUM": 1, "PCOW_MAX": 2, "PPCW": 4, "PC_SCALING": 2, "OIP_INIT": 0, } ]) wvol_waterfall(qc_volumes(qc_frame)) print("Verify that all annotations are visible") pyplot.show()
def test_swatinit_1_far_above_contact(simulator, tmp_path): """If SWATINIT is 1 far above the contact, we are in an unstable situation (water should not be mobile e.g) Eclipse doc says this: "If a cell is given saturation corresponding to a zero capillary pressure (typically 1.0) above the contact, then the Pc curve cannot be scaled to honor the saturation, hence the Pc curve is left unscaled." The SWATINIT is effectively ignored by Eclipse100: SWAT is taken from the SWOF table and the relevant Pc pressure, and since that Pc curve is not touched by SWATINIT, SWAT becomes SWL far above contact. """ os.chdir(tmp_path) model = PillarModel( cells=1, apex=1000, owc=[2000], swatinit=[1], swl=[0.1], maxpc=[3.0] ) qc_frame = run_reservoir_simulator(simulator, model) qc_vols = qc_volumes(qc_frame) assert qc_frame["QC_FLAG"][0] == __SWATINIT_1__ if "flow" in simulator: # Flow accepts this swatinit, but this water will flow out assert np.isclose(qc_frame["SWAT"][0], 1) # PPCW is the input Pc: assert np.isclose(qc_frame["PPCW"][0], 3.0) assert np.isclose(qc_vols[__SWATINIT_1__], (1 - 1) * qc_frame["PORV"]) else: # E100 ignores SWATINIT and sets the saturation to SWL: assert np.isclose(qc_frame["SWAT"][0], 0.1) assert np.isclose(qc_frame["PPCW"][0], 3.0) # Negative number means water is lost: assert np.isclose(qc_vols[__SWATINIT_1__], -(1 - 0.1) * qc_frame["PORV"]) # Not possible to compute PC, it should be Nan: assert np.isnan(qc_frame["PC"][0]) # Bigger reservoir model, so that OWC is within the grid, should # not make a difference: biggermodel = PillarModel( cells=200, apex=1000, owc=[2000], swatinit=[1] * 200, swl=[0.1] ) qc_frame = run_reservoir_simulator(simulator, biggermodel) assert set(qc_frame["QC_FLAG"]) == set([__SWATINIT_1__, __WATER__]) assert qc_frame[qc_frame["Z"] < 2000]["QC_FLAG"].unique()[0] == __SWATINIT_1__ assert qc_frame[qc_frame["Z"] > 2000]["QC_FLAG"].unique()[0] == __WATER__ if "flow" in simulator: assert np.isclose(qc_frame["SWAT"][0], 1) # PPCW is the input Pc: assert np.isclose(qc_frame["PPCW"][0], 3.0) else: # E100 ignores SWATINIT and sets the saturation to SWL: assert np.isclose(qc_frame["SWAT"][0], 0.1) assert np.isclose(qc_frame["PPCW"][0], 3.0) assert np.isnan(qc_frame["PC"][0])
def test_swat_limited_by_ppcwmax_above_contact(simulator, tmp_path): """Test PPCWMAX far above contact. This keyword is only supported by Eclipse100 and will be ignored by flow. This leads to water being lost from SWATINIT to SWAT. """ os.chdir(tmp_path) swatinit = 0.8 model = PillarModel( cells=1, apex=1000, owc=[1100], swatinit=[swatinit], ppcwmax=[3.01] ) qc_frame = run_reservoir_simulator(simulator, model) assert np.isclose(qc_frame["PPCWMAX"][0], 3.01) qc_vols = qc_volumes(qc_frame) swat_if_ppcwmax = 0.5746147 if "eclipse" in simulator: # for PPCWMAX set to 3.01 Eclipse100 will scale the swatinit value to this: assert qc_frame["QC_FLAG"][0] == __PPCWMAX__ assert np.isclose(qc_frame["SWAT"][0], swat_if_ppcwmax) assert np.isclose(qc_frame["PC_SCALING"][0], 3.01 / 3.00) actual_pc = 1.4226775 assert np.isclose( model.evaluate_pc(swat_if_ppcwmax, scaling=3.01 / 3.00), actual_pc ) assert np.isclose(qc_frame["PC"][0], actual_pc) assert np.isclose( qc_vols[__PPCWMAX__], (swat_if_ppcwmax - swatinit) * qc_frame["PORV"][0] ) else: # "flow" does not support PPCWMAX and will scale the Pc curve as much is needed # and will thus reproduce swatinit: assert qc_frame["QC_FLAG"][0] == __PC_SCALED__ assert np.isclose(qc_frame["SWAT"][0], swatinit) expected_scaling = 2.107745 assert np.isclose(qc_frame["PC_SCALING"][0], expected_scaling) actual_pc = 1.405163 # The capillary pressure in this cell: assert np.isclose( model.evaluate_pc(swatinit, scaling=expected_scaling), actual_pc ) assert np.isclose(qc_frame["PC"][0], actual_pc) # If we had done scaling according to PPCWMAX, we would have gotten # the same result as in Eclipse: assert np.isclose( # Worryingly inaccurate? model.evaluate_sw(actual_pc, scaling=3.01 / 3.00), swat_if_ppcwmax, atol=0.02, # (opm probably uses monotone cubic interpolation in SWOF, but that # should not affect SWOF tables with only two points) ) # Not very accurate: assert np.isclose(qc_vols[__PC_SCALED__], 0, atol=0.001)
def test_swatinit_less_than_1_below_contact_neg_pc(simulator, tmpdir): """For an oil-wet system, there can be oil below free water level. Flow will set water saturation to 1 no questions asked. Bug? Eclipse ignores SWATINIT but calculates SWAT based on the input Pc-curve, and can thus give SWAT<1 if pc_min < 0. """ tmpdir.chdir() model = PillarModel( cells=1, apex=1000, owc=[900], swatinit=[0.7], swl=[0.1], maxpc=[3.0], minpc=[-3.0], ) # Eclipse will pick this SWAT: expected_swat = 0.7915066 # This must then be the Pc in the cell: actual_pc = -1.610044 p_cap = model.evaluate_pc(expected_swat) assert np.isclose(p_cap, actual_pc) qc_frame = run_reservoir_simulator(simulator, model) assert qc_frame["QC_FLAG"][0] == __HC_BELOW_FWL__ qc_vols = qc_volumes(qc_frame) if "flow" in simulator: assert np.isclose(qc_frame["SWAT"][0], 1.0) assert np.isclose(qc_frame["PPCW"][0], 3.0) assert np.isclose(qc_frame["PC_SCALING"][0], 1.0) # Computed Pc is wrong here, but is what corresponds # to the saturation picked by OPM-flow: assert np.isclose(qc_frame["PC"][0], -3.0) assert np.isclose(qc_vols[__HC_BELOW_FWL__], (1 - 0.7) * qc_frame["PORV"][0]) else: assert np.isclose(qc_frame["SWAT"][0], expected_swat) # PPCW is set to NaN, so we don't have that column if "PPCW" in qc_frame: assert pd.isnull(qc_frame["PPCW"][0]) if "PC_SCALING" in qc_frame: assert pd.isnull(qc_frame["PC_SCALING"][0]) if "PC" in qc_frame: assert pd.isnull(qc_frame["PC"][0]) assert np.isclose(qc_frame["PCW"][0], 3.0) # Untouched input assert np.isclose( qc_vols[__HC_BELOW_FWL__], (expected_swat - 0.7) * qc_frame["PORV"][0], atol=0.1, )
def test_accepted_swatinit_far_above_contact(simulator, tmpdir): """Test a "normal" scenario, SWATINIT is accepted and some PC scaling will be applied far above the contact """ tmpdir.chdir() model = PillarModel(cells=1, apex=1000, owc=[1100], swatinit=[0.1], swl=[0.0], maxpc=[3.0]) qc_frame = run_reservoir_simulator(simulator, model) assert np.isclose(qc_frame["SWAT"][0], 0.1) assert qc_frame["QC_FLAG"][0] == __PC_SCALED__ qc_vols = qc_volumes(qc_frame) assert np.isclose(qc_vols[__PC_SCALED__], 0, atol=0.0001) if "flow" in simulator: # Flow returns the unscaled SWOF input here assert np.isclose(qc_frame["PCW"][0], 3.0) expected_ppcw = 1.5612928 assert np.isclose( qc_frame["PPCW"][0], expected_ppcw ) # This is what it had to be scaled to to reach swatinit. assert np.isclose(qc_frame["PC_SCALING"][0], expected_ppcw / 3.0) # The actual Pc value can be back-calculated from SWOF: actual_pc = model.evaluate_pc(0.1, scaling=expected_ppcw / 3.0) assert np.isclose(actual_pc, 1.4051635) # in bars. assert np.isclose(qc_frame["PC"], actual_pc) # At surface conditions, density difference is 200 kg/m3, this number # is sort of "close" to 200 kg/m3 * 9.81 m/s^2 * 100 meters / 1e5 = 1.96 # (mismatch due to Bo and compressibility) else: # Eclipse100, numbers are only slightly different: expected_ppcw = 1.5807527 assert np.isclose(qc_frame["PCW"][0], expected_ppcw) assert np.isclose(qc_frame["PPCW"][0], expected_ppcw) assert np.isclose(qc_frame["PC_SCALING"][0], expected_ppcw / 3.0) # (cell centre is 5 meters above contact) # The actual Pc value can be back-calculated from SWOF: actual_pc = model.evaluate_pc(0.1, scaling=expected_ppcw / 3.0) assert np.isclose(actual_pc, 1.42267743) assert np.isclose(qc_frame["PC"], actual_pc)
def test_swatinit_1_slightly_above_contact(simulator, tmpdir): """If we are slightly above the contact, item 9 in EQUIL plays a small role. SWATINIT=1 is still ignored above contact, Pc curve is left untouched. """ tmpdir.chdir() model = PillarModel(cells=1, apex=1000, owc=[1030], swatinit=[1], swl=[0.1], oip_init=0) qc_frame = run_reservoir_simulator(simulator, model) assert qc_frame["QC_FLAG"][0] == __SWATINIT_1__ qc_vols = qc_volumes(qc_frame) if "flow" in simulator: expected_swat = 0.887824 actual_pc = 0.37392 else: expected_swat = 0.887849 actual_pc = 0.3738366 if "flow" in simulator: # Flow accepts this swatinit, but this water will flow out. assert np.isclose(qc_frame["SWAT"][0], 1) assert np.isnan(qc_frame["PC"][0]) assert np.isclose(qc_vols[__SWATINIT_1__], (1 - 1) * qc_frame["PORV"]) else: # E100: assert np.isclose(qc_frame["SWAT"][0], expected_swat) assert np.isclose(qc_frame["PC"][0], actual_pc) assert np.isclose(qc_vols[__SWATINIT_1__], (expected_swat - 1) * qc_frame["PORV"]) assert model.evaluate_pc(0.1) == 3.0 assert model.evaluate_pc(1) == 0 # The actual capillary pressure in this cell: assert np.isclose(model.evaluate_pc(expected_swat), actual_pc) # Check that if we run without SWATINIT, even flow will give this # saturation: model.swatinit = [None] # hacking the model object qc_frame = run_reservoir_simulator(simulator, model) assert np.isclose(qc_frame["SWAT"][0], expected_swat, atol=0.001)
def test_volplot_largenegative(): qc_frame = pd.DataFrame([{ "EQLNUM": 1, "Z": 1000, "SWATINIT": 0.9, "PORV": 100000, "SWAT": 0.2, "VOLUME": 80, "QC_FLAG": __SWL_TRUNC__, "SATNUM": 1, "PCOW_MAX": 2, "PPCW": 4, "PC_SCALING": 2, "OIP_INIT": 0, }]) wvol_waterfall(qc_volumes(qc_frame)) print("Verify that all annotations are visible and placed wisely") pyplot.show()
def test_volplot_zerospan(): # Test when there is no difference from SWATINIT to SWAT: qc_frame = pd.DataFrame([{ "EQLNUM": 1, "Z": 1000, "SWATINIT": 0.9, "PORV": 100000, "SWAT": 0.9, "VOLUME": 80, "QC_FLAG": __SWL_TRUNC__, "SATNUM": 1, "PCOW_MAX": 2, "PPCW": 4, "PC_SCALING": 2, "OIP_INIT": 0, }]) wvol_waterfall(qc_volumes(qc_frame)) print("Verify that all annotations are visible and placed wisely") pyplot.show()
def test_accepted_swatinit_slightly_above_contact(simulator, tmpdir): """Test a "normal" scenario, SWATINIT is accepted and some PC scaling will be applied some meters above the contact QC-wise, these cells will not be flagged, but contribute to average PC_SCALING """ tmpdir.chdir() model = PillarModel(cells=1, apex=1000, owc=[1020], swatinit=[0.5], swl=[0.0], maxpc=[3.0]) qc_frame = run_reservoir_simulator(simulator, model) # E100 is not very accurate here, flow gives exactly 0.5 assert np.isclose(qc_frame["SWAT"][0], 0.5, atol=0.001) assert qc_frame["QC_FLAG"][0] == __PC_SCALED__ # Here, the Pc-curve is scaled (it goes from 3 to 0 in SWOF). At # swatinit=0.5, pc_swof is 1.5. qc_vols = qc_volumes(qc_frame) assert np.isclose(qc_vols[__PC_SCALED__], 0, atol=0.00001) if "flow" in simulator: # Flow returns the unscaled SWOF input here assert np.isclose(qc_frame["PCW"][0], 3.0) expected_ppcw = 0.4495535 assert np.isclose(qc_frame["PPCW"][0], expected_ppcw) # This is what it had to be scaled to to reach swatinit. assert np.isclose(qc_frame["PC_SCALING"][0], expected_ppcw / 3.0) actual_pc = model.evaluate_pc(0.5, scaling=expected_ppcw / 3.0) assert np.isclose(actual_pc, 0.22477675) assert np.isclose(qc_frame["PC"], actual_pc) else: # Eclipse100, numbers are a tad different: assert np.isclose(qc_frame["PCW"][0], 0.4485523) assert np.isclose(qc_frame["PPCW"][0], 0.4485523)
def test_swatinit_1_below_contact(simulator, tmp_path): """An all-good scenario, below contact, water-wet, ask for water, we get water.""" os.chdir(tmp_path) model = PillarModel( cells=1, apex=1000, owc=[100], swatinit=[1], swl=[0.1], maxpc=[3.0], ) qc_frame = run_reservoir_simulator(simulator, model) assert qc_frame["QC_FLAG"][0] == __WATER__ assert np.isclose(qc_frame["SWAT"][0], 1) if "flow" in simulator: assert np.isclose(qc_frame["PPCW"][0], 3.0) assert np.isclose(qc_frame["PC"][0], 0) else: if "PPCW" in qc_frame: assert pd.isnull(qc_frame["PPCW"][0]) qc_vols = qc_volumes(qc_frame) assert np.isclose(qc_vols[__WATER__], 0.0)
def test_qc_volumes(propslist, expected_dict): """Test that we calculate qc volumes correctly from a cell-based qc dataframe""" qc_frame = pd.DataFrame(propslist) qc_vols = qc_volumes(qc_frame) for key in expected_dict.keys(): assert np.isclose(qc_vols[key], expected_dict[key])