Skip to content

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.

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 away
assert m.control("PROD", at="2016-06-01") is not None
m = m.at("2016-01-01", ps.Shut(well="PROD")) # keeps it

The 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 once
except ps.ModelError as refused:
print(refused.issues[0].code) # well.control.missing
m = 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 these
assert not errors

A 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"]).

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 P2
print(m.get("initial/1/datum_pressure")) # one fact
before = ps.load("opm-tests/spe1/SPE1CASE2.DATA")
print([change.address for change in before.diff(m)][:5]) # what changed, by address

ps.help("addresses") explains the address grammar; m.find(pattern) lists what is there.

A number the reservoir decides is never supplied for you:

FactLeft out
Rock compressibilityOPM 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 contactrefused where gas and oil both flow; state it above the reservoir’s top for no gas cap
A live oil’s depth tablerefused unless the datum is at the gas-oil contact
A well’s controlrefused (well.control.missing)
A well’s BHP reference depththe simulator derives it; ps.lift.generate needs it stated
A component pair’s interaction coefficient0

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