Skip to content

Wells and schedule

Wells connect simulator cells to controls that add or remove fluids. Completions state which grid cells are connected to the wellbore. Schedule data states when controls and report steps apply. A well is an identity, ps.Well, in the model’s wells=; what it does is dated: its perforations and its controls are changes a ps.Event states.

The examples below change one small model, a producer and an injector in a 3 by 3 grid:

import petroscript as ps
field = ps.Group(name="FIELD")
producer = ps.Well(name="PROD1", i=1, j=1, group=field)
injector = ps.Well(name="INJ1", i=3, j=3, group=field)
model = ps.Model(
name="WELLS_DOC",
units="field",
start="2020-01-01",
grid=ps.grid.Cartesian(shape=(3, 3, 1), dx=400.0, dy=400.0, dz=40.0, top=2500.0),
rock=ps.Rock(porosity=0.25, permeability=100.0, vertical_ratio=0.1,
compressibility=3.0e-6, reference_pressure=3000.0),
fluid=ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[14.7, 3500.0], oil_fvf=[1.06, 1.0],
oil_viscosity=[1.0, 1.2]),
water=ps.pvt.Water(reference_pressure=3000.0, fvf=1.0, compressibility=3.0e-6,
viscosity=0.5, viscosibility=0.0),
oil_surface_density=53.0,
water_surface_density=64.0,
),
relperm=[ps.relperm.OilWater(sw=[0.2, 0.8], krw=[0.0, 1.0], krow=[1.0, 0.0],
pcow=[0.0, 0.0])],
initial=ps.initial.Equilibrium(datum_depth=2510.0, datum_pressure=3000.0,
oil_water_contact=2600.0),
groups=[field],
wells=[producer, injector],
schedule=ps.Schedule(
end="2020-04-01",
report="monthly",
events=[
ps.Event(
"2020-01-01",
ps.Perforate(well="PROD1", k=(1, 1), diameter=0.3),
ps.Perforate(well="INJ1", k=(1, 1), diameter=0.3),
ps.Produce(well="PROD1", oil_rate=150.0, bhp_limit=1500.0),
ps.Inject(well="INJ1", phase="water", rate=300.0, bhp_limit=4500.0),
),
],
),
)

ps.Perforate(well=..., k=(k1, k2), diameter=...) in a ps.Event connects a well to a run of layers in one grid column, from that date. i and j default to the well’s head column; k is the first and last layer. Every index is one-based. Optional numeric fields such as diameter must be finite scalars or explicit Quantity objects.

Well controls are dated changes, not raw simulator control tokens: ps.Produce(well=..., oil_rate=..., bhp_limit=...) controls producers and ps.Inject(well=..., phase=..., rate=..., bhp_limit=...) injectors, from the event’s date. Each states exactly one target quantity (oil_rate, water_rate, gas_rate, liquid_rate, reservoir_volume or bhp for producers; rate, reservoir_volume or bhp for injectors) with the rate and pressure limits beside it; a control whose target the well does not state, group control, is spelled control="group". A word field such as phase takes one of its listed words in lower case or in capitals (“gas” or “GAS”), and any other word, “Gas” included, is refused; numeric control targets are strict scalars, so strings such as "2500" are rejected instead of coerced. ps.Adjust(well=..., oil_rate=...) changes one target of a well’s control and keeps the control (WELTARG); ps.Adjust(well=..., thp=...), a producer’s artificial_lift_quantity= (WELTARG LIFT) and lift_table= (WELTARG VFP, a table of the well’s kind) are its other one-quantity forms, and ps.wells.TargetMultiplier(well=..., quantity="artificial_lift_quantity", factor=...) scales the lift quantity (WTMULT LIFT). An injector’s lift quantity, a table of the other kind, and a lift quantity onto a well its ps.History drives are refused where the control in force is known.

ps.Well is a well’s identity: its name, its head column i and j, its group, and declared properties such as bhp_reference_depth and preferred_phase. Whether it produces or injects is not a property of the well: it is the control a dated change gives it.

Every verb returns a model that is checked at once, and a well with no control is refused ([well.control.missing]). So a new well, its perforations and its control go in one m.set, each at its address:

