Skip to content

Add get_schedule() to the Python simulators module - #7500

Draft
hakonhagland wants to merge 7 commits into
OPM:masterfrom
hakonhagland:python_get_schedule
Draft

hakonhagland wants to merge 7 commits into
OPM:masterfrom
hakonhagland:python_get_schedule

Conversation

@hakonhagland

@hakonhagland hakonhagland commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Add get_schedule() to the Python simulators

The Python simulators give no access to the Schedule object the simulator itself uses. A Schedule built in Python is a separate object, so changing it, for example with shut_well(), has no effect on a simulator constructed from a file name. The only way to control wells from Python has been the constructor that takes Deck, EclipseState, Schedule and SummaryConfig objects. In a parallel run that constructor works only if None is passed for the EclipseState, which is not documented anywhere. OPM/opm-python-documentation#37 documents exactly that workaround for running in parallel with mpi4py.

This PR adds get_schedule(), which returns the simulator's own Schedule after step_init(). Well controls can then be changed with the filename constructor:

sim = BlackOilSimulator(filename="SPE1CASE1.DATA")
sim.setup_mpi(init=False, finalize=False)
sim.step_init()
sim.get_schedule().shut_well("PROD", 3)   # on every MPI process

It also rewrites the docstring of the objects constructor, which did not say what the simulator does with each of its four arguments.

This branch is based on #7444 and contains its commit. Please review that one there; this PR adds the commits after it.

Add get_schedule() to the Python simulators (commit 1)

  • Main::schedulePtr(): a public getter for the std::shared_ptr<Schedule> that Main already holds and passes on to the vanguard. This is the object the simulation uses.
  • PyBaseSimulator::getSchedule(), bound as get_schedule() in all three simulator classes. It hands over the existing shared pointer, so Python and the simulator share one object. With the objects constructor it returns the very object that was passed in. Calling it before step_init() raises RuntimeError, as step() does.
  • Imports opm.io.schedule before returning. The Python type for Schedule is registered by opm-common's module. Without the import, pybind11 cannot convert the return value when the script has not imported opm.io itself, and every process fails with TypeError: Unregistered type : Opm::Schedule.
  • Docstring: in a parallel run every process holds its own copy of the Schedule, so a change must be made in the same way on every process.
  • New test python/test/test_get_schedule.py, registered as the ctest case python_get_schedule. It checks that get_schedule() raises before step_init(), that a well shut through it is shut in the simulation (FOPR is zero from that report step on), and that with the objects constructor it returns the object that was passed in.

Document that the Deck argument is used only for its file name (commit 2)

  • Adds a sentence to the Deck parameter of the objects constructor: the simulator reads the DATA file again itself, so changes made to the Deck object in Python do not reach the simulation. Commit 3 makes this more precise.

Say what each objects-constructor argument is used for (commit 3)

  • Deck: only its file name is used, and only when EclipseState is None. Otherwise the simulator takes the file name from the EclipseState.
  • EclipseState: None is recommended. When given, the simulator uses it instead of building its own, and takes the name of the DATA file to read from it. In a parallel run it must be None. (Commit 5 rewords this.)
  • Schedule and SummaryConfig: used as they are, so a schedule or a summary configuration built or changed in Python takes effect. From step_init() on, the given Schedule is the object get_schedule() returns.
  • Parallel runs: every change made from Python must be made in the same way on every process, both before and after step_init().
  • Points to the filename constructor together with get_schedule() as the simpler way to change well controls.
  • The signature now shows EclipseState: EclipseState | None.

Run the objects-constructor test from its own copy of the deck (commit 4)

  • Addresses the Copilot review. test_03_objects_constructor ran in the deck directory that test_schedule.py also uses. With an EclipseState given, the simulator clears old result files, and writes its .PRT and .DBG files, in the directory of the deck the EclipseState was built from, even when --output-dir names another one. Run concurrently, the two tests could delete each other's output. The test now copies the deck into a directory of its own.

