Skip to content

ps

ps.Adjust(*, well, oil_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, water_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, gas_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, liquid_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, reservoir_volume: Union[ScalarPropertyInput, Expression, NoneType] = None, bhp: Union[ScalarPropertyInput, Expression, NoneType] = None, thp: Union[ScalarPropertyInput, Expression, NoneType] = None, rate: Union[ScalarPropertyInput, Expression, NoneType] = None, artificial_lift_quantity: Union[ScalarPropertyInput, Expression, NoneType] = None, lift_table: ProductionTable | InjectionTable | None = None) -> None

Change one target of the selected wells and keep their control (WELTARG).

ps.Adjust(well="P1", oil_rate=1200.0) moves P1’s oil-rate target to 1200 from this date; the rest of its control stands. Exactly one quantity is stated, in the words the well’s control states it: oil_rate, water_rate, gas_rate, liquid_rate, reservoir_volume, bhp, thp, or an injector’s rate, in the model’s units (a surface rate, a reservoir volume rate or a pressure). Or it changes a lift setting, as ps.Produce spells it: a producer’s artificial_lift_quantity (WELTARG LIFT, a bare number its lift table reads), or the lift_table the well flows through (WELTARG VFP, a production table for a producer, an injection table for an injector). It is a dated change, held at schedule/<date>/<well>/adjust/<n>; m.control(well, at=) reads the control it leaves.

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field m = m.at(“2010-03-01”, ps.Adjust(well=“M-1”, gas_rate=450_000.0)) # sm3/day: metric m.find(“schedule/2010-03-01//”) [‘schedule/2010-03-01/M-1/adjust/1’] m.control(“M-1”, at=“2010-03-01”).bhp_limit # barsa, kept 50.0

ps.Box(i: tuple[int, int] | None = None, j: tuple[int, int] | None = None, k: tuple[int, int] | None = None) -> None

A box of cells: the deck’s 1-based, inclusive ranges along i, j and k.

i, j and k are each a range (start, end) of cell indices along their axis, counted from 1 with both ends in the box; an axis left out spans the grid. ps.Box(i=(1, 3), k=(2, 2)) is the cells 1 to 3 along i, every row along j, layer 2 along k. array[box.index] reads the box’s cells out of an (nx, ny, nz) array. A grid statement names its cells with one (cells=), and so do the aquifers and boundaries.

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # 10 x 10 x 5 cells top_two = m.array(“porosity”)[ps.Box(k=(1, 2)).index] # layers 1 and 2 top_two.shape (10, 10, 2)

ps.Capacity(*, wells: int | None = None, connections_per_well: int | None = None, groups: int | None = None, wells_per_group: int | None = None, well_lists_per_well: int | None = None, dynamic_well_lists: int | None = None, multisegment_wells: int | None = None, segments_per_well: int | None = None, branches_per_well: int | None = None, rows_per_relperm_table: int | None = None, pressures_per_pvt_table: int | None = None, ratios_per_pvt_table: int | None = None, fluid_in_place_regions: int | None = None, fluid_in_place_sets: int | None = None, independent_regions: int | None = None, flux_regions: int | None = None, operation_regions: int | None = None, polymer_mixing_regions: int | None = None, transmissibility_regions: int | None = None, rows_per_depth_table: int | None = None, rows_per_tracer_depth_table: int | None = None, rows_per_miscible_table: int | None = None, production_lift_tables: int | None = None, production_lift_rate_values: int | None = None, production_lift_thp_values: int | None = None, production_lift_water_fraction_values: int | None = None, production_lift_gas_fraction_values: int | None = None, production_lift_artificial_lift_values: int | None = None, injection_lift_tables: int | None = None, injection_lift_rate_values: int | None = None, injection_lift_thp_values: int | None = None, fault_segments: int | None = None, numerical_aquifer_cells: int | None = None, numerical_aquifer_connections: int | None = None, analytic_aquifers: int | None = None, connections_per_analytic_aquifer: int | None = None, influence_tables: int | None = None, rows_per_influence_table: int | None = None, network_nodes: int | None = None, network_branches: int | None = None, actions: int | None = None, lines_per_action: int | None = None, characters_per_action_line: int | None = None, conditions_per_action: int | None = None, functions_per_user_quantity: int | None = None, terms_per_user_quantity: int | None = None, field_user_quantities: int | None = None, group_user_quantities: int | None = None, well_user_quantities: int | None = None, connection_user_quantities: int | None = None, segment_user_quantities: int | None = None, region_user_quantities: int | None = None, aquifer_user_quantities: int | None = None, cell_user_quantities: int | None = None, user_arguments: int | None = None, distinct_user_arguments: int | None = None, user_tables: int | None = None, rows_per_user_table: int | None = None, interpolation_points_per_user_table: int | None = None, water_skin_pressure_tables: int | None = None, polymer_skin_pressure_tables: int | None = None, polymer_molecular_weight_tables: int | None = None, rows_per_polymer_viscosity_mixing_table: int | None = None, oil_tracers: int | None = None, water_tracers: int | None = None, gas_tracers: int | None = None, summary_vectors: int | None = None) -> None

The sizes a run allocates: ps.Capacity(wells=130, connections_per_well=36).

A model never has to state one: the target counts what the model needs (its wells and their connections, its groups, its tables’ rows) and writes that. An item a model states is written as stated, raised to the need where the model needs more; an item it leaves as None on a card it states others of is Flow’s default, raised to the model’s count alike (the wells, groups and connections; the tables; the equilibration regions; the multi-segment wells; the aquifers; the operating rules; the tracers; the network; the polymer mixing regions; the miscibility tables’ rows). A loaded deck keeps the sizes it states (m.capacity.wells == 130 for Norne). m.set("capacity/wells", 50) states one. Every item is a whole number, zero or more; a table’s rows, a multi-segment size and the summary vectors are one or more. Each item is a slot of a RUNSPEC size card (WELLDIMS, TABDIMS, REGDIMS, EQLDIMS, WSEGDIMS, VFPPDIMS, AQUDIMS, …), held at capacity/<item>.

ps.CompileError(issues: Iterable[Issue]) -> None

A refused compile, or a run refused before it starts: issues holds the error rows.

The rows are the model’s own, the target’s acceptance rows, its own refusal of a sound model such as an unknown target, or deck.check()’s before a run.

except ps.CompileError as error: catches a refused compile; error.issues lists each error row with its code, address and fix.

ps.DeckImportError(keyword: str, *, file: str, line: int, hint: str, reason: str | None = None) -> None

A deck keyword the importer cannot read onto the model.

except ps.DeckImportError as error: catches a deck ps.load refuses; error.at is the file and line, error.fix what to do.

address is the keyword, fix what to do, file and line where the deck states it (at); code the refusal’s kind: a file that cannot be read (deck.file.unreadable), text that is no deck (deck.text.malformed), an include where one file was asked for (deck.include.present), a card the deck never states (deck.card.missing), a keyword with no import (deck.keyword.unsupported), a record Flow itself refuses (deck.record.malformed), a record Flow runs that its import cannot read or has no statement for (deck.record.unsupported), a keyword stating fewer regions than another (deck.regions.short) and a card before its prerequisite (deck.order.prerequisite).

ps.Event(date: Any = None, *changes: Any) -> None

The changes the schedule states at one report time, in the order stated.