added = model.set({
"wells/PROD2": ps.Well(name="PROD2", i=3, j=1, group=field),
"schedule/2020-01-01/PROD2/perforation/1": ps.Perforate(well="PROD2", k=(1, 1),
diameter=0.3), # ft in FIELD
"schedule/2020-01-01/PROD2/control": ps.Produce(well="PROD2", oil_rate=100.0, # stb/day
bhp_limit=1500.0), # psia
})
assert [well.name for well in added.wells] == ["PROD1", "INJ1", "PROD2"]

model.set("wells/PROD2", ...) alone, or model.replace(wells=[...]) followed by m.at(...), raises the error before the control arrives. A well that opens later states introduced= and its date in each address; set alone, it builds, and m.check() and ps.compile refuse it until its perforation and control at that date are stated ([well.introduced.unanchored]).

ps.Group(name=..., parent=...) represents a named operating group used for well membership, group controls, and group summary diagnostics. A parent creates a simple group tree such as operating group A under field group F. The model’s groups= holds them. Each of these runs in OPM Flow: black-oil group trees, production/injection group controls, pressure maintenance, satellite/facility allocation, VFP attachment, network routing, gas-lift, and efficiency factors.

A producer is a ps.Well whose control is a ps.Produce. It removes reservoir fluids according to that control, and its specification names the phase that control targets (gas for a gas_rate, water for a water_rate, else oil; the first of oil, gas and water the model holds when it holds no such phase) unless its preferred_phase states another.

An injector is a ps.Well whose control is a ps.Inject, which adds its phase ("oil", "water" or "gas") according to that control; its specification names that phase as its preferred phase unless its preferred_phase states another.

ps.Schedule(end=..., report=..., events=...) states the run’s report times and what changes at each. report is a list of dates, ps.Every(...) runs of report steps in days, or "monthly" (the first of each month up to end); events holds ps.Event(date, *changes), each at a report time. Report-step lengths must be positive finite numeric scalars or explicit Quantity values. m.at(date, *changes) returns the model with those changes at that date.

A monthly report follows the calendar, so each step is its month’s length, leap days included; steps of a fixed length in days are a ps.Every(...).

ps.Output(print=[...]) describes the report printed during scheduled simulation steps, each request a semantic name: pressure, gas_saturation, solution_gor, wells, fluid_in_place, well_specs, well_details, cpu, restart_frequency=level (write restart data at report times with detail level 1-3), and no_reports (switch off all printed schedule reports; it cannot be combined with other print requests). A request with a level is name=level.

A ps.Output stated in a ps.Event changes the report controls at that date. This lets examples model decks that run for many timesteps, switch reporting detail, and then continue without spelling target report-control records directly.

ps.Output(restart=[...]) describes restart-file output. Frequency requests: basic=level (levels 0-5) and frequency=every (thin periodic restart output at basic levels 3, 4, or 5 to every n-th report step, year, or month). Content requests: all_properties, bubble_dew_points, fluid_in_place, reservoir_fluid_in_place, separator_fluid_in_place, surface_flows, reservoir_flows, phase_densities, saturated_gas_oil_ratio, saturated_oil_gas_ratio, residuals, temperature, rock_compaction, convergence_diagnostics=cells, density, and the per-phase requests oil_relative_permeability, oil_viscosity and oil_formation_volume_factor with their water and gas siblings, or viscosities for every phase at once. A request outside the manual’s vocabulary is refused by name, and so is an array the simulator writes on its own.

The same request serves both places the target deck accepts it: restart= on the model’s output= writes the SCHEDULE card, and initial_restart= the SOLUTION card. ps.Output(restart=...) in a later ps.Event resets the switch at that date.

A dated change names the wells it acts on in well=: one name, a ps.Well, a list of them, a name pattern such as "PROD*", or a declared well list ("*PRODS", below). A selection is written as stated; if a target deck can express several selected wells more compactly, the compiler owns that rendering choice.

Use ps.Open(well=...), ps.Shut(well=...) or ps.Stop(well=...) when a schedule step changes whether selected wells can flow without restating rate or pressure controls. Use ps.Produce or ps.Inject when the operating mode or target changes too.

The same three changes reach a well’s connections instead of the well: completions="all" reaches every connection, completions=(2, 3) a range of completion numbers, and cell=(i, j, k) the connections in that cell. None on an axis of the cell is any index there: cell=(None, None, 3) reaches every connection in layer 3 (WELOPEN’s 0 0 3). The connection must be one the well perforates, except on a well connected along its trajectory, whose cells Flow finds. A negative index is kept as stated, and OPM Flow matches no connection with it.

