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 rowrock_facts = flood.describe()["facts"]["rock"]["1"]assert round(rock_facts["pore_volume_factor"]["value"], 3) == 1.009Leave 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.
Compile and write
Section titled “Compile and write”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 compressibilitydeck.write("flood") # flood/FROM_SCRATCH_WATERFLOOD.DATAdeck.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