How two decks are compared
The comparator (petroscript.equivalence.compare_deck_texts in
src/petroscript/equivalence/decks.py) decides
whether two raw deck texts state the same reservoir semantics. It never
consults Model, the compiler, or example cases: both sides parse
into the same normalized keyword stream, and every difference the comparator
tolerates is one of the named normalization passes below. This document is
the external ledger of those passes; anything not listed here is compared
literally, and unknown keyword candidates are reported as failures, never
treated as layout.
Comparison order
Section titled “Comparison order”_inline_includeson each raw text;parse_normalized_deckon each side;_normalize_deck_semantics(the pipeline below) on each side;- the pairwise passes
_drop_unmatched_default_reportsand_drop_unpaired_title, then_drop_all_active_actnumon each side; - keyword-by-keyword, record-by-record, token-by-token comparison under
TolerancePolicy.
Text-level passes
Section titled “Text-level passes”| Pass | Rule |
|---|---|
parse_normalized_deck | Splits the text into keyword blocks — and takes three consumption rules from the deck reader itself rather than spelling its own. A TITLE body is the next line whatever it spells (manual 5.3.146); a report card’s record (_ONE_RECORD_KEYWORDS) runs to its / terminator, because native decks state report mnemonics unquoted in column one — SWAT FIP=3 THPRES … — where a mnemonic shadows an array keyword’s name; and a keyword is read wherever it stands, not only in column one, so a line carrying nothing but a dispatchable word opens a block (_indented_keyword, imported from the reader) while the program blocks whose records open with a bare array name are excluded exactly as the reader excludes them. A comparator applying a keyword test to a report record reads it as a card and leaves the report card empty; a comparator blind outside column one certifies a deck whose TUNING it never noticed. Either way the difference is billed to whichever side is complete. One deck, two readers, one reading: a test holds all three rules to the reader’s. |
_inline_includes (the reader’s, eclipse/reader.py) | Splices INCLUDE sidecar contents in place so both texts compare inline, resolving the stanza through the deck reader’s own machinery — the same stanza scan and the same PATHS alias table (manual 5.3.111), so both readers of a deck reach the same files. A target that resolves to no file is a refusal naming it, never a dropped stanza: a comparator that reads less deck than the deck states bills the difference to whichever side is complete. |
Deck-level pipeline (_normalize_deck_semantics, in order)
Section titled “Deck-level pipeline (_normalize_deck_semantics, in order)”Elision comes first: ECHO, NOECHO, UNIFIN, and UNIFOUT are removed
from both sides — they carry echo/print and IO framing mechanics, not
reservoir semantics.
| # | Pass | Rule |
|---|---|---|
| 2 | _normalize_phase_declaration_order | RUNSPEC phase/activation cards (OIL, GAS, WATER, dissolution flags, BRINE, PRECSALT, FOAM, SOLVENT, CO2SOL, H2SOL, POLYMER, CO2STORE, H2STORE) are independent switches; both sides sort them into one canonical order at the first card’s position. |
| 3 | _normalize_grid_policy_order | Scalar GRID policy cards with no array interaction (MINPV, MINPVV, PINCH, and the global transmissibility-method cards NEWTRAN/OLDTRAN/OLDTRANR) float to the end of their GRID run; their position inside the section is not deck state. |
| 4 | _normalize_props_keyword_order | PROPS keyword order is non-semantic between edit barriers (EQUALS, COPY, ADD, MULTIPLY, OPERATE); each barrier-free run sorts by keyword. |
| 4b | _normalize_regions_keyword_order | REGIONS keyword order is non-semantic between edit barriers (EQUALS, COPY, ADD, MULTIPLY, OPERATE, BOX, ENDBOX): a region map’s position in its section is deck shape; each run sorts by keyword. So is the order of the region maps GRID states (FLUXNUM, MULTNUM, OPERNUM, after materialization) where they stand side by side; any other GRID card ends the run. |
| 5 | _normalize_summary_keyword_order | SUMMARY vector order is output-column order, not deck semantics; each SUMMARY run sorts by keyword. |
| 2b | _normalize_boundary_connection_defaults | Defaulted BCCON cell bounds state the full grid extent on their axis (I1/J1/K1 → 1, I2/J2/K2 → NX/NY/NZ from the deck’s DIMENS), so an explicit full-range spelling states the same connection. |
| 5b | _normalize_runspec_keyword_order | RUNSPEC keyword order is non-semantic: every keyword in the section is an independent scalar declaration (phase card, dimensioning record, run directive) with no array or edit interaction, so keywords sort canonically by name (stable for restatements). |
| 6 | _normalize_solution_keyword_order | SOLUTION keyword order is non-semantic between edit barriers (EQUALS, COPY, ADD, MULTIPLY, OPERATE, BOX, ENDBOX). |
| 6b | _normalize_report_mnemonic_order | A report card’s record (RPTGRID, RPTPROPS, RPTREGS, RPTRST, RPTSCHED, RPTSOL) is a SET of mnemonics, not a sequence: permuting it leaves the target’s UNSMRY, UNRST and print file byte-identical (probed on 2025.10 and 2026.04), so both sides sort the record. The sort key is the token’s VALUE, not its lexeme — sorting raw lexemes ranked a quote ahead of a letter, so BASIC=2 'PBPD' and 'BASIC=2' 'PBPD' sorted opposite ways and a normalizer written to make order carry nothing turned QUOTING into order. |
| 7 | _normalize_mixing_density_default | A TLMIXPAR density slot equal to its default (the viscosity slot) is dropped. |
| 9 | _normalize_explicit_default_slots | An explicit record value equal to the simulator default for that slot states the same deck as a defaulted slot (curated (keyword, slot) table: WELLDIMS, WELSPECS, COMPDAT, WCONPROD, MULTREGT slots 4 and 5, MULTREGP, FOAMOPTS, MISCIBLE, PLYMAX, ENDSCALE); alternate spellings of one slot value alias (WELSPECS slot 6 WATER → WAT). |
| 9a | _normalize_region_set_defaults | An explicit region set equal to the set its defaulted item selects states the same deck as the defaulted item, the default read by the deck’s own context: MULTIREG, ADDREG, EQUALREG and COPYREG item 4 default to F (FLUXNUM), or M (MULTNUM) when the last GRIDOPTS states a positive NRMULT (opm-common FieldProps.cpp default_region_keyword); MULTREGT item 6 defaults to the set of the MULTREGT record before it, M for the first (MULTREGTScanner.cpp addKeyword), as the importer loads them. |
| 9b | _drop_inert_all_default_records | A sole all-default record on the single-record RUNSPEC dimensioning keywords (TABDIMS, EQLDIMS, ENDSCALE) states exactly the bare keyword: the record only sizes allocations. Runs after the explicit-default slot pass so a fully default-stated ENDSCALE record reconciles with the bare canonical emission. |
| 10 | _normalize_operate_parameter_slots | OPERATE parameter slots are positional; operations that do not consume alpha/beta ignore whatever the deck states there, so an explicit ignored value equals a defaulted slot. |
| 11 | _merge_adjacent_operate_blocks | Adjacent same-section OPERATE blocks state one ordered row list. |
| 12 | _fold_fault_multiplier_blocks | MULTFLT blocks fold to per-fault effective factors: within one section the last record per fault wins, and the EDIT winner multiplies the GRID winner. Only name+factor records fold; any wider record leaves the deck untouched. |
| 13 | _normalize_user_defined_expression_blocks | UDQ and ACTIONX expression records canonicalize through the shared lexical canonicalizer: parenthesization, numeral spellings, and name quoting compare equal, and UNITS placement inside a UDQ block attaches to the named definition, so a spelling difference does not hide a real one; multi-record ACTIONX conditions joined by trailing AND/OR combinators state one condition expression. This comparator tolerates those differences; it does not claim they are nothing. The simulator stores a DEFINE’s expression tokens in the restart’s ZUDL array and a rule’s image in IACT/ZLACT, so the words and the record order are deck state at the output contract even where they are not at the algebra — which is why the importer carries them (see the importer’s rule-text section) rather than leaving the render free to respell them. The image is counted in RECORDS, so a rule body’s BOX/ENDBOX scope cards are deck state on the same ground: no pass here erases them, and the importer carries them so the render can neither drop a stated box nor restate one per card. |
| 13b | _expand_schedule_well_wildcards | A well-name wildcard row (* matches any character run, ? exactly one; a leading * with more characters is a *LIST well-list reference, not a pattern) in a SCHEDULE well keyword (COMPDAT, WCONHIST, WCONINJE, WCONINJH, WCONPROD, WECON, WEFAC, WELOPEN, WELTARG, WTEST) states one explicit row per well the deck has declared via WELSPECS up to that position whose name matches, in declaration order — the deck position is the binding point, mirroring the importer’s resolution exactly. A pattern matching no declared well (and any well-list reference) leaves the row untouched. |
| 14 | _merge_adjacent_schedule_blocks | Adjacent same-name SCHEDULE blocks of the well/time keywords (DATES, TSTEP, WCONHIST, WCONINJE, WCONINJH, WCONPROD, WECON, WEFAC, WELOPEN, WELTARG, WTEST) state one record list; consecutive TSTEP blocks concatenate into one timestep stream. |
| 15 | _coalesce_group_control_blocks | One SCHEDULE block with N per-group control records (GCONPROD, GCONINJE, GEFAC, GPMAINT) states the same schedule as N adjacent single-record blocks of the same keyword. |
| 17 | _normalize_schedule_step_order | SCHEDULE keyword order inside one report step is non-semantic; runs between the step barriers (TSTEP, DATES, END) sort by keyword. |
| 18 | region materialization (_apply_equals_records, _apply_copy_records, _expanded_region_keyword, _region_array_keyword) | When DIMENS is known, EQUALS/COPY records that assign the materialized region arrays (FLUXNUM, MULTNUM, OPERNUM in GRID; PVTNUM, EQLNUM in REGIONS) are applied cell-by-cell and replaced with the expanded per-cell array at the first edit’s position, so box-assigned and literal region arrays compare equal. Explicit region-array keywords expand their repeat runs into one flat record. |
Pairwise passes (after the pipeline, before comparison)
Section titled “Pairwise passes (after the pipeline, before comparison)”| Pass | Rule |
|---|---|
_drop_unmatched_default_reports | Renderer-default report directives absent from the other deck are dropped: a deck that never mentions RPTSCHED (PRES SGAS RS WELLS) or RPTRST (BASIC=1) relies on exactly those defaults. |
_drop_unpaired_title | A one-sided TITLE compares as absent: TITLE is descriptive metadata with no simulation state, and the renderer always states one (falling back to the model name). Two explicit titles still compare record-for-record. |
_drop_all_active_actnum | ACTNUM blocks whose cells are all active are dropped: that is the default. |
Record- and token-level rules (inside _compare_records)
Section titled “Record- and token-level rules (inside _compare_records)”| Rule | Effect |
|---|---|
_without_trailing_empty_records | Trailing empty list-terminator records do not count toward the record comparison. |
_without_trailing_defaults | Repeats expand and trailing defaulted slots strip from each record; an all-default record is a slot count and keeps its length. |
| repeat-run expansion | Repeat runs are lexical, not semantic: 600*x 600*x and 1200*x state the same cells, so records containing repeats compare as expanded value streams. |
| saturation-table interpolation | Defaulted interior cells of SWOF, SGOF, SLGOF, SWFN, SGFN, SOF2, SOF3 mean linear interpolation, so a defaulted cell matches any value on the other side. |
TolerancePolicy | Numeric tokens compare with abs_tol=1e-10, rel_tol=1e-8, NaN equal to NaN; every other token compares literally. |
Change control
Section titled “Change control”A new normalization pass is a new claim that some textual difference carries no deck semantics. Adding one requires a row in this ledger stating the rule, and the pass itself must keep the comparator’s fail-closed posture: when a block does not match the pass’s supported shape exactly, the pass leaves the deck unchanged rather than guessing.
PetroScript is licensed under the Apache License 2.0. © 2026 Blauweiss EDV LLC. PetroScript by Blauweiss AI.For AI agents: llms.txt