Skip to content

Chapter 5 — Strict construction, and what a refusal owes you

Every public class builds a strict, frozen value. There is no path from the public surface to a raw keyword record, no .DATA shortcut, and no second spelling that means the same thing in different words. That is the strict-construction contract, and it is why a model that built at all is already most of the way to being correct.

The contract has a cost you will meet early: things that could have been guessed are asked for instead. ps.Model requires units= and start=. An injection states its phase. A rock states its compressibility, or the model says it is missing. Each of those is a value that, defaulted silently, produces a model that runs and is wrong.

Failures land at the earliest place that can know about them. A model that contradicts itself is refused as it is built, with every problem at once; a sound one carries the rows a check could only warn about, read with m.check():

greedy = model.until("2030-01-01").at("2020-02-01", ps.Adjust(well="PROD1", oil_rate=1.0e7))
rows = greedy.check() # every row at once, each with a code; never raises
assert ("warning", "physics.volumetrics.exceeds_stoiip") in [
(row.severity, row.code) for row in rows
]
ps.compile(model).write("checked") # a compile refuses an error row, so write after it

A refusal carries three parts, in your own values: the rule, the failing value, and the remedy spelled as code. A model’s refusal prints each row as [code] and its message, with the repair on the line below, fix: ...:

[schedule.date.outside_run] schedule/2030-01-01: the run ends 2024-12-29
fix: m = m.until("2030-01-01")

A misspelt field answers with the nearest one (did you mean 'bhp_limit'?), and an address that names nothing answers with the nearest name at the level it missed. When you get one of these, read it rather than guessing again: it names the answer.

Rows come back as ps.Issue values (code, severity, address, message, fix), on ps.ModelError and ps.CompileError in .issues. In tests, assert the code rather than the message:

ps.testing.assert_issue(greedy, code="physics.volumetrics.exceeds_stoiip")

ps.help(code) explains a code and its fix. Message text is prose and prose is allowed to improve; the code is the contract. A warning never blocks. A model that means one states its code in acknowledged_warnings=, and the row stays, demoted to info:

meant = greedy.replace(acknowledged_warnings=["physics.volumetrics.exceeds_stoiip"])
assert ("info", "physics.volumetrics.exceeds_stoiip") in [
(row.severity, row.code) for row in meant.check()
]

Only a physics warning’s code, or one of the three target.* warnings that name what the target cannot run, can be acknowledged; ps.help(code) says so for each one that can. Any other code is refused as the model is built, and an acknowledged code that does not fire is refused too (physics.acknowledge.stale).

A guard is a claim about physics, and a claim about physics can be wrong. So a refusal on physics owes the reader which of two things it is: the deck states physics the simulator has been measured to reject, or a combination it has been measured to abort on. The refusal for a well with no connection quotes the simulator’s own stop. The opm-flow target’s acceptance guards (petroscript/targets/opm_flow/acceptance.py), each a model Flow reads and then refuses to run or does not honour, quote a measurement taken on both pinned releases against its own controls.

A guard that misreads well-formed physics, a bound applied to a number that is not measuring what the bound is about, is a bug to file. Its fix widens the guard only as far as the measurement that forced it, cites that measurement beside the check, and keeps a case that still fails.

Three habits follow from the contract:

  • assert meaning before deck text: m.get, m.find and m.check() say what the model states, and a deck-text assertion that passes for the wrong reason is the most expensive test you can write;
  • when a value is background rather than meaning, leave a defaulted field unstated, and never invent a number for a field the model requires;
  • when the product refuses something you believe is valid, the refusal is either right or a bug worth filing; it is never something to route around with a private import.

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