Skip to content

Storage, brine, and salt chemistry

PetroScript exposes CO2/H2 storage, brine salinity, and salt-precipitation behavior through semantic physics objects. Users describe reservoir intent; the compiler owns the OPM Flow keyword spelling, section placement, record order, and slash/default-token details.

For CO2 storage brine physics, choose the salting-out activity model by its word on the storage package, a ps.storage.Package in the model’s physics=:

import petroscript as ps
storage = ps.storage.Package(
kind="co2",
components=["co2"],
gas_dissolves_in_water=True,
activity_model="duan_sun_direct",
)

The words ("rumpf_spycher_pruess_iterative", "duan_sun_iterative", "duan_sun_direct") are the public surface. Do not pass integer model codes or build a card object directly. H2 storage intentionally rejects this field because it is CO2 activity-model intent. Model validation also requires a brine package before the activity model can lower, so the salting-out model stays inside the Flow-evidenced CO2-brine context.

A uniform salt state is a storage-model concept: it describes the whole field at once for the CO2-brine model that CO2STORE/H2STORE activates, and it belongs to the storage package. The brine package owns the depth profile and the per-cell salinity field instead.

storage = ps.storage.Package(
kind="co2",
components=["co2"],
salinity=ps.brine.UniformSalinity(basis="molality", value=1.0),
)
brine = ps.brine.Package(
pvt=ps.brine.WaterPVT(
reference_pressure=3000.0,
surface_reference_salinity=0.0,
rows=[
(0.0, 1.0, 3.0e-6, 0.5, 0.0),
(10.0, 0.8, 3.0e-6, 0.6, 0.0),
],
),
)

State basis="liquid_mole_fraction" instead of "molality" when the model should be initialized by liquid-phase salt mole fraction; the basis selects the card the storage package renders.

surface_reference_salinity is explicit. If it is omitted, PetroScript does not invent a surface-salinity value; the compiler leaves that item absent. A uniform salt state is read by CO2 and H2 storage alike: the H2-brine PVT properties depend on the salinity too (manual 5.3.61), and three salinities give three different answers in an H2 run as they do in a CO2 one. The salt state is a ps.brine.UniformSalinity with its basis stated; bare floats are rejected because they would otherwise be ambiguous and unlowered.

ps.brine.SalinityVsDepth(rows=...) states the initial salinity profile as (depth, salinity) pairs in that order, or as records naming depth and salinity; a record that lacks a column is refused, naming the column.

brine = ps.brine.Package(
initial_salinity=ps.brine.SalinityVsDepth(
rows=[{"depth": 2500.0, "salinity": 30000.0}, {"depth": 2600.0, "salinity": 35000.0}]
),
)

ps.pvt.Composition(name=..., mole_fraction={component: fraction, ...}) is one WELLSTRE row: the mapping’s keys are the components, so a species is named once by construction; the package reads them as its component words, in lower case or in capitals (“co2” or “CO2”). The package declares it in its compositions=, and an injector injects it by its name, ps.wells.InjectedStream(well=..., composition=...). A component the composition leaves unstated is 0, and WELLSTRE writes every component of the package; fractions that do not sum to one within 1e-6 are refused as the composition is built. The initial composition against depth is ps.initial.CompositionVsDepth(depth=[...], mole_fraction={component: [...]}), each depth summing to one.

stream = ps.pvt.Composition(name="C02STREAM", mole_fraction={"co2": 1.0, "h2o": 0.0})
storage = ps.storage.Package(kind="co2", components=["co2", "h2o"], compositions=[stream])

A storage run that declares OIL, GAS and DISGAS states no formulation card and no oil PVT family: manuals 5.3.18 and 5.3.61 say the normal PVT keywords are not required once the model is activated. There is then nothing for the solution-gas coupling to be read off — no table type, no BLACKOIL declaration — so the storage package carries the declaration itself, beside gas_dissolves_in_water, which is the same shape of statement:

storage = ps.storage.Package(
kind="h2",
components=["h2"],
simulator_supplied_phases=["gas", "oil"],
dissolved_gas=True,
)

The field is only a package declaration where the package supplies both the gas and the oil phase, which is the manuals’ option (2). A run that states its own oil PVT family declares the coupling through that family as usual and leaves this False; asking for both is refused rather than silently preferred. Importing a deck of this shape restates DISGAS and adds no BLACKOIL card the deck never stated.

Convective dissolution, one constant per PVT region

Section titled “Convective dissolution, one constant per PVT region”

OPM Flow reads one convective-dissolution record (DRSDTCON) per PVT region, in region order. ps.storage.ConvectiveDissolution(coefficient=) states it in a ps.Event of a model with a storage package: one number for every region, or {region: number} naming each of the model’s PVT regions; in the start’s Event it is written at the schedule’s head, in a later one at its date. A map naming other regions is refused (storage.drsdtcon.pvt_regions), and so is the statement in a model with no storage package (storage.controls.package_missing), as ps.load refuses DRSDTCON beside neither CO2STORE nor H2STORE:

dissolution = ps.storage.ConvectiveDissolution(coefficient={1: 0.04, 2: -1.0})
# model.at(model.start, dissolution)

A later region may ask for something the manual’s “a real positive value” does not cover. Zero, in the manual’s own words, means dissolution does not occur there. A negative constant means the region wants no limit at all — the instantaneous dissolution described for a run that states the keyword nowhere. That reading was measured off the reference run committed beside co2store/CO2STORE_DRSDTCON.DATA, whose region 1 (constant 0.04) climbs to saturation over 27 report steps while its region 2 (constant -1) is saturated at the first.

Importing a storage deck of this shape also restates its declared PVT region count. A storage run states no PVT family, so nothing counts the regions but the region map itself, and both the TABDIMS cross-check and the model’s PVT-region gate read the map rather than families no table produced. That relaxation is scoped to the PVT map: a saturation region map is still held to the saturation families the deck states.

Salt precipitation capillary-pressure factor

Section titled “Salt precipitation capillary-pressure factor”

Salt precipitation supports a capillary-pressure multiplier as a function of the porosity factor:

brine = ps.brine.Package(
solubility=ps.brine.SaltSolubility(rows=[(0.5, 135.0)]),
initial_precipitated_salt=0.0,
gas_water_vaporization=ps.brine.VaporizationBySalt(
rows=[(3000.0, 0.0, 0.01), (3000.0, 1.0, 0.005)]
),
capillary_pressure_factor=ps.brine.CapillaryPressureFactor(
rows=[(0.0, 2.0), (1.0, 1.0)]
),
)

Salt precipitation is the brine package’s: its solubility limit, its initial precipitated salt, its salt-dependent gas-water vaporization and its capillary-pressure factor. Each table validates finite, positive, strictly ordered semantic values. The public API is the physical table, not a raw keyword record. PCFACT requires at least two rows plus explicit salt gas-water vaporization and vaporized-water gas-water formulation; PetroScript does not auto-enable those prerequisites from the capillary-pressure factor alone.

These stay outside the public API on purpose:

  • Biofilm activation requires a broader biology/BIOFPARA semantic surface; the data-less storage flag is not exposed.
  • Ezrokhi aqueous density/viscosity tables require CNAMES/COMPS-family parser mechanics, which remain outside PetroScript’s public semantic API.
  • Polymer salt/temperature deepenings remain non-public because current Flow docs and evidence treat those columns as ignored for the implemented polymer path.
  • Thermal CO2/brine mixing belongs to the thermal domain, not this storage/brine salt surface.

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