model = model.at(
"2020-02-01",
ps.wells.Efficiency(well=["PROD1"], factor=0.8),
ps.Adjust(well="PROD1", oil_rate=100.0),
ps.Output(print=["well_details=2", "well_specs", "cpu=2"]),
)
shut_connection = ps.Shut(well="PROD1", cell=(1, 1, 1))
shut_layer = ps.Shut(well="PROD1", cell=(None, None, 1))

History controls, well tests, economics, and workovers

Section titled “History controls, well tests, economics, and workovers”

History controls model observed behavior separately from ordinary production or injection targets. Use ps.History(well=..., control=..., ...) for producers or ps.InjectionHistory(well=..., phase=..., rate=...) for injectors directly in an event, keeping history-mode intent explicit. A history control’s bhp_limit is the observed bottom-hole pressure (WCONHIST item 10, WCONINJH item 5): Flow holds the well to it only under a bottom-hole-pressure control, and under a rate control it is reported and limits nothing (measured: SPE1’s producer held to its oil rate fell to 2235 psia with item 10 at 4000, as with none). A rate-controlled history well’s pressure limit is the default one, the producer_bhp or injector_bhp of ps.wells.ControlDefaults (FBHPDEF), else Flow’s own.

Well testing and economics are represented as domain-owned policies. ps.wells.WellTest(well=..., interval=..., reasons=...) names the test cadence and reasons. ps.wells.Economics(well=..., maximum_water_cut=..., minimum_oil_rate=..., ...) names the economic conditions, any of which may be stated together, and its workover_scope ("well", "completion" or "rate") describes the response, not raw schedule rows.

events = (
ps.History(
well=producer, control="oil_rate",
oil_rate=1000.0, water_rate=25.0, bhp_limit=1800.0,
),
ps.wells.WellTest(well=producer, interval=30, reasons={"physical"}),
ps.wells.Economics(
well=producer,
maximum_water_cut=0.8,
workover_scope="completion",
),
)

ps.History.from_frame(frame, control=..., lift_table=...) turns one table of observed rates into the history’s events: one ps.Event per date, its wells’ ps.History. The frame holds one row per well and date, read by column name (a polars or pandas frame, or a mapping of column name to values): date and well, and any of oil_rate, water_rate, gas_rate, thp, bhp_limit and status, in the model’s units. control names the rate the simulator holds each well to, one word for every well or a mapping by well name, and lift_table the table each well’s record binds.

Multipliers, damage, cycling, and group moves

Section titled “Multipliers, damage, cycling, and group moves”

Multipliers and cycling are dated changes that name their wells: ps.wells.ProductivityMultiplier(well=..., factor=...), ps.wells.TargetMultiplier(well=..., quantity=..., factor=...), ps.wells.InjectionMultiplier(well=..., pressure=..., mobility_factor=...), ps.wells.InjectionDamage(well=..., geometry=..., ...), ps.wells.DamageFactorCorrection(well=..., coefficient=..., exponent=...), and ps.wells.Cycling(well=..., injection_period=..., shut_period=...). Multisegment wells are described below.

ps.grid.FaultMultiplier(fault=, factor=) stated in a ps.Event scales transmissibility across a fault FAULTS declared, from that date: one MULTFLT record, 'F1' 0.5 /, and a run of them standing together shares one card. A fault the model does not declare is refused when the model validates (schedule.fault_multiplier.unknown_fault); successive dates compose by multiplication, so a second card of 1.0 changes nothing.

fault_event = ps.Event("2020-01-01", ps.grid.FaultMultiplier(fault="F1", factor=0.1))

ps.wells.Regroup(well=..., group=...) moves the named wells to another group during the schedule. The target is a well, a list of wells, a pattern or a well list.

ps.wells.WellList(name=..., wells=[...]) declares a named static list of wells at its date. Declaring the list in the schedule registers the name; afterwards the list is a first-class selection: well=prods (the list itself) or well=ps.actions.well(lists="PRODS") (by its name) selects every member well, and the list’s events lower to the named-list reference ('*PRODS') instead of one record per well. ps.actions.well(lists="PR*") is a root over list names. A string in well= is always about wells: well="*RO*" is the wells whose names hold RO, written '\*RO*' with the deck’s escape. List names are semantic (1-7 characters, starting with a letter). action="add", "delete" and "move" edit a list an earlier statement opened.

