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 raisesassert ("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 itA 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).
The guards are calibrated, not asserted
Section titled “The guards are calibrated, not asserted”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.findandm.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