Skip to content

A waterflood, explained

A small waterflood stated from what the reservoir is, with no deck to start from: a 3 × 3 × 1 grid, its rock, a dead oil and its water, one producer and one water injector, in field units. It is one ps.Model, stated by keyword; Build a model from a word problem has the questions each keyword answers.

import petroscript as ps
flood = ps.Model(
name="FROM_SCRATCH_WATERFLOOD",
units="field",
start="2020-01-01",
grid=ps.grid.Cartesian(shape=(3, 3, 1), dx=100.0, dy=100.0, dz=20.0, top=2500.0),
rock=ps.Rock(porosity=0.25, permeability=100.0, vertical_ratio=0.1,
compressibility=3.0e-6, reference_pressure=14.7),
fluid=ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[14.7, 3500.0], oil_fvf=[1.06, 1.0],
oil_viscosity=[1.0, 1.2]),
water=ps.pvt.Water(reference_pressure=3000.0, fvf=1.0, compressibility=3.0e-6,
viscosity=0.5, viscosibility=0.0),
oil_surface_density=53.0,
water_surface_density=64.0,
),
relperm=[ps.relperm.OilWater(sw=[0.2, 0.8], krw=[0.0, 1.0], krow=[1.0, 0.0],
pcow=[0.0, 0.0])],
initial=ps.initial.Equilibrium(datum_depth=2510.0, datum_pressure=3000.0,
oil_water_contact=2600.0),
wells=[ps.Well(name="PROD1", i=3, j=3), ps.Well(name="INJ1", i=1, j=1)],
schedule=ps.Schedule(
end="2020-01-31",
events=[ps.Event("2020-01-01",
ps.Perforate(well="PROD1", k=(1, 1), diameter=0.3),
ps.Perforate(well="INJ1", k=(1, 1), diameter=0.3),
ps.Produce(well="PROD1", oil_rate=220.0, bhp_limit=1200.0),
ps.Inject(well="INJ1", phase="water", rate=220.0, bhp_limit=4500.0))],
),
summary=ps.Summary(field=["oil_production_rate", "water_production_rate"],
wells={"*": ["bottom_hole_pressure"]}),
)

The injector puts back about what the producer takes out: 220 stb/day of water for 220 stb/day of oil whose formation volume factor is near 1.01 at the datum.

The rock: porosity, permeability and compressibility

Section titled “The rock: porosity, permeability and compressibility”

Rock is three facts. porosity=0.25 and permeability=100.0 (md, with the vertical permeability a tenth of it, vertical_ratio=0.1) say how much the rock holds and how easily fluid flows through it. compressibility=3.0e-6 at reference_pressure=14.7 says how its pore volume grows with pressure: 3 × 10⁻⁶ 1/psi at 14.7 psia, SPE1’s rock (Odeh, “Comparison of Solutions to a Three-Dimensional Black-Oil Reservoir Simulation Problem”, Journal of Petroleum Technology, January 1981, table 1, as the corpus deck spe1/SPE1CASE1.DATA in opm-tests states it on its ROCK card). The numbers are in the model’s units, 1/psi and psia in a field model. A metric model states the same rock as compressibility=4.35e-5 at reference_pressure=1.0135, in 1/bar and bar (14.5038 psi to the bar).

The porosity is the porosity at the reference pressure, and the simulator grows the pore volume from there: at this model’s 3,000 psia datum the pore volume is about 0.9% larger than a porosity of 0.25 alone gives. flood.describe()["facts"]["rock"] holds that factor:

assert flood.check() == [] # no error, no warning, no information row
rock_facts = flood.describe()["facts"]["rock"]["1"]
assert round(rock_facts["pore_volume_factor"]["value"], 3) == 1.009

Leave the compressibility out and OPM Flow runs the model with incompressible rock: its pore volume does not change with pressure, the rock’s share of the total compressibility is gone, and the pressure falls faster for each barrel produced than the reservoir’s would. m.check() says so once a well moves the pressure, and PetroScript never supplies the number:

incompressible = flood.replace(
rock=flood.rock.replace(compressibility=None, reference_pressure=None))
assert ("warning", "physics.rock.compressibility_unstated", "rock/compressibility") in [
(row.severity, row.code, row.address) for row in incompressible.check()
]

Its fix line says what to type: the field’s own compressibility with its reference pressure, or compressibility=0.0 to record incompressible rock as a choice. A gas-water model gets an information row instead of a warning, since the gas’s expansion carries its pressure response, and a rock compaction table (compaction=) answers the row too.

deck = ps.compile(flood)
text = deck.files["FROM_SCRATCH_WATERFLOOD.DATA"]
rock_card = text.split("\nROCK\n")[1].splitlines()[0].split()
assert rock_card == ["14.7", "3e-06", "/"] # the reference pressure, then the compressibility
deck.write("flood") # flood/FROM_SCRATCH_WATERFLOOD.DATA

deck.run() runs the deck in OPM Flow; QUICKSTART.md runs a model and reads its results by name.

PetroScript is licensed under the Apache License 2.0. © 2026 Blauweiss EDV LLC. PetroScript by Blauweiss AI.For AI agents: llms.txt