Skip to content

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.

  1. _inline_includes on each raw text;
  2. parse_normalized_deck on each side;
  3. _normalize_deck_semantics (the pipeline below) on each side;
  4. the pairwise passes _drop_unmatched_default_reports and _drop_unpaired_title, then _drop_all_active_actnum on each side;
  5. keyword-by-keyword, record-by-record, token-by-token comparison under TolerancePolicy.
PassRule
parse_normalized_deckSplits 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.

#PassRule
2_normalize_phase_declaration_orderRUNSPEC 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_orderScalar 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_orderPROPS keyword order is non-semantic between edit barriers (EQUALS, COPY, ADD, MULTIPLY, OPERATE); each barrier-free run sorts by keyword.
4b_normalize_regions_keyword_orderREGIONS 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_orderSUMMARY vector order is output-column order, not deck semantics; each SUMMARY run sorts by keyword.
2b_normalize_boundary_connection_defaultsDefaulted 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_orderRUNSPEC 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_orderSOLUTION keyword order is non-semantic between edit barriers (EQUALS, COPY, ADD, MULTIPLY, OPERATE, BOX, ENDBOX).
6b_normalize_report_mnemonic_orderA 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_defaultA TLMIXPAR density slot equal to its default (the viscosity slot) is dropped.
9_normalize_explicit_default_slotsAn 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_defaultsAn 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_recordsA 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_slotsOPERATE 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_blocksAdjacent same-section OPERATE blocks state one ordered row list.
12_fold_fault_multiplier_blocksMULTFLT 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_blocksUDQ 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_wildcardsA 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_blocksAdjacent 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_blocksOne 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_orderSCHEDULE keyword order inside one report step is non-semantic; runs between the step barriers (TSTEP, DATES, END) sort by keyword.
18region 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)”
PassRule
_drop_unmatched_default_reportsRenderer-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_titleA 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_actnumACTNUM 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)”
RuleEffect
_without_trailing_empty_recordsTrailing empty list-terminator records do not count toward the record comparison.
_without_trailing_defaultsRepeats expand and trailing defaulted slots strip from each record; an all-default record is a slot count and keeps its length.
repeat-run expansionRepeat 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 interpolationDefaulted interior cells of SWOF, SGOF, SLGOF, SWFN, SGFN, SOF2, SOF3 mean linear interpolation, so a defaulted cell matches any value on the other side.
TolerancePolicyNumeric tokens compare with abs_tol=1e-10, rel_tol=1e-8, NaN equal to NaN; every other token compares literally.

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