Describe a given EclipseState as used, not as unable to carry changes (commit 5)

  • Addresses the Copilot review. A given EclipseState is used as it is, including its grid and properties, so the docstring no longer says that everything except the Schedule and SummaryConfig is read from the DATA file again. Instead of claiming that the state cannot carry changes made in Python, it now says that the state is used as given and should be built from the same DATA file.

Keep the get_schedule() docstring neutral about how to change it (commit 6)

Verification

  • Wrote test_get_schedule.py first; all three tests failed with AttributeError before commit 1. All 8 python_* ctest cases pass after it, and after commit 6.
  • After commit 4, a run of python_get_schedule leaves no result or log files in the shared deck directory; the test's own copy gets them all.
  • On 4 MPI processes, SPE1CASE1, filename constructor, with get_schedule().shut_well("PROD", 3) called on every process: WOPR:PROD per report step is 20000, 20000, 20000, 0, 0, 0. Without the call it stays at 20000. The same result with a script that never imports opm.io before calling get_schedule(), which is what failed before the import was added.
  • A well shut at report step 0 through get_schedule() right after step_init() is shut from the first report step, on 1 and on 4 processes.
  • Checked each claim of the new docstring by running the simulator:
    • a Deck parsed from a copy named OTHER.DATA, together with an EclipseState from SPE1CASE1.DATA, makes the simulator read and write SPE1CASE1; with None for the state it reads and writes OTHER;
    • a Schedule built from the deck text with the oil rate target of PROD changed from 20000 to 10000 gives WOPR:PROD 10000 at every step, on 1 and on 4 processes;
    • a SummaryConfig built from the deck text with FOPT added makes FOPT appear in the summary file;
    • a keyword added with Deck.add() does not change the EclipseState built from that deck, because it lands at the end of the deck, in the SCHEDULE section.

Follow-ups

Two further PRs build on this one:

  • an early, clear error when an EclipseState object is passed in a parallel run, which today aborts while the grid is being distributed;
  • an error when the Schedule differs between the MPI processes, either before step_init() or before a report step. Today a change made on only some of the processes is silently ignored.

The two constructors in docstrings_simulators.json documented their
arguments as

  __init__(deck_filename: str)
  __init__(deck: Deck, state: EclipseState, schedule: Schedule,
           summary_config: SummaryConfig)

but the bindings in python/simulators/Py*Simulator.cpp name them
filename, and Deck, EclipseState, Schedule, SummaryConfig. Passing the
arguments by keyword as documented raises TypeError; only positional
calls worked. OPM#7439 corrected the same kind of mismatch for four
methods, but did not cover the constructors.

Use the bound names in the signatures and in the :param: entries. The
documentation follows the bindings rather than the other way round,
since renaming the bound arguments would break existing keyword calls
such as BlackOilSimulator(filename=...).

Also document the optional args parameter that both constructors take.
It was not mentioned at all. It holds extra command-line options for
the simulator, which are added after the program and deck names and
applied by step_init().
The simulators had no way to give Python the Schedule object they use.
A Schedule built in Python is a different object, so changing it, for
example with shut_well(), has no effect on a simulator built from a
file name. The only way to control wells from Python was the objects
constructor, which in a parallel run works only when None is passed
for the EclipseState.

get_schedule() returns the simulator's own Schedule after step_init(),
as the shared pointer the simulator holds, so Python and the simulator
share one object. With the objects constructor it returns the object
that was passed in. It imports opm.io.schedule itself, since otherwise
pybind11 cannot convert the return value when the script has not
imported opm.io.

In a parallel run each rank holds its own copy of the Schedule, so a
change must be made on every rank; the docstring says so.
The objects constructor passes only the Deck's file name on to the
simulator, which then reads the DATA file again. Changes made to the
Deck object in Python therefore never reach the simulation. Say so in
the constructor docstring.
The docstring listed four objects without saying what the simulator
does with them, and the sentence about the Deck was too generous. In
fact:

