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 forceAdd a well
Section titled “Add a well”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.
Change a control from a date
Section titled “Change a control from a date”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 datem = m.at(one_year, ps.Adjust(well="PROD", oil_rate=15_000.0)) # the BHP limit staysm = m.at(one_year, ps.Produce(well="P2", oil_rate=1500.0, bhp_limit=1200.0)) # all restatedA 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 keptassert "gas_oil_ratio" in m.summary.field # the deck's own request keptChange a property in part of the grid
Section titled “Change a property in part of the grid”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 editm.replace(edits=...) would replace the deck’s own grid edits instead of adding to them.
See what changed, and write it
Section titled “See what changed, and write it”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.DATAexamples/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.
Turn the deck into Python
Section titled “Turn the deck into Python”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.pyassert runpy.run_path(str(script))["model"] == m # it rebuilds the same modelFrom 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