Skip to content

Chapter 2 — From a word problem to a model

Do not start by matching keywords. Start by extracting what the reservoir is, then reach for the keyword argument that says it.

The questionWhat it fixesWhere it is said
What is the run’s identity?name, units, start dateps.Model(name=, units=, start=)
What grid exists?geometrygrid=ps.grid.Cartesian, CornerPoint, Radial, Spider
What rock fabric exists?porosity, permeability, compressibilityrock=ps.Rock(...), regions=ps.Regions(...)
Which fluids and physics are active?phases, tables, packagesfluid=ps.fluid.* with ps.pvt tables, relperm=[ps.relperm.*], physics=[...]
What is the initial state?contacts, pressureinitial=ps.initial.*
What changes over time?wells, controlswells=[ps.Well], schedule=ps.Schedule(events=[ps.Event(...)])
What should be observed?resultssummary=ps.Summary(...)

Fill that table before writing code. If a missing value would change the reservoir’s meaning, ask for it.

The model’s keywords read in the same order, and a part you change is a keyword again (model is chapter 1’s):

case = model.replace(
name="CASE",
grid=ps.grid.Cartesian(shape=(3, 3, 1), dx=100.0, dy=100.0, dz=20.0, top=2500.0),
wells=[ps.Well(name="PROD1", i=3, j=3)],
summary=ps.Summary(field=["oil_production_rate"],
wells={"PROD1": ["bottom_hole_pressure"]}),
)
assert case.grid.shape == (3, 3, 1)

Rock is three facts, not two: porosity, permeability and compressibility. The compressibility is how the pore volume grows with pressure, stated with the pressure it is measured at: ps.Rock(..., compressibility=4.0e-6, reference_pressure=3000.0) in the model’s units, 1/psi and psia in field units, 1/bar and bar in metric. Leave it 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. Once a well moves the pressure, m.check() says so with physics.rock.compressibility_unstated at rock/compressibility: a warning where oil or water carries the pressure response, an information row on a gas-water model, where the gas’s expansion dominates. PetroScript never supplies the number. State the field’s, or compressibility=0.0 to record incompressible rock as a choice; a compaction table (compaction=) moves the pore volume instead, and silences the row too.

unstated = model.replace(rock=ps.Rock(porosity=0.25, permeability=100.0, vertical_ratio=0.1))
assert ("warning", "physics.rock.compressibility_unstated", "rock/compressibility") in [
(row.severity, row.code, row.address) for row in unstated.check()
]
assert "physics.rock.compressibility_unstated" not in [row.code for row in model.check()]

A table is named columns, one value per row: ps.help(ps.relperm.OilWater) prints Rows: (sw, krw, krow, pcow), each column a list, a tuple or an array. from_frame reads the columns by name from a polars or pandas frame, or a mapping:

water_oil = ps.relperm.OilWater.from_frame(
{"sw": [0.2, 0.8], "krw": [0.0, 1.0], "krow": [1.0, 0.0], "pcow": [0.0, 0.0]}
)
assert case.get("relperm/1/oil_water") == water_oil

Two things authors expect to be automatic and are not.

Results are requested, never inferred. A group target does not report a group rate. Ask for the quantity by its name in ps.Summary: field=[names], then wells=, groups=, regions= and blocks= as {member: [names]}, "*" for every well or group.

A well is an identity; what it does is dated. ps.Well in wells= declares a name and a column. Its perforations and its control are changes an event states. A well with no control is refused (well.control.missing). A well with no perforation is a warning (well.completions.missing): Flow runs it and keeps it shut, so every control on it does nothing.

Longer recipes, the semantic red test, the evidence gate and refactoring without weakening simulator support, live in docs/petroscript-with-tests.md, which this chapter deliberately does not repeat.

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