import petroscript as ps
field = ps.Group(name="FIELD")
model = ps.Model(
name="WELL_LIST_DOC",
units="field",
start="2020-01-01",
grid=ps.grid.Cartesian(shape=(3, 3, 1), dx=400.0, dy=400.0, dz=40.0, top=2500.0),
rock=ps.Rock(porosity=0.25, permeability=100.0, vertical_ratio=1.0),
fluid=ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[14.7, 3500.0], oil_fvf=[1.06, 1.0],
oil_viscosity=[1.0, 1.2]),
water=ps.pvt.Water(reference_pressure=3000.0, fvf=1.0, compressibility=3.0e-6,
viscosity=0.5, viscosibility=0.0),
oil_surface_density=53.0,
water_surface_density=64.0,
),
relperm=[ps.relperm.OilWater(sw=[0.2, 0.8], krw=[0.0, 1.0], krow=[1.0, 0.0],
pcow=[0.0, 0.0])],
initial=ps.initial.Equilibrium(datum_depth=2510.0, datum_pressure=3000.0,
oil_water_contact=2600.0, gas_oil_contact=0.0),
groups=[field],
wells=[
ps.Well(name="PROD1", i=1, j=1, group=field),
ps.Well(name="PROD2", i=3, j=3, group=field),
],
schedule=ps.Schedule(
report=[ps.Every(30.0), ps.Every(60.0)],
events=[
ps.Event(
"2020-01-01",
*(change for name in ("PROD1", "PROD2") for change in (
ps.Perforate(well=name, k=(1, 1), diameter=0.3),
ps.Produce(well=name, oil_rate=150.0, bhp_limit=1500.0),
)),
ps.wells.WellList(name="PRODS", wells=["PROD1", "PROD2"]),
),
ps.Event("2020-01-31", ps.Shut(well=ps.actions.well(lists="PRODS"))),
],
),
)

A list must be declared in the schedule before the first list-backed event (schedule.well_list.undeclared), list names must be unique (schedule.well_list.duplicate_name), and every member must be a scheduled well (schedule.well.unknown).

ps.wells.FilterCake(well=..., fraction=...) and ps.wells.InjectedStream(well=..., filtrate_concentration=...) schedule filter-cake maintenance for selected injectors: filtrate_concentration states the injected filtrate concentration in ppm, and fraction removes the given fraction of the accumulated filter-cake skin (1.0 cleans it completely). Cleaning may be scoped to one declared single-layer completion by its completion_i, completion_j and completion_k; filtrate concentration applies per well. These compose with the ps.wells.InjectionDamage(...) filter-cake declaration described above.

ps.wells.InjectedStream(well=..., temperature=..., pressure=...) states the injection-stream temperature (and optional pressure) for selected injectors when the model carries an active ps.thermal.Package(...); without a thermal package the model rejects the event with wells.injection_temperature.requires_thermal. Steam quality and specific enthalpy are not part of the semantic surface because the target simulator does not support them; the lowerer defaults those record slots.

ps.wells.PressureAveraging(...) states the block-average pressure calculation policy used when reporting the WBP, WBP4, WBP5, and WBP9 well vectors requested in ps.Summary as block_average_pressure, block_average_pressure_four_point, block_average_pressure_five_point and block_average_pressure_nine_point. inner_block_weight (0 to 1) weights the well block against its surrounding blocks, pore_volume_inner_weight=True selects pore-volume weighting of the inner and surrounding blocks instead, connection_factor_weight (0 to 1) blends connection-factor and pore-volume averaged pressures, depth_correction picks the hydrostatic correction ("wellbore", "reservoir", or "none"), and connections picks the connection set: "open_only" averages only the blocks of connections that are open, and "open_and_closed" averages every defined connection’s block, which keeps the reported pressure continuous across a connection closing. Leaving connections unstated is open-only, which is what the simulator does by default. Without well= the policy applies to every well; naming wells restricts it to them and overrides the field-wide policy for them. Block-average pressure policies are not applicable to radial or spider geometry (wells.pressure_averaging.radial_geometry).

ps.wells.PressureAveraging(well=..., reference_depth=...) overrides the reference depth used to report the averaged pressures for the selected wells, for example to match a gauge depth during history matching.

