Skip to content

Describe the fluids

The fluid is the model’s fluid=: one object that says which phases flow and gives each phase’s PVT table and surface density. Its class is the phase set:

The reservoir holdsClassTables
Oil and water (a dead oil)ps.fluid.OilWateroil=ps.pvt.DeadOil, water=ps.pvt.Water
Gas and waterps.fluid.GasWatergas=ps.pvt.DryGas, water=ps.pvt.Water
Oil, gas and water, or oil and gas (a live oil is always here)ps.fluid.BlackOiloil=, gas=, water= as they flow
CO2 or H2 in brineps.fluid.BlackOil plus a ps.storage.Package in physics=storage, brine and salt
Components (equation of state)ps.fluid.Compositionalloads, but no target runs it yet (below)

A table is named columns, one value per row, each a list, a tuple or a NumPy array. Every number is in the model’s units: here METRIC (barsa, rm3/sm3, sm3/sm3, cP, kg/m3). In FIELD a gas-oil ratio is Mscf/stb and a gas formation volume factor rb/Mscf (units).

The examples share one small model around the fluid:

import petroscript as ps
water = ps.pvt.Water(reference_pressure=200.0, fvf=1.01, compressibility=4.5e-5,
viscosity=0.4, viscosibility=0.0)
oil_water_curves = ps.relperm.OilWater(sw=[0.2, 0.8], krw=[0.0, 1.0], krow=[1.0, 0.0],
pcow=[0.0, 0.0])
gas_oil_curves = ps.relperm.GasOil(sg=[0.0, 0.8], krg=[0.0, 1.0], krog=[1.0, 0.0],
pcog=[0.0, 0.0])
def model(fluid, initial, relperm):
"""A 2 x 2 x 2 box with one producer, around the fluid being shown."""
return ps.Model(
name="FLUID", units="metric", start="2025-01-01",
grid=ps.grid.Cartesian(shape=(2, 2, 2), dx=100.0, dy=100.0, dz=10.0, top=2000.0),
rock=ps.Rock(porosity=0.2, permeability=100.0, vertical_ratio=0.1,
compressibility=5.0e-5, reference_pressure=200.0),
fluid=fluid, initial=initial, relperm=relperm,
wells=[ps.Well(name="P", i=1, j=1)],
schedule=ps.Schedule(end="2025-03-01", report="monthly", events=[
ps.Event("2025-01-01", ps.Perforate(well="P", k=(1, 2), diameter=0.2),
ps.Produce(well="P", oil_rate=100.0, bhp_limit=50.0))]),
)

A dead oil has no gas in it: its formation volume factor and viscosity against pressure.

dead = ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[50.0, 200.0, 300.0], # barsa
oil_fvf=[1.12, 1.10, 1.09], # rm3/sm3
oil_viscosity=[1.9, 2.0, 2.1]), # cP
water=water,
oil_surface_density=850.0, water_surface_density=1020.0, # kg/m3
)
m = model(dead, ps.initial.Equilibrium(datum_depth=2000.0, datum_pressure=200.0,
oil_water_contact=2030.0), [oil_water_curves])

A live oil holds dissolved gas. Its table is in long form: the rows that share a solution_gor are one family, the first at the bubble point and the rest above it, and the last family needs rows above its bubble point. A live oil flows beside a gas table, so the fluid is a ps.fluid.BlackOil, and its initial state needs two more facts: the gas-oil contact, and how much gas the oil holds at each depth (ps.initial.RsVsDepth, or the bubble point by depth, ps.initial.PbVsDepth).

