Creating a command-line task¶
This document describes how to write a command-line task, which is the LSST version of a complete data processing pipeline. To create a command-line task you will benefit from some background:
Read Overview of the task framework to get a basic idea of what tasks are and what classes are used to implement them.
Read Creating a task. A command-line task is an enhanced version of a regular task. Thus all the considerations for writing a normal task also apply to writing a command-line task, and these will not be repeated in this manual.
Acquire a basic understanding of data repositories and how to use the butler to read and write data (to be written; for now read existing tasks to see how it is done).
Introduction¶
A command-line task is an enhanced version of a regular task (see Creating a task). Regular tasks are only intended to be used as relatively self-contained stages in data processing pipelines, whereas command-line tasks can also be used as complete pipelines. As such, command-line tasks include run scripts that run them as pipelines.
Command-line tasks have the following key attributes, in addition to the attributes for regular tasks:
They are subclasses of
lsst.pipe.base.CmdLineTask
, whereas regular tasks are subclasses oflsst.pipe.base.Task
.They have an associated run script to run them from the command-line as pipelines (this is common, but not required, for regular tasks).
They have a
runDataRef
method which performs the full pipeline data processing.By default the
runDataRef
method takes exactly one argument: a data reference for the item of data to be processed. Variations are possible, but require that you provide a custom argument parser and often a custom task runner.When run from the command line, most command-line tasks save the configuration used and the metadata generated. See Persisting Config and Metadata for more information.
They have an additional class variable,
RunnerClass
, that specifies a “task runner” for the task. The task runner takes a parsed command and runs the task. The default task runner will work for any script whoserunDataRef
method accepts a single data reference, such asExampleCmdLineTask
. If your task’srunDataRef
method needs something else then you will have to provide a custom task runner.They have an additional class variable
canMultiprocess
, which defaults toTrue
. If your task runner cannot run your task with multiprocessing then set itFalse
. Note: multiprocessing only affects how the task runner calls the top-level task; thus it is ignored when a task is used as a subtask.
Run Script¶
A command-line task can be run as a pipeline via run script.
This is usually a trivial script which merely calls the task’s parseAndRun
method. parseAndRun
does the following:
Parses the command line, which includes determining the configuration for the task and which data items to process.
Constructs the task.
Calls the task’s
runDataRef
method once for each data item to process.
examples/exampleCmdLineTask.py
, the runner script for ExampleCmdLineTask
, is typical:
#!/usr/bin/env python2
from lsst.pipe.tasks.exampleCmdLineTask import ExampleCmdLineTask
ExampleCmdLineTask.parseAndRun()
For most command-line tasks you should put the run script into your package’s bin/
directory, so that it is on your $PATH
when you setup your package with eups.
We did not want the run script for ExampleCmdLineTask
to be quite so accessible, so we placed it in the examples/
directory instead of bin/
.
Remember to make your run script executable using chmod +x.
Reading and Writing Data¶
The runDataRef method typically receives a single data reference, as mentioned above. It read and writes data using this data reference (or the underlying butler, if necessary).
Adding Dataset Types¶
Every time you write a task that writes a new kind of data (a new “dataset type”) you must tell the butler about it. Similarly, if you write a new task for which you want to save configuration and metadata (which is the case for most tasks that process data), you have to tell the butler about it.
To add a dataset, edit the mapper configuration file for each observatory package on whose data the task can be run. If the task is of general interest (wanted for most or all observatory packages) then this process of updating all the mapper configuration files can be time consuming.
There are plans to change how mappers are configured.
But as of this writing, mapper configuration files are contained in the policy directory of the observatory package.
For instance the configuration for the lsstSim mapper is defined in obs_lsstSim/policy/LsstSimMapper.paf
.
Persisting Config and Metadata¶
Normally when you run a task you want the configuration for the task and the metadata generated by the task to be saved to the data repository. By default, this is done automatically, using dataset types:
_DefaultName_config
for the configuration_DefaultName_metadata
for the metadata
where _DefaultName
is replaced with the value of the task’s _DefaultName
, see class variable.
Whether you use these default dataset types or customize the dataset types, you will have to add dataset types for the configuration and metadata.
Customizing Config and Metadata Dataset Types¶
Occasionally the default dataset types for configuration and metadata are not sufficient.
For instance in the case of the pipe.tasks.makeSkyMap.MakeSkyMapTask
and various co-addition tasks, the co-add type must be part of the config and metadata dataset type name.
To customize the dataset type of a task’s config or metadata, define task methods _getConfigName
and _getMetadataName
to return the desired names.
Prevent Saving Config and Metadata¶
For some tasks you may wish to not save config and metadata at all.
This is appropriate for tasks that simply report information without saving data.
To disable saving configuration and metadata, define task methods _getConfigName
and _getMetadataName
methods to return None
.
Custom Argument Parser¶
The default lsst.pipe.base.ArgumentParser
-type returned by CmdLineTask._makeArgumentParser
assumes that your task’s runDataRef method processes raw or calibrated images.
If this is not the case you can easily provide a modified argument parser.
Typically this consists of constructing an instance of lsst.pipe.base.ArgumentParser
and then adding some ID arguments to it using add_id_argument
.
This is shown in several examples below.
Please resist the urge to add other kinds of arguments to the argument parser unless truly needed.
One strength of our tasks is how similar they are to each other.
Learning one set of arguments suffices to use many tasks.
Warning
If your task requires a custom argument parser to do more than just change the type of the single data reference, then it also require a custom task runner as well.
Here are some examples:
A task’s
runDataRef
method requires a data reference of some kind other than a raw or calibrated image. This is a common case, and easily solved. For example theprocessCoadd.ProcessCoaddTask
processes co-adds, which are specified by sky map patch. Here isProcessCoaddTask._makeArgumentParser
:@classmethod def _makeArgumentParser(cls): """Create an argument parser """ parser = pipeBase.ArgumentParser(name=cls._DefaultName) parser.add_id_argument("--id", "deepCoadd", help="data ID, e.g. --id tract=12345 patch=1,2", ContainerClass=CoaddDataIdContainer) parser.add_id_argument("--selectId", "calexp", help="data ID, e.g. --selectId visit=6789 ccd=0..9", ContainerClass=SelectDataIdContainer) return parser
The first argument to
ArgumentParser
is the name of the ID argument.The second argument is a dataset type, which specifies the keys that are used with this ID argument. The keys associated with a particular dataset type are specified in the mapper configuration file for the observatory package and camera in question, and thus may vary from camera to camera. In practice, the keys for
raw
andcalexp
dataset types usually do vary from camera to camera, but the keys for coadds do not. However, this is not a fixed rule. For most observatory packagesdeepCoadd
is one of two coadd dataset types, and the other,goodSeeingCoadd
, would work just as well for this argument.A custom
ContainerClass
(for example,lsst.coadd.utils.coaddDataIdContainer.CoaddDataIdContainer
) is provided to support iterating over missing keys (e.g. if you provide a tract but not a patch then the task will iterate over all available patches for that tract). This happens automatically forraw
andcalexp
dataset types, but not most other dataset types. Examine the code inCoaddDataIdContainer
to see how it works.
A task’s
runDataRef
method requires more than one kind of data reference. An example is co-addition, which requires the user to specify the co-add as a sky map patch, and optionally allows the user to specify a list of exposures to co-add.CoaddBaseTask._makeArgumentParser
is a straightforward example of specifying two data IDs arguments: one for the sky map patch, and an optional ID argument for which exposures to co-add:@classmethod def _makeArgumentParser(cls): """Create an argument parser """ parser = pipeBase.ArgumentParser(name=cls._DefaultName) parser.add_id_argument("--id", "deepCoadd", help="data ID, e.g. --id tract=12345 patch=1,2", ContainerClass=CoaddDataIdContainer) parser.add_id_argument("--selectId", "calexp", help="data ID, e.g. --selectId visit=6789 ccd=0..9", ContainerClass=SelectDataIdContainer) return parser
In this case the custom container class
SelectDataIdContainer
adds additional information for the task, to save processing time.A task’s
runDataRef
method requires no data references at all. An example ismakeSkyMap.MakeSkyMapTask
, which makes a sky map for a set of co-adds.makeSkyMap.MakeSkyMapTask._makeArgumentParser
is trivial:@classmethod def _makeArgumentParser(cls): """Create an argument parser No identifiers are added because none are used. """ return pipeBase.ArgumentParser(name=cls._DefaultName)
Custom Task Runner¶
The standard task runner is lsst.pipe.base.TaskRunner
.
It assumes that your task’s runDataRef
method wants a single data reference and nothing else.
If your task uses the pre-2018 naming convention and has a run
method that operates on a data references instead of a runDataRef
method, you can still use this as a CmdLineTask
by using the LegacyTaskRunner
, which will call your task’s run
method.
If neither of those are the case then you will have to provide a custom task runner for your task.
This involves writing a subclass of lsst.pipe.base.TaskRunner
and specifying it in your task using the RunnerClass
class variable.
Here are some situations where a custom task runner is required:
The task’s
runDataRef
method requires extra arguments. An example is co-addition, which optionally accepts a list of images to co-add. The custom task runner iscoaddBase.CoaddTaskRunner
and is pleasantly simple:class CoaddTaskRunner(pipeBase.TaskRunner): @staticmethod def getTargetList(parsedCmd, **kwargs): return pipeBase.TaskRunner.getTargetList(parsedCmd, selectDataList=parsedCmd.selectId.dataList, **kwargs)
The task requires no data references, just a butler. An example is
makeSkyMap.MakeSkyMapTask
, which makes askymap.SkyMap
for a set of co-adds. It uses the custom task runnermakeSkyMap.MakeSkyMapRunner
, which is more complicated than the previous example because the entire__call__
method must be overridden:class MakeSkyMapRunner(pipeBase.TaskRunner): """Only need a single butler instance to run on.""" @staticmethod def getTargetList(parsedCmd): return [parsedCmd.butler] def __call__(self, butler): task = self.TaskClass(config=self.config, log=self.log) if self.doRaise: results = task.runDataRef(butler) else: try: results = task.runDataRef(butler) except Exception as e: task.log.fatal("Failed: %s" % e) if not isinstance(e, pipeBase.TaskError): traceback.print_exc(file=sys.stderr) task.writeMetadata(butler) if self.doReturnResults: return results