ps.wells.Survey(well=..., rft=..., plt=..., segments=...) requests downhole survey profiles on the simulator’s RFT file for the selected wells. Each profile takes a frequency: "once" writes the profile at the current report step, "at_report_steps" and "at_timesteps" keep writing it, and "on_first_open" (RFT profiles only) writes it when each well is first opened to flow. At least one profile must be requested.

import petroscript as ps
field = ps.Group(name="FIELD")
model = ps.Model(
name="SURVEY_CASE",
units="field",
start="2020-01-01",
grid=ps.grid.Cartesian(shape=(3, 3, 1), dx=400.0, dy=400.0, dz=40.0, top=2500.0),
rock=ps.Rock(porosity=0.25, permeability=100.0, vertical_ratio=1.0),
fluid=ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[14.7, 3500.0], oil_fvf=[1.06, 1.0],
oil_viscosity=[1.0, 1.2]),
water=ps.pvt.Water(reference_pressure=3000.0, fvf=1.0, compressibility=3.0e-6,
viscosity=0.5, viscosibility=0.0),
oil_surface_density=53.0,
water_surface_density=64.0,
),
relperm=[ps.relperm.OilWater(sw=[0.2, 0.8], krw=[0.0, 1.0], krow=[1.0, 0.0],
pcow=[0.0, 0.0])],
initial=ps.initial.Equilibrium(datum_depth=2510.0, datum_pressure=3000.0,
oil_water_contact=2600.0, gas_oil_contact=0.0),
groups=[field],
wells=[
ps.Well(name="PROD1", i=1, j=1, group=field),
ps.Well(name="INJ1", i=3, j=3, group=field),
],
summary=ps.Summary(
wells={"PROD1": ["block_average_pressure", "block_average_pressure_nine_point"]}
),
schedule=ps.Schedule(
report=[ps.Every(30.0)],
events=[
ps.Event(
"2020-01-01",
ps.Perforate(well="PROD1", k=(1, 1), diameter=0.3),
ps.Perforate(well="INJ1", k=(1, 1), diameter=0.3),
ps.Produce(well="PROD1", oil_rate=150.0, bhp_limit=1500.0),
ps.Inject(well="INJ1", phase="water", rate=300.0, bhp_limit=4500.0),
ps.wells.PressureAveraging(
inner_block_weight=0.5,
connection_factor_weight=1.0,
depth_correction="wellbore",
),
ps.wells.Survey(well="PROD1", rft="once", plt="once"),
),
],
),
)

ps.Numerics(...) describes bounded solver and timestep controls such as initial, maximum, and minimum timestep lengths, Newton/linear iteration limits, and mass-balance error targets: the model’s start settings in numerics=, and a re-tuning from a date when stated in a ps.Event. Two timestep ramp policies extend that intent: maximum_timestep_growth caps the factor by which a converged timestep may grow (a value greater than 1.0; use maximum_timestep for an absolute cap), and maximum_timestep_after_well_event caps the timestep length in days taken directly after a well event such as opening, closing, or retargeting a well. OPM Flow honors these caps when its documented tuning switch is active. The same settings state the solver policy: constrained_pressure_residual=True requests Flow’s pressure-residual preconditioner, and well_target_update=4 states the Newton iterations of a time step in which well and group targets update (NUPCOL). ps.Numerics(next_step=days, next_step_persists=True) in a ps.Event constrains upcoming timestep length without exposing record positions. Lowerers own sparse output placement and backend capacity cards.

ps.Numerics(maximum_segment_iterations=40, maximum_segment_solves=400) states the multisegment well solver controls (WSEGITER: the iteration ceiling, the re-solve count, and the segment_tolerance_reduction and segment_tolerance_growth factors on the segment residual tolerance). They are the model’s start numerics, stated once; the importer reads a deck’s WSEGITER into the same settings, because the output board measured the target recording the card in its INIT header.

