Skip to content

Edit a loaded deck

You have a deck. ps.load turns it into a model, you change it with verbs, and ps.compile writes it back, everything you did not touch kept as it was. This page makes the edits that come up most, on SPE1 from the opm-tests corpus (a FIELD deck: ft, psia, stb/day, Mscf/day).

Two rules carry every edit:

  • Nothing changes in place. Each verb returns a new model; keep it (m = m.at(...)).
  • Each verb returns a checked model. A model that contradicts itself is refused at once, so facts that only hold together (a well there from the start and its control) go in one call.
import datetime
import petroscript as ps
m = ps.load("opm-tests/spe1/SPE1CASE2.DATA")
print(m.units, m.start, [well.name for well in m.wells]) # field 2015-01-01 ['PROD', 'INJ']
print(m.control("PROD", at=m.start)) # the control in force

A new producer, its perforation and its control are one m.set, each at its address (wells/<name>, schedule/<date>/<well>/perforation/<n>, schedule/<date>/<well>/control):

start = m.start.isoformat()
m = m.set({
"wells/P2": ps.Well(name="P2", i=5, j=5, bhp_reference_depth=8400.0),
f"schedule/{start}/P2/perforation/1": ps.Perforate(well="P2", k=(3, 3), diameter=0.5),
f"schedule/{start}/P2/control": ps.Produce(well="P2", oil_rate=2000.0, bhp_limit=1000.0),
})

An injector is added the same way, with ps.Inject as its control. A well that does not exist before a date states introduced= that date, and the date in each address. Here a water injector opens on 2016-01-01:

m = m.set({
"wells/I2": ps.Well(name="I2", i=1, j=10, introduced="2016-01-01"),
"schedule/2016-01-01/I2/perforation/1": ps.Perforate(well="I2", k=(3, 3), diameter=0.5),
"schedule/2016-01-01/I2/control": ps.Inject(well="I2", phase="water", rate=5000.0, # stb/day
bhp_limit=6000.0), # psia
})

Setting the well alone first and giving it its control with m.at (or a rebuilt ps.Schedule) in a second step fails on the first step, because each step is checked: a well there from the start is refused at once, [well.control.missing] Well 'P2' has no reservoir control; one introduced later builds, but m.check() and ps.compile refuse it, [well.introduced.unanchored] Well 'I2' is introduced 2016-01-01 and no perforation or control at that report time names it. One m.set of all three works for both.

ps.Adjust changes one target or limit and keeps the rest of the control. A new ps.Produce replaces the whole control, so a limit it does not restate is gone:

one_year = m.start + datetime.timedelta(days=365) # a day number is a date
m = m.at(one_year, ps.Adjust(well="PROD", oil_rate=15_000.0)) # the BHP limit stays
m = m.at(one_year, ps.Produce(well="P2", oil_rate=1500.0, bhp_limit=1200.0)) # all restated

A date past the run’s end is refused; move the end first with m = m.until("2018-01-01"). A loaded deck states its report steps, not an end date, so m.schedule.end is None; its last report date is m.describe()["end"] ("2024-12-29" for SPE1).

Ask for more results without dropping the deck’s

Section titled “Ask for more results without dropping the deck’s”

Set the one address that changes. m.summary.replace(wells={...}) would replace every well’s requests:

m = m.set("summary/field/oil_production_total", "oil_production_total")
m = m.set("summary/wells/P2", ["oil_production_rate", "bottom_hole_pressure", "water_cut"])
m = m.set("summary/wells/*", # every well; the deck's own
[*m.summary.wells.get("*", ()), "bottom_hole_pressure"]) # requests are kept
assert "gas_oil_ratio" in m.summary.field # the deck's own request kept

A grid edit goes after the edits the deck already states, with m.apply. Layers, rows and columns are 1-based and inclusive:

m = m.apply(ps.grid.Scale(target="permeability_x", by=2.0, cells=ps.Box(k=(3, 3))))
layer_3 = m.array("permeability_x")[ps.Box(k=(3, 3)).index] # the cells, after the edit

m.replace(edits=...) would replace the deck’s own grid edits instead of adding to them.

original = ps.load("opm-tests/spe1/SPE1CASE2.DATA")
for change in original.diff(m): # address, before, after
print(change.address)
deck = ps.compile(m)
deck.write("spe1-edited") # spe1-edited/SPE1CASE2.DATA

examples/add_well.py does the same from the command line and runs the result in Flow when it is found. To change one number by its address, m.set("rock/porosity", ...) or any address m.find(...) lists works the same way.

A loaded deck can also be written as the Python that builds it, to keep and edit as code from then on. The script binds model; large arrays go beside it as .npy files:

import runpy
script = m.to_python("spe1_script") # spe1_script/SPE1CASE2.py
assert runpy.run_path(str(script))["model"] == m # it rebuilds the same model

From the shell, petroscript load CASE.DATA folder does the same (CLI).

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