Skip to content

Control wells, groups and networks

A well is an identity: ps.Well(name=, i=, j=) names it and puts its head in a grid column. What the well does is dated: its perforations, its control and its status are changes the schedule states at a date, in a ps.Event(date, ...) or with m.at(date, ...).

In ps.Produce and ps.Inject the one quantity given bare is the target; every other quantity is a limit, named *_limit. The well holds its target while every limit holds, and switches to the limit that binds.

WellStatementTargets (one, bare)LimitsUnits (FIELD, METRIC)
Producerps.Produce(well=, ...)oil_rate, water_rate, liquid_rate, gas_rate, reservoir_volume, bhp, thpoil_rate_limit, water_rate_limit, liquid_rate_limit, gas_rate_limit, reservoir_volume_limit, bhp_limit, thp_limitstb/day and Mscf/day, sm3/day; rb/day, rm3/day; psia, barsa
Injectorps.Inject(well=, phase=, ...)rate (of the phase), reservoir_volume, bhprate_limit, reservoir_volume_limit, bhp_limit, thp_limitas above
Under a groupeither, with control="group"none: the group sets itevery quantity statedas above
History matchps.History(well=, ..., control=)the observed rate control= names, or with control="bottom_hole_pressure" the observed bhp_limitbhp_limit is the observed BHP: under a rate control it is reported, not a limitas above

A THP target or limit needs a lift table of the well’s kind: a producer’s lift_table= names a ps.lift.ProductionTable (a hand-built one, or one NeqSim makes), an injector’s a ps.lift.InjectionTable. A production table on an injector is refused: Inject: lift_table: Input should be a valid dictionary or instance of InjectionTable.

Changing a control later:

  • ps.Adjust(well=, oil_rate=...) changes one target or limit and keeps the rest.
  • A new ps.Produce or ps.Inject replaces the whole control: a limit it does not restate is gone.
  • ps.Shut closes the well at the surface, ps.Stop stops it but leaves it open to the reservoir, ps.Open reopens it.
import petroscript as ps
field = ps.Group(name="FIELD")
north = ps.Group(name="NORTH", parent=field)
south = ps.Group(name="SOUTH", parent=field)
wells = [ps.Well(name="N1", i=2, j=4, group=north), ps.Well(name="S1", i=4, j=1, group=south),
ps.Well(name="I1", i=1, j=1, group=field)]
m = ps.Model(
name="WELLS", units="metric", start="2025-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=200.0, vertical_ratio=0.1,
compressibility=5.0e-5, reference_pressure=200.0),
fluid=ps.fluid.OilWater(
oil=ps.pvt.DeadOil(pressure=[50.0, 300.0], oil_fvf=[1.1, 1.08],
oil_viscosity=[2.0, 2.2]),
water=ps.pvt.Water(reference_pressure=200.0, fvf=1.01, compressibility=4.5e-5,
viscosity=0.4, viscosibility=0.0),
oil_surface_density=850.0, water_surface_density=1020.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=2000.0, datum_pressure=200.0,
oil_water_contact=2030.0),
groups=[field, north, south],
wells=wells,
schedule=ps.Schedule(end="2026-01-01", report="monthly", events=[ps.Event(
"2025-01-01",
*(ps.Perforate(well=w.name, k=(1, 2), diameter=0.2) for w in wells),
ps.Produce(well="N1", control="group", oil_rate_limit=300.0, bhp_limit=80.0),
ps.Produce(well="S1", oil_rate=200.0, bhp_limit=80.0), # its own target
ps.Inject(well="I1", phase="water", rate=400.0, bhp_limit=300.0),
ps.groups.ProductionTarget(group=north, oil_rate=150.0), # NORTH's wells share it
)]),
)
m = m.at("2025-06-01", ps.Adjust(well="S1", oil_rate=150.0)) # keeps bhp_limit 80
m = m.at("2025-09-01", ps.Shut(well="S1"))
now = m.control("S1", at="2025-07-01") # what is in force
assert isinstance(now, ps.Produce) and now.oil_rate == 150.0

What goes wrong:

  • A well with no control: [well.control.missing]. A new well and its control go in one m.set (edit a loaded deck).
  • No target: [control.target.count] ps.Produce states no target.
  • An injector’s rate as water_rate=: ps.Inject takes no water_rate=: an injector's rate is rate=.
  • A date past the run’s end: [schedule.date.outside_run]; m = m.until(date) first.

ps.Group(name=, parent=) builds the group tree; a well joins one with ps.Well(group=). A group target (ps.groups.ProductionTarget(group=, oil_rate=...)) is shared among the wells under it that are on control="group". A group’s results are asked for like any other: ps.Summary(groups={"NORTH": ["oil_production_rate"]}). examples/group_tree.py runs a two-level tree in Flow.

A production network (ps.groups.Network) is nodes (ps.groups.Node) joined by pipelines (ps.groups.Branch(from_node=, to_node=)), each branch holding the node objects themselves. So a node cannot change alone: replacing it leaves the branches pointing at the old node (InputError: Network: NetworkModel branches must reference declared nodes). Rebuild the branches with the new node, and set the whole network:

deck = ps.load("opm-tests/model5/5_NETWORK_MODEL5_STDW.DATA") # METRIC, PLAT-A at 21 bar
network = deck.get("network")
new = {"PLAT-A": next(n for n in network.nodes if n.name == "PLAT-A")
.replace(terminal_pressure=25.0)}
def swap(node):
return new.get(node.name, node)
deck = deck.set("network", network.replace(
nodes=[swap(n) for n in network.nodes],
branches=[b.replace(from_node=swap(b.from_node), to_node=swap(b.to_node))
for b in network.branches],
))
assert [n.terminal_pressure for n in deck.get("network").nodes][0] == 25.0

Addresses below network read (m.get("network/nodes/1/terminal_pressure")), but setting a node there is refused the same way, since the branches still hold the old node: rebuild the branches as above. ps.help(ps.groups.Network) and the facilities reference have the rest.

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