import petroscript as ps
model = ps.Model(
name="TIMESTEP_POLICY",
units="field",
start="2020-01-01",
grid=ps.grid.Cartesian(shape=(2, 2, 1), dx=500.0, dy=500.0, dz=40.0, top=2500.0),
rock=ps.Rock(porosity=0.25, permeability=120.0, vertical_ratio=1.0),
fluid=ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[14.7, 3500.0], oil_fvf=[1.06, 1.0],
oil_viscosity=[1.0, 1.2]),
water=ps.pvt.Water(reference_pressure=3000.0, fvf=1.0, compressibility=3.0e-6,
viscosity=0.5, viscosibility=0.0),
oil_surface_density=53.0,
water_surface_density=64.0,
),
relperm=[ps.relperm.OilWater(sw=[0.2, 0.8], krw=[0.0, 1.0], krow=[1.0, 0.0],
pcow=[0.0, 0.0])],
initial=ps.initial.Equilibrium(datum_depth=2510.0, datum_pressure=3000.0,
oil_water_contact=2600.0, gas_oil_contact=0.0),
wells=[ps.Well(name="PROD1", i=1, j=1), ps.Well(name="INJ1", i=2, j=2)],
numerics=ps.Numerics(
maximum_timestep=30.0,
maximum_timestep_growth=1.5,
maximum_timestep_after_well_event=5.0,
),
schedule=ps.Schedule(
report=[ps.Every(30.0, 30.0)],
events=[
ps.Event(
"2020-01-01",
ps.Perforate(well="PROD1", k=(1, 1), diameter=0.3),
ps.Perforate(well="INJ1", k=(1, 1), diameter=0.3),
ps.Produce(well="PROD1", oil_rate=150.0, bhp_limit=1200.0),
ps.Inject(well="INJ1", phase="water", rate=300.0, bhp_limit=4500.0),
),
],
),
)

Bounded group policies model SPE9-style operating logic without exposing raw deck records. Each policy is one dated change that names its group and states everything else by keyword. ps.groups.ProductionTarget(group=group, ...) states the production target (oil_rate, water_rate, gas_rate, liquid_rate, reservoir_volume, or control="parent") together with the rate-limit ceilings (oil_rate_limit and its siblings) and the on_violation actions; ps.groups.InjectionTarget(group=group, phase=...) states an injection target (rate, reservoir_volume, reinjection_fraction, or voidage_replacement) with control, guide_rate/guide_basis, and the reinjection_source_group/voidage_source_group source groups; ps.groups.Economics(group=group, ...) states the group economic limits; ps.groups.PressureMaintenance(group=group, intent=..., region=..., basis=..., pressure=...) governs pressure maintenance, and intent="disabled" stops it; ps.groups.GasSales and ps.groups.FuelGas(group=group, intent="rate" | "fraction_of_produced", ...) govern gas disposition; ps.groups.GuideRates(...) states the field guide-rate formula. ps.groups.Efficiency(group=group, factor=...) applies multiplicative group efficiency factors. Strict internal schedule events remain the compiler-facing lowering substrate.

top = ps.Group(name="F")
group = ps.Group(name="A", parent=top)
events = (
ps.groups.ProductionTarget(group=top, liquid_rate=1500.0),
ps.groups.Efficiency(group=group, factor=0.85),
ps.groups.InjectionTarget(
group=group,
phase="water",
voidage_replacement=1.0,
voidage_source_group=top,
),
)

DissolutionLimits (ps.pvt), with maximum_rate and scope, bounds how fast the dissolved gas-oil ratio may rise per day in a cell: one rate for every cell (DRSDT), or one per PVT region as {region: rate}, regions numbered from 1 (DRSDTR). scope is "all" or "free" (only cells holding free gas); None states none, which Flow reads as "all". Stated in the start’s ps.Event it is written at the schedule’s head; in a later ps.Event, at its date.

ps.aquifers.Fetkovich(...), ps.aquifers.CarterTracy(...) with its influence_table=, ps.aquifers.Numerical(...) with its cells=, and ps.aquifers.ConstantFlux(...) represent water support as reservoir-domain objects in the model’s aquifers=, each with the connections= it reaches. A ps.aquifers.Connection names a ps.Box of one-based cell ranges and the face it crosses ("plus_i", "minus_k" and so on); users describe finite volume, analytical influence data, numerical aquifer cells, or prescribed flux policy instead of writing backend records. ps.aquifers.Flux(aquifer=..., water_flux_rate=...) in a ps.Event changes a constant-flux aquifer’s prescribed rate from that date, naming the aquifer by its number. Aquifer numeric fields are finite scalars only; target-specific defaults should not be smuggled through strings.