live = ps.fluid.BlackOil(
oil=ps.pvt.LiveOil(solution_gor=[50.0, 100.0, 100.0], # sm3/sm3
pressure=[100.0, 200.0, 300.0], # barsa: 300 is above the
oil_fvf=[1.20, 1.30, 1.28], # bubble point of the last
oil_viscosity=[1.0, 0.8, 0.85]), # family (100 sm3/sm3)
gas=ps.pvt.DryGas(pressure=[50.0, 400.0], gas_fvf=[0.02, 0.003], # rm3/sm3
gas_viscosity=[0.013, 0.025]),
water=water,
oil_surface_density=850.0, gas_surface_density=0.9, water_surface_density=1020.0,
)
initial = ps.initial.Equilibrium(
datum_depth=2005.0, datum_pressure=200.0, oil_water_contact=2020.0,
gas_oil_contact=1990.0, # above the top: no gas cap
solution_gor_by_depth=ps.initial.RsVsDepth(depth=[1990.0, 2020.0],
solution_gor=[100.0, 100.0]),
)
m = model(live, initial, [oil_water_curves, gas_oil_curves])

What goes wrong, with the message you get:

  • Oil and water only, stated as ps.fluid.BlackOil: InputError: Model: the model's phases are oil and water, which is the ps.fluid.OilWater system.
  • An oil or gas surface density left out is refused, by phase ([fluids.density.oil.missing], [fluids.density.gas.missing]); a water density left out is a warning (fluids.density.water.defaulted): Flow takes fresh water’s 999.014 kg/m3.
  • A live oil inside ps.fluid.OilWater: [phases.dissolved_gas.requires_gas] Dissolved gas requires the gas phase. A live oil is a ps.fluid.BlackOil with a gas table.
  • The last live-oil family has only its bubble-point row: [physics.pvt.undersaturated_missing].
  • No gas-oil contact where gas and oil both flow: [initialization.equil.required_value] EQUIL gas_oil_contact is required.
  • No depth table, the datum away from the gas-oil contact: [physics.initial.datum_off_gas_oil_contact].
try:
model(live, initial.replace(solution_gor_by_depth=None), [oil_water_curves, gas_oil_curves])
except ps.ModelError as refused:
print(refused) # [physics.initial.datum_off_gas_oil_contact] ... fix: State the depth table

Gas that carries oil (wet gas) in a deck you loaded

Section titled “Gas that carries oil (wet gas) in a deck you loaded”

A wet gas (ps.pvt.WetGas, Flow’s PVTG) needs the run’s vaporized-oil switch and, like a live oil, a depth table in the initial state. On a loaded deck all three change together, in one m.set:

spe1 = ps.load("opm-tests/spe1/SPE1CASE1.DATA") # FIELD: psia, stb/Mscf, rb/Mscf
wet = ps.pvt.WetGas(pressure=[14.7, 14.7, 4014.7, 4014.7],
vaporized_oil_gas_ratio=[0.0001, 0.0, 0.0002, 0.0],
gas_fvf=[166.666, 166.0, 0.6, 0.59],
gas_viscosity=[0.008, 0.008, 0.03, 0.03])
state = spe1.get("initial/1").replace(
vaporized_oil_by_depth=ps.initial.RvVsDepth(depth=[8300.0, 8450.0],
vaporized_oil_gas_ratio=[0.0002, 0.0002]),
use_vaporized_oil_table=True,
)
spe1 = spe1.set({
"fluid/1/gas": wet,
"dimensions/phases/vaporized_oil": True, # VAPOIL
"initial/1": state,
})

Setting the table alone is refused with [fluids.gas.pvtg_without_vapoil] A ps.pvt.WetGas table requires the vaporized-oil phase; switching the phase on without the depth table, with [physics.initial.datum_off_gas_oil_contact].

A gas field over water is a ps.fluid.GasWater: a ps.pvt.DryGas table, the water, their surface densities and, for PetroScript’s checks, the table’s temperature. The quickstart builds one.

ps.fluid.Compositional describes a fluid by its components (ps.pvt.Components), an equation of state and its binary interaction coefficients. ps.load reads a compositional deck, you can inspect and edit it, and m.to_python() writes it back as Python. No target compiles one yet: ps.compile raises ps.CompileError [target.unsupported]. A model whose fluid is compositional also needs the reservoir temperature (temperature=). A compositional fluid is also how you describe the stream for a NeqSim lift table.

ps.help(ps.fluid.BlackOil), ps.help(ps.pvt.LiveOil) and the fluids reference have every field.

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