Skip to content

Initialization

Initialization defines the starting pressure and saturation state before the first timestep. A model’s initial is one of the ps.initial kinds: an Equilibrium (the datum, the contacts and the depth tables), an ExplicitState (the state cell by cell) or a Restart. One Equilibrium states every equilibration region; {region: Equilibrium} states one per region, numbered from 1, and region 1’s holds the statements of the whole model (the threshold pressures, the whole-grid initial water saturation and the vaporized-oil controls). The public API describes reservoir meaning; SOLUTION/RUNSPEC keyword names remain compiler internals.

ps.initial.Equilibrium(...) is a standard equilibrium-contact initial state. It requires an explicit datum depth and datum pressure, and states the contacts the model has; PetroScript does not fabricate OPM defaults for those values.

import petroscript as ps
state = ps.initial.Equilibrium(
datum_depth=8400.0,
datum_pressure=4800.0,
oil_water_contact=8450.0,
gas_oil_contact=8300.0, # above the top (8325 ft): no gas cap
)
# ps.Model(..., initial=state)

A model with gas and oil states its gas-oil contact even when it has no gas cap: above the reservoir’s top. OPM Flow places free gas where a depth is at or above the contact: each cell’s centre when EQUIL’s accuracy (item 9) is 0, and depths sampled within each cell with the default (-5) or any other non-zero accuracy. So at accuracy 0 a contact at a cell’s centre already puts gas there, and with sampling a contact inside the top cells can put gas in them though it lies below their centres. A contact above the top starts with no free gas at any accuracy.

A gas-water model — GASWAT, or CO2 storage into a saline aquifer — has no oil leg for a contact to sit against, and EQUIL carries one contact slot that OPM Flow reads as the gas-water contact when no oil phase is active. Say so:

storage_state = ps.initial.Equilibrium(
datum_depth=2400.0,
datum_pressure=250.0,
gas_water_contact=2450.0,
)

gas_water_contact and oil_water_contact are two contacts that fill the one EQUIL slot, each stored as itself, so state the one the model actually has. Stating both is a contradiction and is refused. A gas-water contact placed below the grid base initializes every cell in the gas leg — the aquifer disappears — and warns under physics.contacts.gas_water_below_grid, which acknowledges like any other physics warning when the gas-filled tank is the point.

The depth tables and the other equilibrium statements are its fields too:

state = ps.initial.Equilibrium(
datum_depth=8400.0,
datum_pressure=4800.0,
oil_water_contact=8450.0,
gas_oil_contact=8300.0,
solution_gor_by_depth=ps.initial.RsVsDepth(
depth=[8400.0, 8500.0], solution_gor=[700.0, 650.0]
),
bubble_point_by_depth=ps.initial.PbVsDepth(
depth=[8400.0, 8500.0], bubble_point_pressure=[4800.0, 4700.0]
),
report_datum_depth=8400.0,
threshold_pressures=(
ps.initial.ThresholdPressure(from_region=1, to_region=2, pressure=25.0),
),
)

report_datum_depth= states the depth pressures are reported at, and its shape is the card: one depth writes DATUM (the whole model), one depth per equilibration region in region order writes DATUMR, and one named fluid-in-place set’s depths write DATUMRX. datum_depth stays EQUIL’s own datum.

whole_model = state.replace(report_datum_depth=8400.0)
per_region = state.replace(report_datum_depth=(2510.0, 2520.0))
per_named_set = state.replace(report_datum_depth={"ABC": (2510.0, 2520.0)})

ps.initial.Restart(case=..., step=...) starts from a previous run’s restart file: step= is the report-step number (the deck’s RSNUM, RESTART item 2) or a date resolved against the schedule to the same item.

ps.initial.RsVsDepth(depth=..., solution_gor=...) is the depth-indexed solution-GOR table used by equilibrium initialization, held by column (depth, solution_gor). The table is semantic input; the compiler lowers it internally to the appropriate Flow deck rows. ps.initial.PbVsDepth, PdVsDepth and RvwVsDepth hold the bubble-point pressure, the dew-point pressure and the vaporized water-gas ratio against depth the same way.

