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, ...).
Controls: one target, the rest limits
Section titled “Controls: one target, the rest limits”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.
| Well | Statement | Targets (one, bare) | Limits | Units (FIELD, METRIC) |
|---|---|---|---|---|
| Producer | ps.Produce(well=, ...) | oil_rate, water_rate, liquid_rate, gas_rate, reservoir_volume, bhp, thp | oil_rate_limit, water_rate_limit, liquid_rate_limit, gas_rate_limit, reservoir_volume_limit, bhp_limit, thp_limit | stb/day and Mscf/day, sm3/day; rb/day, rm3/day; psia, barsa |
| Injector | ps.Inject(well=, phase=, ...) | rate (of the phase), reservoir_volume, bhp | rate_limit, reservoir_volume_limit, bhp_limit, thp_limit | as above |
| Under a group | either, with control="group" | none: the group sets it | every quantity stated | as above |
| History match | ps.History(well=, ..., control=) | the observed rate control= names, or with control="bottom_hole_pressure" the observed bhp_limit | bhp_limit is the observed BHP: under a rate control it is reported, not a limit | as 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.Produceorps.Injectreplaces the whole control: a limit it does not restate is gone. ps.Shutcloses the well at the surface,ps.Stopstops it but leaves it open to the reservoir,ps.Openreopens 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 80m = m.at("2025-09-01", ps.Shut(well="S1"))now = m.control("S1", at="2025-07-01") # what is in forceassert isinstance(now, ps.Produce) and now.oil_rate == 150.0What goes wrong:
- A well with no control:
[well.control.missing]. A new well and its control go in onem.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.
Groups
Section titled “Groups”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.
Networks
Section titled “Networks”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 barnetwork = 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.0Addresses 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