Wells and schedule
Reservoir meaning
Section titled “Reservoir meaning”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), ), ], ),)PetroScript API
Section titled “PetroScript API”Completion
Section titled “Completion”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
Section titled “Well controls”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.
Add a well to a model you hold
Section titled “Add a well to a model you hold”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.
Producer
Section titled “Producer”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.
Injector
Section titled “Injector”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.
Schedule
Section titled “Schedule”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(...).
Schedule report controls
Section titled “Schedule report controls”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.
Selecting wells
Section titled “Selecting wells”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", ),)History tables
Section titled “History tables”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.
Well lists
Section titled “Well lists”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).
Injection-stream maintenance
Section titled “Injection-stream maintenance”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.
Pressure averaging and downhole surveys
Section titled “Pressure averaging and downhole surveys”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"), ), ], ),)Solver tuning and next-step limits
Section titled “Solver tuning and next-step limits”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), ), ], ),)Group schedule events
Section titled “Group schedule events”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
Section titled “DissolutionLimits”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.
Aquifer objects and flux events
Section titled “Aquifer objects and flux events”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.
Facilities
Section titled “Facilities”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.Satellitestates a satellite group’s rates from its date, stated rather than simulated: its production byoil_rate=,water_rate=,gas_rate=,reservoir_volume=andlift_gas_rate=(GSATPROD), or its injection of one phase byphase=andrate=(GSATINJE);group=is the satellite’sGrouphandle. ps.lift.ProductionTableandps.lift.InjectionTablemodel table axes and values in the model’slift_tables=, and a control’slift_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, andps.groups.Branchmodel a narrow flow network with group-backed nodes, typed branches and disabled or no-pressure-loss routes, in the model’snetwork=;ps.Numerics’ network fields state its balancing (NETBALAN).ps.wells.Efficiencyandps.groups.Efficiencyapply typed well and group efficiency factors, and theirnetwork_propagation("apply_factor"or"ignore_factor") says whether the network sees them, without asking users to spell item positions.ps.groups.GasLiftOptimizationmodels field/group lift optimization knobs, whileps.groups.GasLiftenables 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.ICDandps.wells.AICDattach 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’strajectories=, maps measured-depth trajectory samples to completions along one branch.ps.wells.Skinchanges 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
WELSPECSand itsCOMPDAT. - 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.
Where a segment table is written
Section titled “Where a segment table is written”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.
Units and shapes
Section titled “Units and shapes”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.
Validation and common mistakes
Section titled “Validation and common mistakes”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.
Lowers to
Section titled “Lowers to”Current status: emitted now for the implemented simple well/schedule path.
WELSPECSis emitted from eachps.Well. A well that states nobhp_reference_depthrenders item 5 as the default1*; the simulator derives the datum, and PetroScript does not restate a value it would derive (see Wells).COMPDATis emitted from theirps.Perforatechanges.WCONPRODis emitted fromps.Producecontrols.WCONINJEis emitted fromps.Injectcontrols. The injection phase keeps the word the deck spelled it with. BothWCONINJEandGCONINJEaccept two words for water; the reader takes both and keeps the one the deck wrote, so a deck writingWATgetsWATback. Inside anACTIONXrule body that is not a formatting choice — the target stores the body’s card TEXT in the restart file, so onopm-tests/actionx/ACTIONX_GCONINJEthe one extra character shifts the rest of the stored line and moves fourUNRST:ZLACTentries. A model that never stated a word still derives the long one.WCONHISTandWCONINJHare emitted from semantic history-mode controls.WELOPENis emitted from well lifecycle status events. The record’s scope is part of the statement: defaulted coordinates address the WELL, coordinates stated as0 0 0address 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, andWGRUPCONare emitted from well-test, economic-limit, workover, and guide-rate policies.WPIMULT,WTMULT,WINJMULT,WDFACCOR,WINJDAM, andWCYCLEare emitted from multiplier, injection-damage, and cycling events.WLISTis emitted from declared named well lists; list-backed selections lower to the named-list reference in well event records.WINJCLNandWINJFCNCare emitted from filter-cake cleaning and filtrate concentration policies.WINJTEMPis emitted from injection-stream temperature policies in thermal models.WPAVE,WWPAVE, andWPAVEDEPare emitted from block-average pressure policies and per-well reference-depth overrides.WRFTPLTis emitted from downhole RFT/PLT/segment survey requests.RPTSCHED,RPTRST, andDRSDTare emitted by the current schedule lowerer for initial and event-level report/restart and gas-solution timestep behavior.DRSDTRis emitted fromDissolutionLimits.AQUDIMS,AQUFETP,AQUCT,AQUTAB,AQUNUM,AQUCON,AQUANCON, andAQUFLUXare emitted internally from semantic aquifer objects and flux events.GRUPTREE,GCONPROD,GEFAC, andGCONINJEare emitted from bounded group objects and group schedule events.GPMAINT,GSATINJE, andGSATPRODare emitted from pressure-maintenance and facility-allocation policies.VFPPROD,VFPINJ,WVFPDP, andWVFPEXPare emitted from structured VFP tables and well/table attachments.NETWORK,BRANPROP,NODEPROP,GRUPNET, andNETBALANare emitted from semantic network models and balancing policies.LIFTOPT,GLIFTOPT, andWLIFTOPTare emitted from gas-lift optimization policies.NEXTSTEPis emitted from a datedps.Numerics(next_step=...).TSTEPis emitted from the schedule’s report steps.ENDis 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