Facility-scale authoring keeps keyword-heavy behavior behind semantic objects. Users describe the reservoir intent and the compiler owns sparse keyword layout:

  • A ps.groups.Satellite states a satellite group’s rates from its date, stated rather than simulated: its production by oil_rate=, water_rate=, gas_rate=, reservoir_volume= and lift_gas_rate= (GSATPROD), or its injection of one phase by phase= and rate= (GSATINJE); group= is the satellite’s Group handle.
  • ps.lift.ProductionTable and ps.lift.InjectionTable model table axes and values in the model’s lift_tables=, and a control’s lift_table= binds one to a well. An axis kind is one of its listed words, in lower case or in capitals, and any other word is refused.
  • ps.groups.Network, ps.groups.Node, and ps.groups.Branch model a narrow flow network with group-backed nodes, typed branches and disabled or no-pressure-loss routes, in the model’s network=; ps.Numerics’ network fields state its balancing (NETBALAN).
  • ps.wells.Efficiency and ps.groups.Efficiency apply typed well and group efficiency factors, and their network_propagation ("apply_factor" or "ignore_factor") says whether the network sees them, without asking users to spell item positions.
  • ps.groups.GasLiftOptimization models field/group lift optimization knobs, while ps.groups.GasLift enables or disables lift optimization for one named well.

These APIs are meant to be written from word problems such as “route producer group A through this platform node” or “enable gas-lift optimization for this producer with this maximum lift gas rate.” Tests may assert emitted keywords at the compiler boundary, but examples should not ask users to construct keyword cards or raw row tuples.

Multisegment wells, trajectories, devices, and skin

Section titled “Multisegment wells, trajectories, devices, and skin”

This authoring is semantic: users describe wellbore topology, completion mapping, devices, trajectories, and skin policies with reservoir-domain values. They do not construct a keyword card, row tuple, or public backend wrapper.

  • ps.wells.Segments(well=..., segments=[...], connections=[...]) states one well’s segment tree, each segment a row naming its number, branch, parent, length and depth, and which segment each single-layer connection joins. The compiler emits multisegment dimensions and segment topology from it.
  • ps.wells.Valve, ps.wells.ICD and ps.wells.AICD attach active flow-control devices to a segment of a scheduled tree, named by the well and the segment’s number (well="P1", segment=12). Use typed device/status concepts instead of magic strings or keyword rows.
  • ps.wells.Trajectory, in a well’s trajectories=, maps measured-depth trajectory samples to completions along one branch.
  • ps.wells.Skin changes the skin factor of a well’s connections in a run of layers (well="P1", i=10, j=10, k=(1, 3), skin_factor=2.5); an axis left out selects every connection along it.

The current lowerers own the emitted WSEGDIMS, WELSEGS, COMPSEGS, COMPORD, COMPLUMP, WSEGVALV, WSEGAICD, WSEGSICD, WELTRAJ, COMPTRAJ, and CSKIN records. Tests may assert those compiler outputs at the boundary, but user-facing examples should read like “connect this completion to this segment and shut this declared completion,” not like simulator row authoring.

Where the completion-ordering card is written

Section titled “Where the completion-ordering card is written”

COMPORD is not free to sit anywhere the keyword is legal. The simulator reads it once per schedule block, out of the block that declares the well, and applies the last record whose name pattern matches. Two consequences the lowerer is built around:

  • A row written at the schedule head is invisible to a well introduced after a time card. The card has to ride the block that declares its well, between that well’s WELSPECS and its COMPDAT.
  • A block that declares several wells gets one card carrying all of their rows. A second card in the same block is never read, so every well named only there silently falls back to the default ordering.

Wells declared before the first time card share the schedule head’s block, so their rows travel on the head’s card rather than a second one.

Note also that the directive is not consulted at all for a multisegment well: such a well’s connection order is the record sequence of its COMPSEGS block.

A segment tree is a statement with a position, the same way a completion block is. A model states one either in its schedule head — the tree a well is introduced with — or on its timeline, where it takes effect from the position it stands at and not before. Both spellings write the same WELSEGS/COMPSEGS records; what differs is where in the deck they land, and the same is true of the WSEGVALV/WSEGAICD/WSEGSICD cards that hang equipment on a tree.

An imported deck is read this way too: a table or an equipment card the deck states after its first time card is carried at that position. The exception is the block that DECLARES the well — a well introduced after a time card takes its tables inside its own block, after the COMPDAT that creates the connections they name, for the same reason the ordering directive rides there.

A well is segmented because a tree names it, wherever the tree stands: the segmented spelling of its WELSPECS record, the WSEGDIMS maxima and the segment summary vectors all read every tree the model states. WSEGDIMS item 1 counts the wells those trees name, so a model holding two trees for one well — the tree it is introduced with and the tree an event gives it later — still declares one multi-segment well.