ps.Event("2021-01-01", ps.Perforate(...), ps.Produce(...)). The date is a report time of the schedule: a date or its text, which is how it prints. The text is 'YYYY-MM-DD', and 'YYYY-MM-DDTHH:MM' (‘2021-01-01T06:00’) where a step ends during a day, its seconds only where they are not zero; no other form is read. Midnight is the day. The deck writes the changes’ cards after the time card that reaches the date: the date’s DATES record, or the TSTEP step that ends there in a ps.Every run; an event at the start has no time card before it, so its cards come before the first one. The event is held at schedule/<date> and each change below it (schedule/<date>/<well>/control). m.at(date, *changes) adds changes to a model’s event.

import petroscript as ps event = ps.Event(“2009-06-01”, … ps.Perforate(well=“M-1”, k=(1, 2), diameter=0.2), # m … ps.Produce(well=“M-1”, gas_rate=500_000.0, # sm3/day … bhp_limit=50.0)) # barsa str(event.date) ‘2009-06-01’ [type(change).name for change in event.changes] [‘Perforate’, ‘Produce’]

ps.Every(*steps: Any) -> None

A run of report steps of the stated lengths, in days: ps.Every(31, 28, 31).

Each length ends one report step after the time the schedule has reached; the deck writes the run as one TSTEP card, cut where an event is dated at the end of a step inside it. The lengths are days in every unit system (a “lab” deck’s TSTEP states them in hours). It is one of ps.Schedule(report=[...])’s items, held at schedule/report.

ps.Fault(*, name: str, segments) -> None

A named fault: the cell faces it runs along (FAULTS).

ps.Fault(name="F1", segments=[((1, 1), (1, 4), (1, 3), "plus_i")]): each segment is a row (i, j, k, face), each of i, j and k a 1-based, inclusive (first, last) range of cells, as ps.Box takes them, and face the cell face the fault runs along ("plus_i" is the cells’ positive i-face, "minus_i" the negative one; "plus_j", "minus_j", "plus_k", "minus_k" alike). A model’s faults are held at faults/<n>, in the order stated; ps.grid.FaultMultiplier scales a fault’s transmissibility (MULTFLT).

ps.Group(*, name: str, parent: Group | str | None = None) -> None

A group of wells in the field’s group tree (GRUPTREE), under an optional parent group.

ps.Group(name="G1", parent=north) states group G1 under north, itself a ps.Group; a well joins it with ps.Well(group=), and a dated ps.Group in an Event moves it to another parent. The model’s groups are held at groups/<name>.

Groups organize wells for the group controls (ps.groups.ProductionTarget, ps.groups.InjectionTarget) and for summary reporting; the production network over them is the model’s ps.groups.Network.

ps.History(*, well, oil_rate: Optional[ScalarPropertyInput] = None, water_rate: Optional[ScalarPropertyInput] = None, gas_rate: Optional[ScalarPropertyInput] = None, control: Optional[Literal['oil_rate', 'water_rate', 'gas_rate', 'liquid_rate', 'reservoir_volume', 'bottom_hole_pressure']] = None, thp: Optional[ScalarPropertyInput] = None, bhp_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, lift_table: ProductionTable | None = None, status: Literal['open', 'shut', 'stop'] = 'open') -> None

Hold the selected producers to observed rates, as a history match does (WCONHIST).

ps.History(well="P1", oil_rate=1200.0, water_rate=300.0, control="oil_rate") holds P1 to its observed oil rate from this date and records the water rate observed beside it.

Every rate is an observation, so more than one is normal; control names the one the simulator holds the well to (control="gas_rate"), and it is required when more than one rate is given. A single rate is the control. Each rate is a surface volume rate in the model’s units, thp and bhp_limit pressures. It is a dated change, held at schedule/<date>/<well>/control as a producer’s control is; ps.History.from_frame builds the dated rows of a whole history from a frame.

lift_table is the vertical-lift table the observed record binds to the well, WCONHIST 12.3.252 item 7. It is not a control: measured on both pinned releases, unbinding the table drives the reported tubing-head pressure to exactly zero (WTHP:PROD1 82.3713 -> 0.0000 barsa) and moves no bottom-hole pressure at all, which is manual 12.3.252 item 9’s “only used for comparing” read back out of the simulator. So the binding rides the observed record rather than the reservoir-control restatement a lift binding carries — a history well has no control row to restate, and the measurement says there is nothing to restate.

Left unstated the record defaults the slot, which the target resolves as the table it last declared for this well. That resolution is the target’s; an unstated slot stays unstated here and comes back out defaulted.

ps.Inject(*, well, phase: Literal['oil', 'water', 'gas'], rate: Union[ScalarPropertyInput, Expression, NoneType] = None, reservoir_volume: Union[ScalarPropertyInput, Expression, NoneType] = None, bhp: Union[ScalarPropertyInput, Expression, NoneType] = None, control: Optional[Literal['group']] = None, rate_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, reservoir_volume_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, bhp_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, thp_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, lift_table: InjectionTable | None = None, bhp_adjustment: Optional[ScalarPropertyInput] = None, pressure_loss_multiplier: Optional[ScalarPropertyInput] = None, lift_calculation: Optional[Literal['implicit', 'explicit']] = None, unstable_thp: Optional[Literal['allow', 'prevent_report_once', 'prevent_report_always']] = None, status: Literal['open', 'shut', 'stop'] = 'open', phase_word: str | None = None) -> None

Control the selected injectors of phase from this date (WCONINJE).

ps.Inject(well="I1", phase="water", rate=500.0, bhp_limit=300.0) injects 500 of water a day while the bottom-hole pressure stays below 300. The one quantity given bare is the target; every other quantity is a limit (rate_limit, bhp_limit, …).

  • well: the well’s name (or the ps.Well).
  • phase: "water", "gas" or "oil", required.
  • The target, one of: rate, the injected phase’s surface rate (stb/day of water or Mscf/day of gas in FIELD, sm3/day in METRIC); reservoir_volume (rb/day, rm3/day); or bhp (psia, barsa).
  • The limits: rate_limit, reservoir_volume_limit, bhp_limit and thp_limit.

Common mistakes: the rate is rate=, never water_rate= or gas_rate= (refused, naming rate=); a gas rate in FIELD is Mscf/day, not scf/day. A new well needs its control in the same m.set as the well, or it is refused ([well.control.missing]).

A control is a dated change, held at schedule/<date>/<well>/control; ps.Adjust changes one target or limit and keeps the rest. control="group" puts the well under its group’s control, with rate_limit, reservoir_volume_limit and bhp_limit constraining it. lift_table names the ps.lift.InjectionTable the well injects through, and thp_limit the tubing-head pressure it holds the well below, read through that table; a table that reuses a number comes into force at the date of the first control that names it, as ps.Produce’s does.

import petroscript as ps control = ps.Inject(well=“I1”, phase=“gas”, rate=100_000.0, # sm3/day in METRIC … bhp_limit=300.0) # barsa control.rate 100000.0

On a model m that holds the well, m = m.at("2011-04-01", control) puts it in force from that date, and m.control("I1", at="2011-04-01") reads it back.

ps.InjectionHistory(*, well, phase: Literal['oil', 'water', 'gas'], rate: ScalarPropertyInput, control: Literal['rate', 'bottom_hole_pressure'] = 'rate', thp: Optional[ScalarPropertyInput] = None, bhp_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, lift_table: InjectionTable | None = None, status: Literal['open', 'shut', 'stop'] = 'open') -> None

Hold the selected injectors to an observed injection rate (WCONINJH).

ps.InjectionHistory(well="I1", phase="water", rate=2000.0) holds I1 to its observed water injection rate from this date.

control is "rate", the observed rate, or "bottom_hole_pressure", the observed bottom-hole pressure. The rate is a surface volume rate in the model’s units. It is a dated change, held at schedule/<date>/<well>/control as an injector’s control is.

lift_table is WCONINJH 12.3.255 item 7, the injection twin of the binding described on History, and it reads the same way on both pinned releases: unbinding it takes WTHP:INJ1 from 209.1630 to 0.0000 barsa and moves no bottom-hole pressure. The table is typed, so an injection record cannot name a production table — which is the refusal both releases make in their own words when a deck tries it.

ps.Issue(*, code: str, severity: Literal['error', 'warning', 'info'], address: str | None = None, message: str, fix: str | None = None, value: float | str | None = None, unit: str | None = None, held_against: float | str | None = None, basis: Optional[Literal['flow', 'ill_posed', 'contradiction', 'context']] = None, facts: tuple[str, ...] = ()) -> None

One row of what PetroScript found: [code] message, then fix:.

ps.Issue(code="acme.pressure_floor", severity="warning", message="the limit is below the floor", value=40.0, unit="bar", held_against=50.0) is a row a registered check returns.

code : str The registered code, "well.control.missing". severity : {“error”, “warning”, “info”} An error refuses; a warning prints once at compile; an info row only informs. address : str, optional The address of what the model states that the row is about, "schedule/2021-01-01/P1/control". message : str What is wrong, in a sentence. fix : str, optional What to type; an error’s comes from the catalog when the check states none. value, unit, held_against : optional A plugin check’s measured quantity, its unit and the bound it was held against. basis : {“flow”, “ill_posed”, “contradiction”, “context”}, optional Why an error refuses, from the catalog: flow, OPM Flow refuses the deck; ill_posed, the arithmetic has no answer; contradiction, two statements of the model that together break a law of physics or state one fact twice; context, a gate on the authoring. None for a warning or info row. facts : tuple of str, optional The derived facts the row rests on, each "facts/<group>/<subject>/<name>", read in m.describe()["facts"].

ps.Model(*, name: str | None = None, title: str | None = None, units: Literal['field', 'metric', 'lab'], start: DayInput, capacity: Capacity = <factory>, dimensions: Dimensions = <factory>, grid: Cartesian | CornerPoint | Radial | Spider, rock: Rock, fluid: dict[int, BlackOil | OilWater | GasWater] | BlackOil | OilWater | GasWater | Compositional | None = None, relperm: dict[int, tuple[OilWater | GasOil | Gas | Water | Oil | GasWater | HydrocarbonRelPerm | LETRelPerm | MaxCapillaryPressure | ThreePhase | EndpointScaling | CriticalTolerance | Hysteresis | JFunction | SaturationOptions, ...]] | tuple[OilWater | GasOil | Gas | Water | Oil | GasWater | HydrocarbonRelPerm | LETRelPerm | MaxCapillaryPressure | ThreePhase | EndpointScaling | CriticalTolerance | Hysteresis | JFunction | SaturationOptions, ...] | None = None, regions: Regions | None = None, passive_tracers: tuple[Tracer, ...] = (), physics: tuple[Package | Package | Package | Package | Package | Package | Package | Package | Package, ...] = (), initial: dict[int, Equilibrium] | Equilibrium | ExplicitState | Restart | None = None, summary: Summary = <factory>, output: Output = <factory>, numerics: Numerics | None = None, edits: tuple[Assign | Scale | Add | Clip | Operate | Copy | CellDepthOverride | PoreVolumeOverride | PoreVolumeScale | FaultMultiplier | NonNeighbor | RegionPairMultiplier, ...] = (), transmissibility: tuple[CellTransmissibilityField | DirectionalTransmissibilityMultiplier | RegionalPoreVolumeMultiplier | RegionPairMultiplier, ...] = (), faults: tuple[Fault | FaultMultiplier, ...] = (), non_neighbor_connections: tuple[NonNeighbor | ConnectionTransmissibilityEdit | NonNeighborConnectionPolicy, ...] = (), wells: tuple[Well, ...] = (), groups: tuple[Group, ...] = (), lift_tables: tuple[ProductionTable | InjectionTable, ...] = (), network: Network | None = None, schedule: Schedule = <factory>, aquifers: tuple[Fetkovich | CarterTracy | Numerical | ConstantFlux | InfluenceTable, ...] = (), boundary_conditions: tuple[BoundaryCondition, ...] = (), acknowledged_warnings: tuple[str, ...] = ()) -> None

A reservoir model: one frozen value, built by keyword, that compiles to a simulator’s deck.

It is the whole case a deck states: ps.compile(m) writes it as one deck, RUNSPEC to SCHEDULE, and ps.load reads a deck back into one. Every bare number is in the system units names (“metric”, “field” or “lab”), apart from a duration that times the run (a report step, a time step’s length or limit, the summary’s write interval, a schedule event’s interval, wait or period), which is days in every system: a “lab” deck writes it in hours. Any other time, a table’s value (a polymer’s half-life), is in the system’s own time unit, hours in “lab”. Its fields are the parts of the reservoir, each read and written by its address (m.get("rock/porosity")): the case’s name, title, units and start, then capacity, grid, rock, fluid, relperm, regions, passive_tracers, physics (the family packages), initial, summary, output, numerics, edits, transmissibility, faults, non_neighbor_connections, the wells, groups, lift_tables and network, the aquifers and boundary_conditions, the acknowledged_warnings, and the dated changes in schedule.

A model that contradicts itself is refused as it is built (ps.ModelError, holding every error row); m.check() returns the same rows without raising. Nothing is edited in place: m.at, m.set, m.until and m.replace return a new model.

import petroscript as ps pond = ps.Model( … name=“POND”, … units=“metric”, # m, barsa, mD, rm3/sm3, cP, kg/m3 … start=“2020-01-01”, … grid=ps.grid.Cartesian(shape=(5, 5, 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=4.5e-5, reference_pressure=200.0), # 1/bar at barsa … fluid=ps.fluid.BlackOil( … water=ps.pvt.Water(reference_pressure=200.0, fvf=1.02, compressibility=4.4e-5, … viscosity=0.5, viscosibility=0.0), … water_surface_density=1000.0), … relperm=[ps.relperm.Water(sw=[0.0, 1.0], krw=[0.0, 1.0], pc=[0.0, 0.0])], … initial=ps.initial.Equilibrium(datum_depth=2000.0, datum_pressure=200.0), … schedule=ps.Schedule(end=“2021-01-01”), … ) pond.get(“rock/porosity”) 0.2 pond.grid.shape (5, 5, 2)

ps.ModelError(issues: Iterable[Issue]) -> None

A model that contradicts itself, or a construction refused: issues holds the rows.

except ps.ModelError as error: catches a model refused as it is built; error.issues holds its rows, each with its code and fix.

It is not a ValueError, so pydantic does not fold its rows into a generic wrapper.

ps.Numerics(*, initial_timestep: Optional[ScalarPropertyInput] = None, maximum_timestep: Optional[ScalarPropertyInput] = None, minimum_timestep: Optional[ScalarPropertyInput] = None, minimum_choppable_timestep: Optional[ScalarPropertyInput] = None, maximum_timestep_growth: Optional[ScalarPropertyInput] = None, maximum_timestep_decay: Optional[ScalarPropertyInput] = None, convergence_failure_decay: Optional[ScalarPropertyInput] = None, convergence_failure_growth: Optional[ScalarPropertyInput] = None, maximum_timestep_after_well_event: Optional[ScalarPropertyInput] = None, target_truncation_error: Optional[ScalarPropertyInput] = None, target_nonlinear_convergence_error: Optional[ScalarPropertyInput] = None, mass_balance_error_target: Optional[ScalarPropertyInput] = None, target_linear_convergence_error: Optional[ScalarPropertyInput] = None, maximum_truncation_error: Optional[ScalarPropertyInput] = None, maximum_nonlinear_convergence_error: Optional[ScalarPropertyInput] = None, maximum_mass_balance_error: Optional[ScalarPropertyInput] = None, maximum_linear_convergence_error: Optional[ScalarPropertyInput] = None, maximum_well_flow_convergence_error: Optional[ScalarPropertyInput] = None, target_local_fluid_in_place_error: Optional[ScalarPropertyInput] = None, target_surfactant_change: Optional[ScalarPropertyInput] = None, ion_exchange_damping_threshold: Optional[ScalarPropertyInput] = None, tracer_implicitness: int | None = None, maximum_newton_iterations: int | None = None, minimum_newton_iterations: int | None = None, maximum_linear_iterations: int | None = None, minimum_linear_iterations: int | None = None, maximum_well_iterations: int | None = None, maximum_thp_iterations: int | None = None, maximum_pressure_change: Optional[ScalarPropertyInput] = None, maximum_saturation_change: Optional[ScalarPropertyInput] = None, target_pressure_change: Optional[ScalarPropertyInput] = None, maximum_tolerable_pressure_change: Optional[ScalarPropertyInput] = None, minimum_newton_iterations_before_bisection: int | None = None, pressure_residual_solver=None, well_target_update: int | None = None, maximum_segment_iterations: int | None = None, maximum_segment_solves: int | None = None, segment_tolerance_reduction: float | None = None, segment_tolerance_growth: float | None = None, user_quantity_magnitude_limit: float | None = None, user_quantity_undefined_value: float | None = None, user_quantity_equality_tolerance: float | None = None, network_balance_interval: int | None = None, network_pressure_tolerance: Optional[ScalarPropertyInput] = None, maximum_network_iterations: int | None = None, network_thp_tolerance: Optional[ScalarPropertyInput] = None, maximum_network_thp_iterations: int | None = None, stack_depth: int | None = None, constrained_pressure_residual: bool | None = None, linear_solver: str | None = None, fully_implicit: bool | None = None, data_check_only: bool | None = None, parallel_domains: int | None = None, parallel_method: Optional[Literal['distributed', 'serial']] = None, next_step: Optional[ScalarPropertyInput] = None, next_step_persists: bool = False) -> None

The run’s numerical settings: ps.Numerics(maximum_timestep=5.0, ...).

The time-step lengths and their growth and decay factors, the target and maximum convergence errors and the iteration ceilings a run may spend on a step (TUNING, manual 12.3.231); the pressure-residual preconditioner and the Newton iterations in which well targets update (CPR, NUPCOL); the multisegment-well solver’s iterations (WSEGITER); how user quantities evaluate (UDQPARAM); when the production network is balanced and how its iterations converge (NETBALAN); the solver hints (stack_depth, constrained_pressure_residual, fully_implicit: NSTACK, CPR, FULLIMP; linear_solver, which Flow takes only on its command line: deck.run() does not pass it, and the compile warns) and the run mode (data_check_only, parallel_domains, parallel_method: NOSIM, PARALLEL). OPM Flow honours TUNING’s time-step controls only when it runs with --enable-tuning=true (off by default), which deck.run() does not pass; the compile warns where the model states them.

A model states its start settings in Model.numerics. Inside ps.Event(date, ...) a ps.Numerics is a dated change: a re-tuning (the TUNING fields it states replace those in force), a re-balanced network (the NETBALAN fields it states), a limit on the next time step (next_step, NEXTSTEP; next_step_persists keeps it applying), or a restated well-target update (well_target_update, NUPCOL). A start-only setting stated at a date is refused. The time-step lengths are days in every unit system (a “lab” deck’s TUNING states them in hours). The start settings are held at numerics/<field>, a dated one at schedule/<date>/numerics.

ps.Open(*, well, completions=None, cell: tuple[int | None, int | None, int | None] | None = None) -> None

Open the selected wells from this date: ps.Open(well="P1") (WELOPEN, OPEN).

A dated change, held at schedule/<date>/<well>/status; completions= or cell= opens connections instead of the well (ps.Shut says how the three scopes differ). A reopened well flows under the control it held.

ps.Output(*, static_file: bool = True, static_file_endpoints: bool = False, static_file_minus_multipliers: bool = False, restart=None, initial_restart=None, print=None, print_run_specification: bool | None = None, print_grid: tuple[str, ...] = (), print_solution: tuple[str, ...] = (), print_fluid_in_place: Optional[Literal[1, 2, 3]] = None, print_restart_output: Optional[Literal[1, 2, 3]] = None, print_properties: tuple[str, ...] = (), print_regions: tuple[str, ...] = (), grid_file: Optional[Literal['none', 'standard', 'extended']] = None, egrid_file: bool | None = None, static_index: bool | None = None, restart_index: bool | None = None, message_file: bool | None = None, formatted_input: bool | None = None, formatted_output: bool | None = None, spreadsheet_summary: bool | None = None, summary_dates: bool | None = None, message_limits=None, table_extrapolation: Optional[Literal['off', 'pvt', 'vfp', 'all', 'detailed']] = None, monitoring: bool | None = None) -> None

What a run writes beside its answers: ps.Output(static_file=True, print=...).

static_file writes the static file, the grid’s static arrays (INIT, manual 6.3.97; the file run.static reads), and static_file_endpoints adds the saturation end points the target derives to it (FILLEPS, manual 8.3.68), for a post-processor to read; static_file_minus_multipliers adds the minus-direction transmissibility multipliers (MULTX-, MULTY-, MULTZ-; each 1.0 where the grid states none) to it (GRIDOPTS item 1, TRANMULT YES, manual 5.3.59). restart is what the restart files hold from the schedule’s start (RPTRST in SCHEDULE, manual 12.3.207) and initial_restart what the initial restart holds (RPTRST in SOLUTION, manual 10.3.79). print is the schedule’s printed report (RPTSCHED, manual 12.3.208); the print_* fields are the other sections’ printed reports: print_run_specification (RPTRUNSP), print_grid (RPTGRID), print_solution with its print_fluid_in_place and print_restart_output levels (RPTSOL), print_properties (RPTPROPS) and print_regions (RPTREGS), each item a semantic name (these cards’ older integer form Flow answers as if the record were not there, measured on both supported releases, so a deck’s integers are read past). print and restart carry their cards’ integer form as stated, each position the request the card’s own table gives it (RPTSCHED, manual 12.3.208; RPTRST, 12.3.207). The file fields choose the output files’ forms (grid_file, egrid_file, static_index, restart_index, message_file, formatted_input, formatted_output, spreadsheet_summary, summary_dates); message_limits, table_extrapolation and monitoring the run’s messages. The summary vectors a run allocates (SMRYDIMS) are ps.Capacity(summary_vectors=).

Inside ps.Event(date, ...) a ps.Output states print or restart (or both) from that report time on; the other fields are the run’s own and are refused at a date. The model’s own is held at output/<field> (m.set("output/static_file", True)).

ps.Perforate(*, well, k: tuple[int, int], i: int | None = None, j: int | None = None, diameter: Optional[ScalarPropertyInput] = None, status: Literal['open', 'shut', 'stop'] = 'open', saturation_region: int | None = None, connection_factor: Optional[ScalarPropertyInput] = None, kh: Optional[ScalarPropertyInput] = None, skin_factor: float | None = None, non_darcy_factor: Optional[ScalarPropertyInput] = None, direction: Optional[Literal['x', 'y', 'z']] = None, pressure_equivalent_radius: Optional[ScalarPropertyInput] = None) -> None

One dated connection of a well to the grid (COMPDAT): a run of layers in one column.

ps.Perforate(well="P1", k=(1, 3), diameter=0.2) connects P1 to layers 1 to 3 of its head’s column; i and j state another column (a deviated well). A perforation is a change the schedule dates, never a field of the well: a well is an identity, and what changes with time lives in the schedule. It is held at schedule/<date>/<well>/perforation/<n>, the well’s n-th perforation at that date.

well : str or Well The well’s name, or the well. k : tuple of int The first and last layer, 1-based and inclusive. i, j : int, optional The column, 1-based; the well’s head when left out. diameter : float, optional The wellbore diameter, a length in the model’s units. status : {“open”, “shut”, “stop”} Whether the connection is open; open by default. saturation_region : int, optional The saturation region whose tables give the connection’s mobilities (COMPDAT item 7): the well function, which governs the inflow alone; the cell’s own region still moves the fluids around it. Left out, the cell’s. connection_factor : float, optional The connection’s transmissibility factor between the wellbore and the cell (COMPDAT item 8), a viscosity times a reservoir volume per time per pressure in the model’s units, used as stated. Left out, or zero or less, the simulator computes it by Peaceman’s formula from kh, the diameter, skin_factor and the cell’s radius across direction (or pressure_equivalent_radius). kh : float, optional The connection’s permeability-thickness product (COMPDAT item 10), a permeability times a length in the model’s units. With no stated connection_factor, a kh above zero gives the factor; left out, or zero or less, the simulator takes it from the cell: its permeability across the bore times its length along direction. With a stated factor, kh does not change the factor but is still read: it sets the connection’s length (kh over the cell’s permeability across the bore, which the non-Darcy correlation and a filter cake’s flow area read) and the Peaceman denominator a later ps.wells.Skin rescales the factor by. There, left out or negative, the simulator derives it from the factor, the diameter, the cell’s radius (or pressure_equivalent_radius) and skin_factor; zero, it takes it from the cell. skin_factor : float, optional The connection’s skin factor, dimensionless (COMPDAT item 11); left out, 0. With no stated connection_factor it enters the computed factor’s Peaceman denominator. With a stated factor the factor stays as stated, but the skin enters the kh derived from it when kh is left out, and a later ps.wells.Skin rescales the factor by its change from this skin. non_darcy_factor : float, optional The non-Darcy D factor of a gas well’s rate-dependent skin (COMPDAT item 12), a time per surface gas volume in the model’s units: zero or more states the well’s, which the simulator shares among its connections; negative states this connection’s own, its sign reversed. Left out, none. direction : str, optional The direction the wellbore crosses the cell (COMPDAT item 13): "x", "y" or "z". It orients the cell for the connection: the cell’s permeabilities and sizes across the bore give its Peaceman radius, and their permeability (the geometric mean of the two) times the cell’s length along the bore gives the cell’s kh; the connection’s length is kh over that permeability, whether or not the factor is stated. Left out, "z". pressure_equivalent_radius : float, optional The connection’s pressure-equivalent radius, a length (COMPDAT item 14), in place of the one Peaceman’s formula gives from the cell. Left out, Peaceman’s.

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field m = m.at(“2010-01-01”, ps.Perforate(well=“M-1”, k=(3, 4), diameter=0.2)) # m m.get(“schedule/2010-01-01/M-1/perforation/1”).k (3, 4)

ps.PetroScriptError(*args: Any, code: str | None = None, address: str | None = None, fix: str | None = None, at: str | None = None) -> None

A refusal PetroScript raises: code, address, message, fix and at.

except ps.PetroScriptError as error: catches every refusal PetroScript raises, error.code naming its kind.

message is the text the error prints; at is the author’s script line where the refusal was met (an import’s is the deck’s file and line).

The one category PetroScript prints its warnings through, one line per code.

ps.compile prints a model’s and its deck’s warning rows through it, once each. warnings.filterwarnings("ignore", category=ps.PetroScriptWarning) silences them for the session; a model that means what a physics warning flags states its code in acknowledged_warnings.

ps.Produce(*, well, oil_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, water_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, gas_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, liquid_rate: Union[ScalarPropertyInput, Expression, NoneType] = None, reservoir_volume: Union[ScalarPropertyInput, Expression, NoneType] = None, bhp: Union[ScalarPropertyInput, Expression, NoneType] = None, thp: Union[ScalarPropertyInput, Expression, NoneType] = None, control: Optional[Literal['group']] = None, oil_rate_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, water_rate_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, gas_rate_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, liquid_rate_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, reservoir_volume_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, bhp_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, thp_limit: Union[ScalarPropertyInput, Expression, NoneType] = None, lift_table: ProductionTable | None = None, artificial_lift_quantity: Union[ScalarPropertyInput, Expression, NoneType] = None, bhp_adjustment: Optional[ScalarPropertyInput] = None, pressure_loss_multiplier: Optional[ScalarPropertyInput] = None, lift_calculation: Optional[Literal['implicit', 'explicit']] = None, unstable_thp: Optional[Literal['allow', 'prevent_report_once', 'prevent_report_always']] = None, status: Literal['open', 'shut', 'stop'] = 'open') -> None

Control the selected producers from this date (WCONPROD).

The one quantity given bare is the target; every other quantity is a limit (bhp_limit, liquid_rate_limit, …) and constrains the well beside it::

ps.Produce(well="P1", oil_rate=1500.0, liquid_rate_limit=3000.0, bhp_limit=100.0)

The target is one of oil_rate, water_rate, gas_rate, liquid_rate, reservoir_volume, bhp or thp (a tubing-head pressure, which the well’s lift table turns into a rate). Each is in the model’s units: a rate is a surface volume rate (a reservoir volume rate for reservoir_volume), bhp and thp are pressures. A control is a dated change, held at schedule/<date>/<well>/control; a well holds one control per date, and m.control(well, at=) reads the one in force. control="group" puts the well under its group’s control: the group sets the target, so every stated quantity is a limit. lift_table names the ps.lift.ProductionTable the well flows through, and thp_limit the tubing-head pressure it holds the well above, read through that table::

ps.Produce(well="P1", gas_rate=5.0e5, bhp_limit=50.0, thp_limit=30.0, lift_table=t1)

A producer opened, shut or stopped with its targets cleared states its status and no target (WCONPROD with its control mode defaulted): ps.Produce(well="P1", status="shut"); “open” is stated (a producer stating no status and no target is refused, its target forgotten). The limits it states are written where the deck writes them and hold no target (measured: they leave the run as it is). ps.Shut changes the status alone and leaves the targets standing.

The lift settings ride with the table: artificial_lift_quantity (the value on the table’s lift axis), bhp_adjustment and pressure_loss_multiplier (WVFPDP), lift_calculation and unstable_thp (WVFPEXP).

A table the model does not hold yet joins Model.lift_tables. A control at a later date may name a different table under a number (number) in use: the well flows through it from that date and the deck writes the table there, which is refused while another well’s control or a network member still names the earlier table (give those wells a control at that date naming the new table, or give the table its own number).

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field held = ps.Produce(well=“M-1”, gas_rate=450_000.0, bhp_limit=60.0) # sm3/day, barsa m = m.at(“2010-03-01”, held) m.get(“schedule/2010-03-01/M-1/control”) == held True

ps.Quantity(value: float, unit: str) -> None

A number with its own unit: ps.Quantity(30.0, "barsa").

value is the number and unit the word of the unit it is counted in; kind is what it is read as (a pressure, a length, a time, …), a unit’s own kind until a slot says, and is never stated.

The slot it is given to says what it is: a pressure slot reads barsa as a pressure and converts it to the model’s unit system, and refuses a unit of another kind; a unit that names one kind (barsa, m, day) is that kind wherever it stands. A field that states no kind (a fraction, a factor, a compressibility), a grid statement’s number and a condition’s or a formula’s number take a bare number in the model’s units and refuse a Quantity ([units.wrong_kind]). ps.help("units") lists the units each kind takes. A unit’s case is not read (PSIA, degc), but in the pascal words, which are read as written: Pa, kPa, MPa (mPa is a millipascal, and no pressure a model states).

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # metric: barsa m = m.set(“initial/1/datum_pressure”, ps.Quantity(3626.0, “psia”)) # read as a pressure m.get(“initial/1/datum_pressure”) ps.Quantity(value=3626.0, unit=‘psia’)

ps.Regions(*, saturation=None, relperm_x=None, relperm_y=None, relperm_z=None, pvt=None, equilibration=None, rock=None, flux=None, transmissibility=None, operation=None, fluid_in_place=None, fluid_in_place_sets=<factory>, imbibition=None, imbibition_x=None, imbibition_y=None, imbibition_z=None, miscible=None, polymer_mixing=None) -> None

The model’s region maps (SATNUM, PVTNUM, …): which region of each kind each cell is in.

ps.Regions(pvt=[1, 1, 2], saturation=1) puts the first two layers in pvt region 1 and the third in region 2, and every cell in saturation region 1.

saturation assigns saturation-function region IDs to cells (SATNUM); relperm_x, relperm_y and relperm_z the tables a cell’s flow reads in each direction. pvt, equilibration, and rock select PVT, equilibration, and rock/compaction families (PVTNUM, EQLNUM, ROCKNUM); fluid_in_place the regions the fluid in place is reported by (FIPNUM), and fluid_in_place_sets further named sets of them, {name: map} (FIP<name>, the name 1 to 5 upper-case letters or digits); flux, transmissibility and operation the regions grid statements (ps.grid.InRegion) and region-pair multipliers select; miscible the miscible region whose tables a cell reads (MISCNUM) and polymer_mixing its polymer mixing region (PLMIXNUM). Every map is one region everywhere (a positive integer), one region per layer (pvt=[1, 1, 2], each held in every cell of its layer) or an explicit (nx, ny, nz) NumPy tensor of them.

The sizes a deck states beside the maps (REGDIMS items 4 and 7, GRIDOPTS item 2) are ps.Capacity’s flux_regions, operation_regions and transmissibility_regions; the maps carry only the regions they assign. The maps are held at regions/<map> (regions/fluid_in_place). m.array reads most as a cell array named by the map’s name joined to region with an underscore (m.array("fluid_in_place_region")), with two other spellings: transmissibility is "multiplier_region" (MULTNUM) and relperm_x, relperm_y and relperm_z are "relperm_region_x" and its kin; imbibition_x, imbibition_y, imbibition_z, miscible, polymer_mixing and fluid_in_place_sets have no m.array name.

import numpy as np import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # 10 x 10 x 5 cells layers = np.ones(m.grid.shape, dtype=int) layers[:, :, 3:] = 2 # layers 4 and 5 m = m.set(“regions”, ps.Regions(fluid_in_place=layers)) m.array(“fluid_in_place_region”)[0, 0, :].tolist() [1, 1, 1, 2, 2]

ps.Rock() -> None

The rock’s porosity, permeability and pore-volume compressibility (PORO, PERMX, ROCK).

Each cell property is in the model’s units: porosity a fraction, each permeability a permeability, compressibility a compressibility (per unit of pressure) and reference_pressure a pressure. The deck writes PORO and PERMX, PERMY and PERMZ in the GRID section and ROCK in PROPS; the rock is held at rock/<field> (m.get("rock/porosity")), and m.array("permeability_z") reads a cell array after the grid program.

The permeability is stated in one of two forms, direction by direction, and the model holds the form stated: permeability=150.0, vertical_ratio=0.1 (the horizontal permeability, along x and y, and the vertical permeability as a share of it: permeability_z is vertical_ratio times the permeability along x), or permeability_x, permeability_y and permeability_z each on its own. The forms mix across directions (permeability=150.0, permeability_z=15.0); one direction stated in both is refused.

Each cell property (porosity, permeability, permeability_x, permeability_y, permeability_z, dispersivity) is a single value, one value per layer (porosity=[0.20, 0.25, 0.30], the model spreading each over its layer’s cells), or a full (nx, ny, nz) array matching the model grid. vertical_ratio is one number, zero or more, read of the permeability the rock states, before the model’s grid statements: a later ps.grid.Scale("permeability_x", by=0.5) leaves permeability_z where the ratio put it.

compressibility is the pore volume’s compressibility at reference_pressure (ROCK), stated together: ps.Rock(..., compressibility=4.5e-5, reference_pressure=250.0), or one value each per PVT region as two lists of one length (per rock region with table_regions="rock_regions"). porosity is the porosity at reference_pressure: the simulator grows the pore volume from there with the pressure, and m.describe()["facts"]["rock"] shows each rock region’s pore_volume_factor at its datum pressure.

compaction is the pore volume’s response to pressure beyond compressibility: a ps.grid.RockPressureCompaction (multipliers tabulated against pressure) or a ps.grid.RockCompaction (its water-induced form); None states none.

table_regions names the region family that allocates the rock property tables to cells. It stands on its own: a model may choose the allocation while stating only pore-volume compressibility, which is what the manual means by allocating “the various ROCK and ROCKTAB property tables” (8.3.259 item 3). Left unstated, the target’s own default applies.

import petroscript as ps rock = ps.Rock(porosity=0.22, permeability=150.0, vertical_ratio=0.1, # mD: metric … compressibility=4.5e-5, reference_pressure=250.0) # 1/bar, barsa m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # metric m = m.set(“rock”, rock) float(m.array(“permeability_z”)[0, 0, 0]) # 0.1 x 150 mD 15.0

ps.RunError(message: str, *, folder: str | None = None, exit_code: int | None = None) -> None

Raised when a simulator could not run a deck to its end, or none was found to run it.

except ps.RunError as error: catches a run that stopped short; error.folder holds its files and error.exit_code the simulator’s exit status.

The message carries what the simulator said: the first error lines of its print file and the last lines of its terminal output. folder is where its files are and exit_code what it exited with (both None when no simulator started).

ps.Schedule(*, end: Optional[DateInput] = None, report: Union[Literal['monthly'], tuple[Union[DateInput, Every], ...]] = (), initial_timesteps=(), events: tuple[Event, ...] = ()) -> None

The run’s report times and what changes at each: ps.Schedule(end=, report=, events=).

It is the deck’s SCHEDULE section: each report time a DATES record (a ps.Every run a TSTEP card), each event’s changes the cards after the time card that reaches its date (an event at the start: before the first time card). events holds ps.Event(date, *changes), each at a report time, its changes (a well’s perforations and controls among them) in the order stated. report states the report times: each a date (a date and its time of day where the deck states one) or a ps.Every(...) run of steps in days, or "monthly", the first of each month to end. An action rule is one of an event’s changes, and so are the start’s dissolution limits, Python hooks, user tables and quantities, well equipment and gas lift (written at the deck’s head). The wells, groups and network are the model’s. Collection fields are tuples so a constructed schedule cannot be mutated in place. It is held at schedule: schedule/end, schedule/report and each event at schedule/<date>; m.until moves the end and m.at dates a change.

import petroscript as ps schedule = ps.Schedule( … end=“2011-06-01”, … report=“monthly”, # the first of each month, and the end … events=[ps.Event(“2009-06-01”, … ps.Perforate(well=“M-1”, k=(1, 2), diameter=0.2), # m … ps.Produce(well=“M-1”, gas_rate=500_000.0, # sm3/day: metric … bhp_limit=50.0))], # barsa … ) m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field m.replace(schedule=schedule).find(“schedule///*”) [‘schedule/2009-06-01/M-1/perforation/1’, ‘schedule/2009-06-01/M-1/control’]

ps.Shut(*, well, completions=None, cell: tuple[int | None, int | None, int | None] | None = None) -> None

Shut the selected wells at the surface from this date: ps.Shut(well="P1"), WELOPEN SHUT.

A dated change, held at schedule/<date>/<well>/status; the well’s control and targets stand, and ps.Open reopens it. The change reaches the well itself (no completions, no cell), every connection (completions="all"), the connections of a range of completion numbers (completions=(2, 3); one number is that one completion), or the connections in a cell (cell=(10, 10, 3), a range beside it narrowing it; None on an axis is any index there, so cell=(None, None, 3) shuts every connection in layer 3). The three scopes are three different runs.

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field m = m.at(“2011-05-01”, ps.Shut(well=“M-2”)) m.find(“schedule/2011-05-01//”) [‘schedule/2011-05-01/M-2/status’] m.control(“M-2”, at=“2011-05-01”).status ‘shut’

ps.Stop(*, well, completions=None, cell: tuple[int | None, int | None, int | None] | None = None) -> None

Stop the selected wells from this date, open to the reservoir: ps.Stop(well="P1").

It writes WELOPEN with STOP, held at schedule/<date>/<well>/status. OPM Flow leaves a flowing well flowing after STOP, a producer producing and an injector injecting; ps.Shut closes a well at the surface.

ps.Summary(*, field: tuple[str, ...] = (), wells: dict[str, tuple[str, ...]] = {}, groups: dict[str, tuple[str, ...]] = {}, completions: dict[tuple[str, int, int, int, int], tuple[str, ...]] = {}, blocks: dict[tuple[int, int, int], tuple[str, ...]] = {}, aquifers: dict[typing.Union[int, typing.Literal['*']], tuple[str, ...]] = {}, numerical_aquifers: dict[typing.Union[int, typing.Literal['*']], tuple[str, ...]] = {}, regions: dict[typing.Union[int, typing.Literal['*'], tuple[str, typing.Union[int, typing.Literal['*']]]], tuple[str, ...]] = {}, region_pairs: dict[tuple[int, int], tuple[str, ...]] = {}, segments: dict[str | tuple[str, int], tuple[str, ...]] = {}, connections: dict[str | tuple[str, int, int, int], tuple[str, ...]] = {}, undeclared_wells: dict[str, tuple[str, ...]] = {}, undeclared_groups: dict[str, tuple[str, ...]] = {}, undeclared_connections: dict[str, tuple[str, ...]] = {}, user_defined: dict[str, tuple[str, ...]] = {}, tracers: dict[str, dict[typing.Literal['field', 'wells', 'groups', 'undeclared_wells', 'undeclared_groups'], tuple[str, ...] | dict[str, tuple[str, ...]]]] = {}, undeclared_tracers: dict[str, dict[typing.Literal['field', 'wells', 'groups', 'undeclared_wells', 'undeclared_groups'], tuple[str, ...] | dict[str, tuple[str, ...]]]] = {}, history: dict[typing.Literal['field', 'wells', 'groups', 'undeclared_wells', 'undeclared_groups'], tuple[str, ...] | dict[str, tuple[str, ...]]] = {}, column_words: tuple[str, ...] = (), performance: bool = False, run_summary: bool = False, separate_reports: bool = False, well_status_vectors: bool = False, write_frequency: Optional[Literal['report_steps_only', 'every_timestep']] = None, write_interval: float | Quantity | None = None) -> None

The vectors a run records, by member: ps.Summary(field=[...], wells={...}, ...).

You ask for each result by the name you later read it by: ps.Summary(field=["oil_production_rate"], wells={"*": ["bottom_hole_pressure"]}), then run.field("oil_production_rate") and run.wells("bottom_hole_pressure"). field takes a list of names; wells, groups and regions take {member: [names]}, with "*" for every well or group. Common names: oil_production_rate, gas_production_rate, water_production_rate, oil_production_total, water_cut, gas_oil_ratio, bottom_hole_pressure, pressure. ps.help("WBHP") turns a Flow mnemonic into its name.

Common mistakes: a Flow mnemonic ("FOPR") is refused, naming its name; wells=["P1"] is refused (a map is needed: wells={"P1": [...]}); and m.summary.replace(wells=...) replaces every well’s requests, as m.set("summary/field", [...]) replaces the field’s list. To add one without dropping the others, keep what is there: m.set("summary/field", [*m.summary.field, "water_cut"]), or set the one quantity’s own address, m.set("summary/field/water_cut", "water_cut"); for one well, m.set("summary/wells/P1", [*m.summary.wells.get("P1", ()), "water_cut"]).

It is the deck’s SUMMARY section, one card per vector (gas_production_rate is FGPR), and what run.field, run.wells, run.groups and run.regions read back by the same names. It is held at summary.

field takes the field quantities by their names; every other scope takes {member: [names]}, the quantities asked for each member, a bare string one quantity (wells={"P1": "water_cut"}):

  • wells: a well’s name, a pattern ("P*", the wells it matches when Flow reads it) or "*"; groups: a group’s name or "*". A well or group the model does not declare is refused (summary.wells.unknown); undeclared_wells, undeclared_groups and undeclared_connections (a well’s name, for its connections) keep the names a deck asks for and declares nothing behind (Flow runs it and writes no column);
  • blocks: an (i, j, k) cell; completions: (well, i, j, k1, k2), a perforated interval; connections: (well, i, j, k) or a well’s name for each of its connections (one entry per well); segments: (well, number), a well’s name for each of its segments or "*";
  • regions: a region number or "*", another fluid-in-place set’s region as ("LAY", 2); region_pairs: (from, to); aquifers and numerical_aquifers: an aquifer’s number or "*";
  • user_defined: a user quantity’s export name, holding the members asked for (none: every member);
  • tracers: a tracer’s quantities (production_rate, production_total, injection_rate, injection_total, production_concentration, injection_concentration) by scope, shaped as history is: tracers={"WT1": {"field": ["production_rate"], "wells": {"P1": ["production_total"]}}} asks for FTPRWT1 and WTPTWT1; a tracer’s "*" and well patterns are Flow’s, stated in undeclared_wells (undeclared_groups for "*"), as a deck’s record loads; undeclared_tracers keeps a tracer the model declares none of by the name a deck asks for it (Flow runs it and writes no column).

The polymer and MICP quantities are the scopes’ own names (polymer_injection_rate, microbes_in_place, oxygen_injection_rate_per_completion).

The names are the ones a run’s results are read by (oil_in_place, water_production_rate); a deck’s mnemonic ("FOPR") is refused naming its name. history takes the observed vectors the same way, by scope: history={"wells": {"*": ["oil_production_total"]}} requests WOPTH. The model reads its summary by address: summary/field/<quantity>, summary/<scope>/<member> (m.set("summary/wells/M-3", m.get("summary/wells/M-1"))), the observed ones under summary/history/....

column_words names a column by the second word Flow reads its vector by: column_words=["BOKR"] writes a block’s oil_relative_permeability as BOKR, and the column carries that word; the vector’s own word listed beside it (column_words=["FGSR", "FSGR"]) writes both cards, one column each. A word is written while its vector is asked for; one Flow reads no vector by is refused listing the words.

The target writes one card per vector and word, the vectors in its own order first (FOPR, FOPT, the wells’ and blocks’ it lists) and the rest in the order they first appear walking the members (each member’s quantities in the order stated, the observed after them), each card’s members in member order. A summary read from a deck keeps the deck’s own card order while it is what the deck states (an edit writes the walk). The policies say what else the summary file carries: performance (PERFORMA), run_summary (RUNSUM), separate_reports (SEPARATE), well_status_vectors (FMWSET), write_frequency (RPTONLY or RPTONLYO) and write_interval (SUMTHIN: a number of days, or a time ps.Quantity, above 0; a LAB deck writes it in hours).

import petroscript as ps summary = ps.Summary(field=[“gas_production_rate”, “pressure”], # sm3/day, barsa … wells={"": [“bottom_hole_pressure”]}) # every well’s summary.field (‘gas_production_rate’, ‘pressure’) summary.wells[""] (‘bottom_hole_pressure’,)

On a model m, m.set("summary", summary) replaces its requests, and m.get("summary/wells/*") reads one member’s back.

ps.Well() -> None

A well: an identity. Its name, its head’s column, its group and its declared properties.

ps.Well(name="P1", i=5, j=5) declares P1 with its head in column (5, 5). What a well does over time is not a field of the well: its perforations (ps.Perforate) and its controls are changes the schedule dates, and a well’s role, producer or injector, is the control it is given. The deck declares it with WELSPECS, at the start or with its first change at introduced; it is held at wells/<name>.

name : str The well’s name; no * or ?. i, j : int, optional The head’s column, 1-based. Left out on a well with a trajectory, Flow takes that axis from the first cell the trajectory crosses (WELSPECS item 3 or 4 defaulted), each axis on its own, and PetroScript never states one for it. group : Group, optional The group the well belongs to; none states no group. bhp_reference_depth : float, optional The depth the bottom-hole pressure is referred to, a length in the model’s units; the target derives it when left out. preferred_phase : {“oil”, “water”, “gas”}, optional The phase the well is declared to prefer. Left out, it is derived from its controls: an injector’s is the phase it injects, a producer’s the phase its target names (a gas or a water rate), else the first of oil, gas and water the model holds. The deck’s WELSPECS always writes a phase, the derived one when this is left out, and the well loaded back from that deck holds it stated: a well that leaves it out does not compare equal to its own round trip. Stating it is not enough on its own: a well that leaves group out loads back in ps.Group(name="FIELD"). introduced : date, datetime or its text, optional The report time the well is introduced at ("2011-03-01"): its declaration is written with its first change on that date (a ps.Perforate, or its control). None declares it at the start. connection_order : str, optional The order the well’s connections are numbered in: "input_sequence", the order its perforations are stated (COMPORD’s INPUT, written in the block that declares the well). None leaves Flow’s own order. trajectories : list of ps.wells.Trajectory, optional The well’s path, one ps.wells.Trajectory per branch (WELTRAJ), with the connections along it (COMPTRAJ), written in the block that declares the well.

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field m = m.set({ # a new well mid-run … “wells/M-3”: ps.Well(name=“M-3”, i=5, j=5, introduced=“2010-01-01”), … “schedule/2010-01-01/M-3/perforation/1”: … ps.Perforate(well=“M-3”, k=(1, 2), diameter=0.2), # m … “schedule/2010-01-01/M-3/control”: … ps.Produce(well=“M-3”, gas_rate=300_000.0, bhp_limit=50.0), # sm3/day, barsa … }) m.find(“wells/*”) [‘wells/M-1’, ‘wells/M-2’, ‘wells/M-3’]

ps.compile(model: Model, target: str = 'opm-flow') -> Deck

Compile a model for one simulator and return the Deck: its input files, in memory.

ps.compile(m) is the deck of m for opm-flow, one <NAME>.DATA file; deck.write("out") writes its files there and deck.run() runs it.

The Deck is held in memory: nothing is written and nothing is run. deck.write(folder) writes its files; deck.issues holds the rows the compile attached. The model’s warnings and the deck’s are printed here, once: one line per code, through ps.PetroScriptWarning.

model : Model The model to compile. target : str The simulator’s one name.

Deck The target’s files for the model.

TypeError When model is not a ps.Model. CompileError target.unknown when no target has the name target; else the error rows: the model’s own (m.check()), what the target cannot run of it, and schedule.report.missing for a schedule with no report time (state its end).

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s field deck = ps.compile(m) # for opm-flow list(deck.files) [‘MERLE.DATA’] deck.files[“MERLE.DATA”].count(“WCONPROD”) # one per control date 2

Targets (the default is opm-flow): opm-flow measured on release 2026.04 jutuldarcy measured on release 0.4.0

ps.help(subject: object = None) -> None

Print what PetroScript knows about subject, or the quickstart index.

subject is anything a script can name: a class or a value, a deck keyword ("WCONINJE"), a summary mnemonic ("FGPR") or a result’s name ("gas_production_total"), a deck word ("RATE"), a report mnemonic ("ALLPROPS"), an issue code ("well.control.missing"), a topic ("addresses", "facts", "units") or a public path ("ps.aquifers.CarterTracy"). An unknown name is answered with the nearest.

subject : object, optional What to answer; nothing prints the quickstart index.

import petroscript as ps ps.help(“FGPR”) # a summary mnemonic: the name it is read by FGPR: the field quantity gas_production_rate ps.Summary(field=[“gas_production_rate”]); run.field(“gas_production_rate”)

ps.load(path: str | Path) -> Model

Read a deck into a ps.Model: its output and numerics are the model’s.

ps.load("model/CASE.DATA") reads the deck and its includes.

A deck with something the model cannot mean is refused whole, every gap listed at once: the first refusal’s line, then N more gaps: and one line per other gap in deck order, each led by its code and ended by (K times) when the deck states it more than once.

path : str or Path The deck’s main file (CASE.DATA); its includes are read where it names them.

Model The model the deck states.

DeckImportError When the deck states something the model cannot mean. The message is the first gap, at its file and line, its code on error.code (deck.keyword.unsupported, deck.record.malformed, …; ps.DeckImportError lists them); each further gap follows on a line of its own, led by its code in brackets. ModelError When what the deck states contradicts itself, the rows located at the deck’s file and line.

import petroscript as ps m = ps.load(“tests/results_fixtures/QUICKSTART/MERLE.DATA”) # the quickstart’s deck m.name, m.units, m.grid.shape (‘MERLE’, ‘metric’, (10, 10, 5)) m.find(“wells/*”) [‘wells/M-1’, ‘wells/M-2’]

ps.simulators() -> list[Simulator]

List the simulators the targets find to run a deck, each target’s in its own order.

ps.simulators() lists every simulator found, each with the target it runs for (found.name); a deck’s deck.run() takes the first one for the deck’s own target.

For opm-flow: the PETROSCRIPT_FLOW environment variable, then flow on PATH. Each is asked its version; one that does not answer, or whose first line is not OPM Flow’s banner (flow and a release, flow 2026.04), is left out. deck.run() takes the first rung present and raises RunError when it is missing or not OPM Flow; deck.check() does the same for PETROSCRIPT_FLOW, and with it unset leaves out a flow on PATH that is not OPM Flow (its pre-flight is then not computed).

For jutuldarcy: the PETROSCRIPT_JUTULDARCY environment variable, naming a runner (a program that runs Julia with JutulDarcy and GeoEnergyIO loadable) whose driver.jl --version first prints jutuldarcy and a release; one that does not is left out.

list One Simulator per executable found: its target, its version line and its path.

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