One value, checked at every step
A PetroScript model is one value, ps.Model. It is frozen: nothing about it changes in place.
Every edit is a verb that returns a new model, and every new model is checked as it is made.
Three habits follow.
1. Keep what the verb returns
Section titled “1. Keep what the verb returns”import petroscript as ps
m = ps.load("opm-tests/spe1/SPE1CASE2.DATA")m.at("2016-01-01", ps.Shut(well="PROD")) # returns a new model, then throws it awayassert m.control("PROD", at="2016-06-01") is not Nonem = m.at("2016-01-01", ps.Shut(well="PROD")) # keeps itThe verbs: m.at(date, *changes) dates changes, m.set(address, value) sets any fact by its
address (None removes it), m.until(date) moves the end, m.apply(edit) adds a grid edit, and
x.replace(**fields) copies any part with fields changed. Python’s own dataclasses.replace and
attribute assignment (m.grid = ...) are refused: a model is not a dataclass, and it is frozen.
2. Facts that only hold together change together
Section titled “2. Facts that only hold together change together”Each verb’s result is checked at once, so a step that leaves the model contradicting itself is
refused even if the next line would have fixed it. A new well without its control is the common
case: set them in one m.set, with a mapping of addresses:
start = m.start.isoformat()try: m.set("wells/P2", ps.Well(name="P2", i=5, j=5)) # alone: refused at onceexcept ps.ModelError as refused: print(refused.issues[0].code) # well.control.missingm = m.set({ "wells/P2": ps.Well(name="P2", i=5, j=5), 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=1000.0, bhp_limit=1000.0),})The same holds for a live oil and its depth table, a wet gas and its phase switch, a network node and its branches: whatever only makes sense together goes in one call.
3. Read the rows: errors refuse, warnings inform
Section titled “3. Read the rows: errors refuse, warnings inform”The check has two halves. What makes a model impossible is refused as the value is built
(ps.ModelError, or InputError for a part built wrong), naming the address and the fix. What
might be a mistake but can be meant is a row of m.check(): a warning or an information row,
never a refusal, each with its address and code.
for issue in m.check(): print(issue.severity, issue.address, issue.code)errors = [issue for issue in m.check() if issue.severity == "error"] # ps.compile refuses theseassert not errorsA few facts can only be judged on the whole model, and wait for m.check() and ps.compile
rather than refusing at once: a well introduced at a later date with nothing at that date
([well.introduced.unanchored]). A warning the model means is acknowledged by its code:
m = m.replace(acknowledged_warnings=["physics.rock.compressibility_unstated"]).
Everything has an address
Section titled “Everything has an address”Every fact a model states has an address, and the verbs read and write by it:
print(m.find("schedule/*/P2/*")) # the dated statements naming P2print(m.get("initial/1/datum_pressure")) # one factbefore = ps.load("opm-tests/spe1/SPE1CASE2.DATA")print([change.address for change in before.diff(m)][:5]) # what changed, by addressps.help("addresses") explains the address grammar; m.find(pattern) lists what is there.
What PetroScript never fills in
Section titled “What PetroScript never fills in”A number the reservoir decides is never supplied for you:
| Fact | Left out |
|---|---|
| Rock compressibility | OPM Flow runs incompressible rock; once a well moves the pressure, m.check() warns (physics.rock.compressibility_unstated; an information row on a gas-water model) |
| Gas-oil contact | refused where gas and oil both flow; state it above the reservoir’s top for no gas cap |
| A live oil’s depth table | refused unless the datum is at the gas-oil contact |
| A well’s control | refused (well.control.missing) |
| A well’s BHP reference depth | the simulator derives it; ps.lift.generate needs it stated |
| A component pair’s interaction coefficient | 0 |
PetroScript is licensed under the Apache License 2.0. © 2026 Blauweiss EDV LLC. PetroScript by Blauweiss AI.For AI agents: llms.txt