Well coordinates and completion intervals are one-based grid indices, not Python zero-based indices. Reference depth is a depth in the active unit system. Completion diameter is a length. Rate and pressure control values use the active unit system unless wrapped in explicit quantity helpers. Schedule controls, group rates, group efficiency factors, and well target events share the same strict scalar contract. Solver tuning and next-step limits use positive finite day/scalar values. Economic fractions such as water-cut limits use simulator native fractions, not percentages, unless wrapped by a domain-specific helper in future.

Common mistakes are using zero-based cell indices, placing wells outside the grid, reversing the two layers of k, mixing injector phase names, passing numeric strings, and assuming the order of wells is irrelevant. Model.wells is ordered; the compiler preserves that order when lowering well specifications and controls.

Current status: emitted now for the implemented simple well/schedule path.

  • WELSPECS is emitted from each ps.Well. A well that states no bhp_reference_depth renders item 5 as the default 1*; the simulator derives the datum, and PetroScript does not restate a value it would derive (see Wells).
  • COMPDAT is emitted from their ps.Perforate changes.
  • WCONPROD is emitted from ps.Produce controls.
  • WCONINJE is emitted from ps.Inject controls. The injection phase keeps the word the deck spelled it with. Both WCONINJE and GCONINJE accept two words for water; the reader takes both and keeps the one the deck wrote, so a deck writing WAT gets WAT back. Inside an ACTIONX rule body that is not a formatting choice — the target stores the body’s card TEXT in the restart file, so on opm-tests/actionx/ACTIONX_GCONINJE the one extra character shifts the rest of the stored line and moves four UNRST:ZLACT entries. A model that never stated a word still derives the long one.
  • WCONHIST and WCONINJH are emitted from semantic history-mode controls.
  • WELOPEN is emitted from well lifecycle status events. The record’s scope is part of the statement: defaulted coordinates address the WELL, coordinates stated as 0 0 0 address every CONNECTION, and stated coordinates address one completion. A connection-scoped record also carries the completion-NUMBER range in items 6-7 when the deck stated one, which bounds it to the connections those completions name; a record that stated no range renders without one.
  • WTEST, WECON, GECON, and WGRUPCON are emitted from well-test, economic-limit, workover, and guide-rate policies.
  • WPIMULT, WTMULT, WINJMULT, WDFACCOR, WINJDAM, and WCYCLE are emitted from multiplier, injection-damage, and cycling events.
  • WLIST is emitted from declared named well lists; list-backed selections lower to the named-list reference in well event records.
  • WINJCLN and WINJFCNC are emitted from filter-cake cleaning and filtrate concentration policies.
  • WINJTEMP is emitted from injection-stream temperature policies in thermal models.
  • WPAVE, WWPAVE, and WPAVEDEP are emitted from block-average pressure policies and per-well reference-depth overrides.
  • WRFTPLT is emitted from downhole RFT/PLT/segment survey requests.
  • RPTSCHED, RPTRST, and DRSDT are emitted by the current schedule lowerer for initial and event-level report/restart and gas-solution timestep behavior.
  • DRSDTR is emitted from DissolutionLimits.
  • AQUDIMS, AQUFETP, AQUCT, AQUTAB, AQUNUM, AQUCON, AQUANCON, and AQUFLUX are emitted internally from semantic aquifer objects and flux events.
  • GRUPTREE, GCONPROD, GEFAC, and GCONINJE are emitted from bounded group objects and group schedule events.
  • GPMAINT, GSATINJE, and GSATPROD are emitted from pressure-maintenance and facility-allocation policies.
  • VFPPROD, VFPINJ, WVFPDP, and WVFPEXP are emitted from structured VFP tables and well/table attachments.
  • NETWORK, BRANPROP, NODEPROP, GRUPNET, and NETBALAN are emitted from semantic network models and balancing policies.
  • LIFTOPT, GLIFTOPT, and WLIFTOPT are emitted from gas-lift optimization policies.
  • NEXTSTEP is emitted from a dated ps.Numerics(next_step=...).
  • TSTEP is emitted from the schedule’s report steps.
  • END is emitted at the end of the generated deck.

Behavior beyond the group, facility, lift-table, network and gas-lift statements and the multisegment, trajectory, completion-skin and segment-device statements above remains future/unsupported.

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