- the Deck is used only for its file name, and only when EclipseState
  is None; otherwise the file name comes from the EclipseState;
- the EclipseState only saves the simulator building its own, and it
  cannot carry changes made in Python (its Python API is read-only); in
  a parallel run it must be None;
- the Schedule and the SummaryConfig are used as they are, so changes
  made to them in Python take effect.

Say so per argument, add the rule that in a parallel run every change
must be made on every process, before and after step_init(), and point
to the filename constructor plus get_schedule() as the simpler way to
change well controls.
@hakonhagland hakonhagland added the manual:irrelevant This PR is a minor fix and should not appear in the manual label Oct 6, 2026
@hakonhagland

Copy link
Copy Markdown
Contributor Author

jenkins build this please

@hakonhagland
hakonhagland requested a balanced review from Copilot October 6, 2026 10:49
@hakonhagland
hakonhagland marked this pull request as draft October 6, 2026 10:50
@hakonhagland

Copy link
Copy Markdown
Contributor Author

Putting this in draft mode until #7444 has been merged

@bska

bska commented Oct 6, 2026

Copy link
Copy Markdown
Member

Just a heads up: I appreciate the reasoning, but please know that once I get OPM/opm-common#5371 implemented, this isn't going to be very useful. Any operation that's currently based on using the Schedule object and a report step index will be superseded by direct ScheduleState access.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new objects-constructor test shares output paths with another test, allowing concurrent runs to delete or overwrite active simulation files.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds access to the simulator’s shared Schedule from Python, enabling well-control changes with the filename constructor.

Changes:

  • Exposes get_schedule() in all three simulator classes after initialization.
  • Documents constructor argument handling and MPI usage.
  • Adds tests for initialization checks, schedule identity, and well-control effects.
File Description
python/​test/​test_get_schedule.py Tests schedule access and simulation effects.
python/​simulators/​PyOnePhaseSimulator.cpp Binds get_schedule().
python/​simulators/​PyGasWaterSimulator.cpp Binds get_schedule().
python/​simulators/​PyBlackOilSimulator.cpp Binds get_schedule().
python/​simulators/​CMakeLists.txt Registers the new Python tests.
python/​docstrings_simulators.json Documents schedule access and constructor behavior.
opm/​simulators/​flow/​python/​PyBaseSimulator.hpp Declares the shared-schedule getter.
opm/​simulators/​flow/​python/​PyBaseSimulator_impl.hpp Implements guarded access and Python type registration.
opm/​simulators/​flow/​Main.hpp Exposes the simulation’s shared Schedule pointer.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/test/test_get_schedule.py Outdated
Comment thread python/docstrings_simulators.json Outdated
@hakonhagland

hakonhagland commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Any operation that's currently based on using the Schedule object and a report step index will be superseded by direct ScheduleState access.

@bska Thanks for the heads-up, that is useful to know before going further. I read through OPM/opm-common#5371, and I agree that the examples in this PR, shut_well(name, step) and friends, are exactly the report-step-indexed operations it will supersede. Those are opm-common's existing Python bindings, though, and as far as I can see they would need a new form after OPM/opm-common#5371 whether or not this PR goes in.

