Multi-scenario workflows (:mod:`.workflow`) ******************************************* .. contents:: :local: :backlinks: none Concept & design ================ Research with MESSAGEix models often involves multiple scenarios that are related to one another or derived from one another by certain modifications. Together, the solutions/reported information from these scenarios provide the output data used in research products, e.g. a plot comparing total emissions in a policy scenario to a reference scenario. :mod:`.model.build` provides tools to build models or scenarios based on (possibly empty) base scenarios; and :mod:`~message_ix_models.tools` provides tools for manipulating scenarios or model input data (parameters). The :class:`.Workflow` API provided in this module allows researchers to use these pieces and atomic, reusable functions to define arbitrarily complex workflows involving many, related scenarios; and then to solve, report, or otherwise operate on those scenarios. The generic pattern for workflows is: - Each scenario has zero or 1 (or more?) base/precursor/antecedent scenarios. These must exist before the target scenario can be created. - A workflow ‘step’ includes: 1. A precursor scenario is obtained. It may be returned by a prior workflow step, or loaded from a :class:`~ixmp.Platform`. 2. (Optional) The precursor scenario is cloned to a target model name and scenario name. 3. A function is called to operate on the scenario. This function may do zero or more of: - Apply structure or data modifications, for example: - Set up a model variant, e.g. adding the MESSAGEix-Materials structure to a base MESSAGEix-GLOBIOM model. - Change policy variables via constraint parameters. - Any other possible modification. - Solve the target scenario. - Invoke reporting. 4. The resulting scenario is passed to the next workflow step. - A workflow can consist of any number of scenarios and steps. - The same precursor scenario can be used as the basis for multiple target scenarios. - A workflow is :meth:`.Workflow.run` starting with the earliest precursor scenario, ending with 1 or many target scenarios. The implementation is based on the observation that these form a graph (specifically, a directed, acyclic graph, or DAG) of nodes (= scenarios) and edges (= steps), in the same way that :mod:`message_ix.report` calculations do; and so the :mod:`dask` DAG features (via :mod:`genno`) can be used to organize the workflow. Usage ===== General ------- Define a workflow using ordinary Python functions, each handling the modifications/manipulations in an atomic workflow step. These functions **must**: - Accept at least 2 arguments: 1. A :class:`.Context` instance. 2. The precursor scenario. 3. (Optional) Additional, keyword-only arguments. - Return either: - a :class:`~.message_ix.Scenario` object, that can be the same object provided as an argument, or a different scenario, e.g. a clone or a different scenario, even from a different platform. - :class:`None`. In this case, any modifications implemented by the step should be reflected in the Scenario given as an argument. The functions **may**: - call any other code, and - be as short (one line) or long (many lines) as desired; and they **should**: - respond in documented, simple ways to settings on the Context argument and/or their keyword argument(s), if any. .. code-block:: python def changes_a(c: Context, s: Scenario) -> None: """Change a scenario by modifying structure data, but not data.""" with s.transact(): s.add_set("technology", "test_tech") # Here, invoke other code to further modify `s` def changes_b(c: Context, s: Scenario, *, value=100.0) -> None: """Change a scenario by modifying parameter data, but not structure. This function takes an extra argument, `values`, so functools.partial() can be used to supply different values when it is used in different workflow steps. See below. """ with s.transact(): s.add_par( "technical_lifetime", make_df( "technical_lifetime", node_loc=s.set("node")[0], year_vtg=s.set("year")[0], technology="test_tech", value=100.0, unit="y", ), ) # Here, invoke other code to further modify `s` With the steps defined, the workflow is composed using a :class:`.Workflow` instance. Call :meth:`.Workflow.add_step` to define each target model with its precursor and the function that will create the former from the latter: .. code-block:: python from message_ix_models import Context, Workflow # Create the workflow ctx = Context.get_instance() wf = Workflow(ctx) # "Model name/base" is loaded from an existing platform wf.add_step( "base", None, target="ixmp://example-platform/Model name/base#123", ) # "Model/A" is created from "Model/base" by calling changes_a() wf.add_step("A", "base", changes_a, target="Model/A") # "Model/B1" is created from "Model/A" by calling changes_b() with the # default value wf.add_step("B1", "A", changes_b, target="Model/B1") # "Model/B2" is similar, but uses a different value wf.add_step("B2", "A", partial(changes_b, value=200.0), target="model/B2") Finally, the workflow is triggered using :meth:`.Workflow.run`, giving either one step name or a list of names. The indicated scenarios are created (and solved, if the workflow steps involve solving); if this requires any precursor scenarios, those are first created and solved, etc. as required. Other, unrelated scenarios/steps are not created. .. code-block:: python s1, s2 = wf.run(["B1", "B2"]) Usage examples -------------- - :mod:`message_data.projects.navigate.workflow` .. todo:: Expand with discussion of workflow patterns common in research projects using MESSAGEix, e.g.: - Run the same scenario with multiple emissions budgets. API reference ============= .. currentmodule:: message_ix_models.workflow .. automodule:: message_ix_models.workflow :members: :exclude-members: WorkflowStep .. autoclass:: WorkflowStep :members: :special-members: __call__