ps.initial.RvVsDepth(depth=..., vaporized_oil_gas_ratio=...) is the depth-indexed vaporized-oil ratio table for equilibrium initialization. ps.pvt.VaporizationControls is VAPPARS (its oil_vaporization_propensity and oil_density_propensity), stated on the equilibrium as vaporization_controls=.

ps.initial.TemperatureVsDepth(depth=..., temperature=...) states the initial reservoir temperature against depth for one equilibration region, and the equilibrium’s temperature_by_depth= attaches it.

Every depth table (RsVsDepth, RvVsDepth, PbVsDepth, PdVsDepth, RvwVsDepth, TemperatureVsDepth) is held by column, depth and the value column named for the quantity: solution_gor, vaporized_oil_gas_ratio, bubble_point_pressure, dew_point_pressure, vaporized_water_gas_ratio, temperature. from_frame reads those columns by name from a polars or pandas frame, or a mapping of column name to values:

profile = ps.initial.TemperatureVsDepth.from_frame(
{"depth": [2500.0, 2600.0], "temperature": [80.0, 83.0]}
)

A frame that lacks a column is refused, naming the table and the missing column.

Use it only where nothing else already holds the temperature. A thermal model carries its own (physics.thermal.initial_temperature) and reads it for its own physics; so does a CO2/H2 storage package (physics.storage.temperature). What is left is the run that states a profile and activates neither — opm-tests/diffusion/BO_DIFFUSE_CASE1 declares OIL, GAS and DIFFUSE, states the profile, and the target runs it — and there the temperature is an initial condition and nothing more.

A constant temperature with no thermal model and no storage package behind it has no such home and stays refused by name.

ps.initial.ExplicitState(...) sets pressure and optional saturation/solution fields directly instead of deriving pressure from contacts:

state = ps.initial.ExplicitState(
pressure=3000.0,
water_saturation=0.2,
gas_saturation=0.0,
oil_saturation=0.8,
)

Direct ratio fields such as solution_gor, solution_gas_water_ratio, vaporized_oil_gas_ratio, and vaporized_water_gas_ratio require the matching active phase/formulation semantics. Invalid combinations fail closed with stable validation issue codes.

OILAPI in the SOLUTION section is intentionally not exposed through ps.initial: OPM Flow (2025.10 and 2026.04) rejects that keyword (‘OILAPI: keyword not supported’). A deck’s PROPS/GRAVITY, which states an oil API gravity, loads as the fluid system’s surface densities.

Under FIELD, depths are feet, pressure is psi, and gas/oil ratios are in the model’s deck-native ratio units. Depth tables hold one value per depth, column by column. Initialization property tensors use the same strict scalar-or (nx, ny, nz) contract as grid and rock cell properties.

Common mistakes are missing initial-state data, inconsistent contact depths, non-positive datum pressure, non-finite scalar values, numeric strings, depth tables with non-monotonic depth rows, and direct ratio fields supplied without matching phase semantics. Use ps.testing.assert_issue(..., code=...) for agent-friendly red/green diagnostics.

Current status: emitted now for the implemented equilibrium and direct paths.

  • EQUIL is emitted from ps.initial.Equilibrium(...).
  • RSVD, RVVD, PBVD, PDVD, and RVWVD are emitted from semantic depth tables.
  • RTEMPVD is emitted from ps.initial.TemperatureVsDepth(...) when neither a thermal model nor a storage package carries the temperature instead.
  • DATUM, DATUMR, and DATUMRX are emitted from report_datum_depth.
  • EQLOPTS THPRES and THPRES are emitted from typed threshold-pressure semantics.
  • PRESSURE, SWAT, SGAS, SOIL, RS, RSW, RV, and RVW are emitted from ps.initial.ExplicitState(...) when the corresponding semantic field is supplied.

Restart-based initialization is ps.initial.Restart; raw keyword-shaped initialization helpers are not public PetroScript APIs.

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