What this PR adds is narrower: a way for Python to reach the Schedule instance the simulator actually uses. At the moment a script can only change a Schedule it built itself and passed to the constructor, which in parallel only works with None for the EclipseState (see OPM/opm-python-documentation#37). After OPM/opm-common#5371 the Schedule object would still own the ScheduleDeck and the current ScheduleState, so I would expect Python to still need this access point, with different operations on it, much as PYACTION needs some way to change the schedule during a run.

So my question is whether you see get_schedule() itself as going away, or mainly the step-indexed operations on what it returns. If it is the latter, would you be OK with merging this as an interim step, with the docstring wording kept neutral about the step-indexed calls, and adapting the Python side together with OPM/opm-common#5371? If you would rather wait, I will keep it in draft. Is there a rough timeline for OPM/opm-common#5371?

test_03_objects_constructor ran in the deck directory that
test_schedule.py also uses. With an EclipseState given, the simulator
clears old result files, and writes its .PRT and .DBG files, in the
directory of the deck that the EclipseState was built from, even when
--output-dir names another directory. Run concurrently, either test
could therefore delete the other's output. Copy the deck into a
directory of its own and run the test there.
The objects-constructor docstring said that everything except the
Schedule and SummaryConfig is read from the DATA file again, and that
the EclipseState cannot carry changes made in Python. A given
EclipseState is in fact used as it is, including its grid and
properties. Say that, and that it should be built from the same DATA
file, instead of claiming what it cannot do.
@bska

bska commented Oct 8, 2026

Copy link
Copy Markdown
Member

So my question is whether you see get_schedule() itself as going away, or mainly the step-indexed operations on what it returns.

Once I have OPM/opm-common#5371, about the only thing you'll be able to meaningfully do to the Schedule object as a whole, at least from Python, is retrieve the "current" ScheduleState. I'm also hoping to limit the amount of Python-level operations defined as "construct keyword text and apply that to the current state", but that's a (much) bigger task.

I sadly don't have a (good) timeline for OPM/opm-common#5371. It probably won't be ready for testing for several weeks yet, end of October at the earliest. I'm working on it in the background in addition to reviewing other PRs and my release manager work for the upcoming 2026.10 release.

The docstring named shut_well() and per-report-step changes as the way
to use the returned Schedule. Those step-indexed operations belong to
opm-common's Schedule bindings and are expected to change. Only say
that changes made to the returned object take effect in the simulation.
@hakonhagland

hakonhagland commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

the only thing you'll be able to meaningfully do to the Schedule object as a whole, at least from Python, is retrieve the "current" ScheduleState

@bska Thanks, that is clear. So after OPM/opm-common#5371, get_schedule() is still how a script reaches the simulator's own Schedule, and from there the current ScheduleState; what changes is what one can do with it. I have made the get_schedule() docstring neutral about how the returned object is changed (74abd03), so this PR no longer points at the step-indexed calls.

Since OPM/opm-common#5371 is still some weeks out, I would like to keep this as an interim step: until then it is the only documented way for a Python driver to change well controls in a parallel run. I will keep it in draft until #7444 is merged. Would you be OK with merging it then, or would you rather it wait for OPM/opm-common#5371, or until after the 2026.10 release? Either is fine with me.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No blocking issues remain; the retained suggestion concerns optional regression coverage for automatic type registration.

0 open findings

2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity Add regression test for schedule registration via simulator import

opm/​simulators/​flow/​python/​PyBaseSimulator_impl.hpp:155

Optional regression coverage: test_get_schedule.py imports opm.io.schedule at module load, so its tests would still pass if this import were removed. Add a fresh-interpreter test that imports only opm.simulators, initializes a filename-based simulator, and successfully obtains and uses get_schedule(). This would catch the reported unregistered-type failure.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

@bska

bska commented Oct 8, 2026

Copy link
Copy Markdown
Member

So after OPM/opm-common#5371, get_schedule() is still how a script reaches the simulator's own Schedule, and from there the current ScheduleState; what changes is what one can do with it. I have made the get_schedule() docstring neutral about how the returned object is changed (74abd03), so this PR no longer points at the step-indexed calls.

That's a fair assessment, yes.

I would like to keep this as an interim step: until then it is the only documented way for a Python driver to change well controls in a parallel run. I will keep it in draft until #7444 is merged. Would you be OK with merging it then, or would you rather it wait for OPM/opm-common#5371, or until after the 2026.10 release?

I think it's fine to get this in somewhat early, after I've created the release branches for 2026.10, as the current behaviour will be what's exposed in 2026.10. The semantics will be different in 2027.04 however, so we'll have to adapt the descriptions to the new interfaces at that point.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual:irrelevant This PR is a minor fix and should not appear in the manual

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants