Adding a Shared Step
In Polaris, shared steps are a powerful way to avoid redundant computation and ensure consistency across related tasks. Rather than each task duplicating the same setup or preprocessing work, a shared step can be created once and then referenced by multiple tasks that need it. This is especially useful when many tasks require identical inputs or initial conditions. For more on the concept and implementation of shared steps, see the Shared steps.
The init step in overflow (which we will mimic in
my_overflow) represents a common example. Many tasks use the same
mesh and initial condition for a given resolution, so it makes sense to define
a single init step and share it among all relevant tasks. This approach saves
compute time and ensures that all tasks are using exactly the same initial
state.
In this section, we’ll walk through how to create a shared init step for your
new category of tasks, following the established pattern in Polairs.
This step is involved enough that it divided into several pages, each covering a different part of the process. We’ll start out by just setting up some basic infrastructure, and making sure it’s wired together right.
Adding the init Step
In polaris, steps are defined in python modules by classes that descend
from the polaris.Step base class. The modules can be defined
within the task package (if they are unique to the task) or in the
category of tasks (if they are shared among several tasks). In this example,
we have only added one task (default) so far but we anticipate
adding more. All tasks will require a similar init step, so
it makes sense for the init.py module to be located in the test
group’s package to promote Code sharing.
The init step will create the MPAS mesh and initial condition for
the task. To start with, we’ll just create a new Init class. In this
example, we will have Init descend from Step. (In the overflow version,
it descends from OceanIOStep so add some functionality for writing out files
either in Omega or MPAS-Ocean format, just so you’re not surprised by the
difference.) Here’s the beginnings of the Init step:
$ vim polaris/tasks/ocean/my_overflow/init.py
from polaris import Step
class Init(Step):
"""
A step for creating a mesh and initial condition for "my overflow" test
cases.
"""
def __init__(self, component, name, indir):
"""
Create the step
Parameters
----------
component : polaris.Component
The component the step belongs to
name : str
The name of the step
indir : str
The name of the directory the task will be set up in
"""
super().__init__(component=component, name=name, indir=indir)
The step takes the component it belongs to as an input to its constructor,
and passes that along to the superclass’ version of the constructor, along with
the name of the step and the directory that the step should go in wihtin a
subdirectory that’s the same as name. An alternative would be to provide
subdir argument if we want to specify the full subdirectory of the step
withing the component without using name as the final subdirectory.
Create a Shared Step Object
Update your add_my_overflow_tasks() function in
polaris/tasks/ocean/my_overflow/__init__.py to create and add the shared
Init step:
from polaris.tasks.ocean.my_overflow.init import Init as Init
def add_my_overflow_tasks(component):
"""
Add a task following the "my overflow" test case of Petersen et al. (2015)
doi:10.1016/j.ocemod.2014.12.004
component : polaris.ocean.Ocean
the ocean component that the task will be added to
"""
init_step = Init(component=component, name='init', indir=indir)
This will place the init step in a subdirectory planar/my_overflow/init
within the ocean component. It will also define a shared config file at
planar/my_overflow/my_overflow.cfg that will have the config options for
my_overflow tasks and steps generally and init in particular.
Test Things Out Again
There’s still not much to test but it doesn’t hurt to rerun:
polaris list
Again, if you get import errors, something isn’t quite hooked up right. It’s still to early to see anything new in the list of tasks.
Adding a Config File
To set default config options (see Config Files) that are shared across
all the tasks and steps of a category of tasks, we typically add them to to a
config file with the same name as the category of tasks (e.g.
my_overflow.cfg). Having a shared config file means fewer places that a user
has to edit the config options. (But for this same reason it’s improtant that
the config options really can be shared between steps and tasks – if the
same config option should have different values for different tasks, it
obviously doesn’t make sense to try to use a config file shared between these
tasks.)
In this case, we know that these config options are going to be used across
many tasks so it makes sense to put them directly in the
my_overflow subdirectory:
$ vim polaris/tasks/ocean/my_overflow/my_overflow.cfg
# Options related to the "my overflow" case
[my_overflow]
We’ll flesh out the config options as we need them below.
There is another way to get define default config options. The my_overflow
tasks don’t use this but we can also define them in the code in
a configure() method of the task. These config options will also show
up in the config file in the task’s work directory. There is no
configure() method for individual steps because it is not a good idea to
change config options within a step, since other steps may be affected in
potentially unexpected ways. You can see an example of this in the
cosine_bell task.
Add a Shared Config File to the Step
Here’s what you need to add in ``polaris/tasks/ocean/my_overflow/init.pyto create a shared config file and add it to yourinit` step:
from polaris.config import PolarisConfigParser as PolarisConfigParser
from polaris.tasks.ocean.my_overflow.init import Init as Init
def add_my_overflow_tasks(component):
"""
Add a task following the "my overflow" test case of Petersen et al. (2015)
doi:10.1016/j.ocemod.2014.12.004
component : polaris.ocean.Ocean
the ocean component that the task will be added to
"""
indir = 'planar/my_overflow'
config_filename = 'my_overflow.cfg'
config = PolarisConfigParser(filepath=f'{indir}/{config_filename}')
config.add_from_package('polaris.tasks.ocean.my_overflow', config_filename)
init_step = Init(component=component, name='init', indir=indir)
init_step.set_shared_config(config, link=config_filename)
The config file will live within the ocean component in a work directory
at planar/my_overflow/my_overflow.cfg. Later, we’ll add it to the tasks
within my_overflow as well.
Creating a Horizontal Mesh
The run() method of the init step does the actual work of
creating a mesh and initial condition. Below, We will present the method in 3
pieces. Please browse the code yourself to see the complete method.
First, we create a regular, planar, hexagonal mesh that is periodic in the x
direction but not in y. The number of cells in mesh are based on the physical
sizes of the mesh in x and y, which come from config options lx and ly
discussed below. The distance between grid-cell centers dc is just the
resolution converted from km to m. Then, we “cull” (remove) the the top and
bottom row of cells in the y direction so the mesh is no longer periodic in
that direction (nonperiodic_y=True).
$ vim polaris/tasks/ocean/my_overflow/init.py
from mpas_tools.io import write_netcdf
from mpas_tools.mesh.conversion import convert, cull
from mpas_tools.planar_hex import make_planar_hex_mesh
from polaris import Step
from polaris.mesh.planar import compute_planar_hex_nx_ny
class Init(Step):
...
def run(self):
"""
Run this step of the test case
"""
config = self.config
logger = self.logger
section = config['my_overflow']
lx = section.getfloat('lx')
ly = section.getfloat('ly')
resolution = section.getfloat('resolution')
nx, ny = compute_planar_hex_nx_ny(lx, ly, resolution)
dc = 1e3 * resolution
ds_mesh = make_planar_hex_mesh(
nx=nx, ny=ny, dc=dc, nonperiodic_x=True, nonperiodic_y=False
)
write_netcdf(ds_mesh, 'base_mesh.nc')
ds_mesh = cull(ds_mesh, logger=logger)
ds_mesh = convert(
ds_mesh, graphInfoFileName='culled_graph.info', logger=logger
)
write_netcdf(ds_mesh, 'culled_mesh.nc')
We use mpas_tools.planar_hex.make_planar_hex_mesh() to compute the
number of grid cells in x and y from the physical sizes and the resolution.
We will continue with the run() method as we move through the tutorial, but
first it is worth discussing how to set the config options used to generate
the horizontal mesh.
Adding a the Config Options to the Config File
We need a way to get the physical extent of the mesh lx and ly in km and
the resolution (in km) of the cells. We could hard-code these in the task
directly and sometimes we do but this can also have several disadvantages.
First and foremost, unless they are explicitly given as part of the name of
the task or step, this it hides these physical values in a way that isn’t
accessible to users. They become “magic numbers” in the code. Second, by
making them available to users, they should be easy to alter so a user can
explore the effects of modifying them if they choose to. Finally, the config
options are available to each step in the tasks so it is easy to look them up
again later (e.g. during plotting) if they are needed.
$ vim polaris/ocean/tasks/my_overflow/my_overflow.cfg
# Options related to the "my overflow" case
[my_overflow]
# The width of the domain in the across-slope dimension (km)
ly = 40
# The length of the domain in the along-slope dimension (km)
lx = 200
# Distance from two cell centers (km)
resolution = 2.0
Test Things One More Time
Give it one more test:
polaris list
Import errors, missing files, and such will tell you something’s missing or not hooked up quite right.