*********************** Numina Pipeline Example *********************** This guide is intended as an introductory overview of the creation of instrument reduction pipelines with Numina. For detailed reference documentation of the functions and classes contained in the package, see the :ref:`reference`. .. warning:: This "Pipeline Creation Guide" is still a work in progress; some of the material is not organized, and several aspects of Numina are not yet covered sufficient detail. Execution environment of the Recipes #################################### Recipes have different execution environments. Some recipes are designed to process observing modes required for the observation. These modes are related to visualization, acquisition and focusing. The Recipes are integrated in the GTC environment. We call these recipes the **Data Factory Pipeline**, (:term:`DFP`). Other group of recipes are devoted to scientific observing modes: imaging, spectroscopy and auxiliary calibrations. These Recipes constitute the **Data Reduction Pipeline**, (:term:`DRP`). The software is meant to be standalone, users shall download the software and run it in their own computers, with reduction parameters and calibrations provided by the instrument team. Users of the DRP will use the simple Numina CLI. Users of the DFP shall interact with the software through the GTC Inspector. Instrument Reduction Pipeline Example ##################################### In the following sections we create an Instrument Reduction Pipeline for an instrument name *CLODIA*. In order to make a new Instrument Reduction Pipeline visible to Numina and the GTC Control System you have to create a full Python package that will contain the reduction recipes, data types and other processing code. The creation of Python packages is described in detail (for example) in the `Python Packaging User Guide `_. Then, we create a Python package called `clodiadrp` with the following structure (we ignore files such as README or LICENSE as they are not relevant here):: clodiadrp |-- clodiadrp | |-- __init__.py |-- pyproject.toml From here the steps are: 1. Create a configuration yaml file. 2. Create a loader file. 3. Link the *entry_point* option in *pyproject.toml* with the loader file. 4. Create the Pipeline's Recipes. In the following we will continue with the same example as previously. Configuration File ****************** The configuration file contains basic information such as: * the list of modes of the instrument * the list of recipes of the instrument * the mapping between recipes and modes. In this example, we assume that CLODIA has three modes: **Bias**, **Flat** and **Image**. The first two modes are used for pedestal and flat-field illumination correction. The third is the main scientific mode of the instrument. Create a new yaml file in the root folder named *drp.yaml*. .. code-block:: yaml name: CLODIA configurations: default: {} modes: -key: bias name: Bias summary: Bias mode description: > Full description of the Bias mode -key: flat name: Flat summary: Flat mode description: > Full description of the Flat mode -key: image name: Image summary: Image mode description: > Full description of the Image mode pipelines: default: version: 1 recipes: bias: clodiadrp.recipes.recipe flat: clodiadrp.recipes.recipe image: clodiadrp.recipes.recipe The entry `modes` contains a list of the observing modes of the instrument. There are three: Bias, Flat and Image. Each entry contains information about the mode. A *name*, a short *summary* and a multi-line *description*. The field *key* is used to map the observing modes and the *recipes*, so *key* has to be unique and equal to only one value in each `recipes` block under `pipelines`. The entry `pipelines` contains only one pipeline, called *default* by convention. The `pipeline` contains recipes, each related to one observing mode by means of the filed *key*. For the moment we haven't developed any recipe, so the value of each key (*clodiadrp.recipes.recipe*) doesn't exist yet. Loader File *********** Create a new loader file in the root folder named *loader.py* with the following information: .. code-block:: python import numina.core def drp_load(): """Entry point to load CLODIA DRP.""" return numina.core.drp_load('clodiadrp', 'drp.yaml') Create entry point ****************** Once we have created the *loader.py* file, the only thing we have to do is to make CLODIA visible to Numina. To do so, just modify the *pyproject.toml* file to add an entry point. .. code-block:: toml [project.entry-points."numina.pipeline.1"] CLODIA = "clodiadrp.loader:drp_load" The Numina CLI tool checks this particular entry point. They call the function provided by the entry point. The function :func:`~numina.core.pipelineload.drp_load` reads and parses the YAML file and creates an object of class :class:`~numina.core.pipeline.InstrumentDRP` for each recipes it founds. These objects are used by Numina CLI to discover the available Instrument Reduction Pipelines. At this stage, the file layout is as follows:: clodiadrp |-- clodiadrp | |-- __init__.py | |-- loader.py | |-- drp.yaml |-- pyproject.toml .. note:: In fact, it is not necessary to use a YAML file to contain the Instrument information. The only strict requirement is that the function in the entry point 'numina.pipeline.1' must return a valid :class:`~numina.core.pipeline.InstrumentDRP` object. The use of a YAML file and the :func:`~numina.core.pipelineload.drp_load` function is only a matter of convenience. Recipes Creation **************** We haven't created any reduction recipe yet. As a matter of organization, we suggest to create a dedicated subpackage for recipes `clodiadrp.recipes` and a module for each recipe. The file layout is:: clodiadrp |-- clodiadrp | |-- __init__.py | |-- loader.py | |-- drp.yaml | |-- recipes | | |-- __init__.py | | |-- bias.py | | |-- flat.py | | |-- image.py |-- pyproject.toml Recipes must provide three things: 1) a description of the inputs of the recipe; 2) a description of the products of the recipe and 3) a *run* method which is in charge of executing the proccessing. Additionally, all Recipes must inherit from :class:`~numina.core.recipes.BaseRecipe`. We start with a simple `Bias` recipe. Its purpose is to process images previously taken in *Bias* mode, that is, a series of pedestal images. The recipe will receive the result of the observation and return a master bias image. .. code-block:: python from numina.core import Result, Requirement from numina.core import DataFrameType from numina.types.obsresult import ObservationResultType from numina.core.recipes import BaseRecipe class Bias(BaseRecipe): (1) obresult = Requirement(ObservationResultType, "Observation Result") (2) master_bias = Result(DataFrameType) (3) def run(self, rinput): (4) # Here the raw images are processed # and a final image myframe is created result = self.create_result(master_bias=myframe) (5) return result 1. Each recipe must be a class derived from :class:`~numina.core.recipes.BaseRecipe` 2. This recipe only requires the result of the observation. Each requirement is an object of the :class:`~numina.core.requirements.Requirement` class or any subclass of it. The type of the requirement is :class:`~numina.types.obsresult.ObservationResultType`, representing the result of the observation. 3. This recipe only produces one result. Each product is an object of :class:`~numina.core.dataholders.Result` class. The type of the product is given by :class:`~numina.core.products.DataFrameType`, representing an image. 4. Each recipe must provide a `run` method. The method has only one argument that collects the values of all inputs declared by the recipe. In this case, `rinput` has a member named `obresult` and can be accessed through `rinput.obresult` which belongs to :class:`~numina.core.oresult.ObservationResult` class. 5. The recipe must return an object that collects all the declared products of the recipe, of :class:`~numina.core.recipeinout.RecipeResult` class. This is accomplished internally by the `create_result` method. It will raise a run time exception if any of the declared products are not provided. We can now create the `Flat` recipe (inside `flat.py`). This recipe has two requirements, the observation result and a master bias image (flat-field images require bias subtraction). .. code-block:: python from numina.core import Result, Requirement from numina.core import DataFrameType from numina.types.obsresult import ObservationResultType from numina.core.recipes import BaseRecipe class Flat(BaseRecipe): obresult = Requirement(ObservationResultType, "Observation Result") (1) master_bias = Requirement(DataFrameType, "Master Bias") (2) master_flat = Result(DataFrameType) def run(self, rinput): (3) # Here the raw images are processed # and a final image myframe is created result = self.create_result(master_flat=myframe) (4) return result 1. This recipe only requires the result of the observation. Each requirement is an object of the :class:`~numina.core.requirements.Requirement` class or any subclass of it. The type of the requirement is :class:`~numina.types.obsresult.ObservationResultType`, representing the result of the observation. 2. It also requires a master bias image which belongs to :class:`~numina.core.products.DataFrameType` class (represents an image). 3. In this case, `rinput` has two members: 1) `rinput.obresult` of :class:`~numina.core.oresult.ObservationResult` class and 2) a `rinput.master_bias` of :class:`~numina.core.dataframe.DataFrame` class 4. The arguments of `create_result` must be the same names used in the product definition. Finally, the recipe for `Image` mode reduction (inside `image.py`) has three requirements, the observation result, a master bias and a master flat images .. code-block:: python from numina.core import Result, Requirement from numina.core import DataFrameType from numina.types.obsresult import ObservationResultType from numina.core.recipes import BaseRecipe class Image(BaseRecipe): obresult = Requirement(ObservationResultType) master_bias = Requirement(DataFrameType) master_flat = Requirement(DataFrameType) final = Result(DataFrameType) def run(self, rinput): (1) # Here the raw images are processed # and a final image myframe is created result = self.create_result(final=myframe) return result 1. In this case, `rinput` will have three members `rinput.obresult` of :class:`~numina.types.obsresult.ObservationResult` class, `rinput.master_bias` of :class:`~numina.core.dataframe.DataFrame` class and `rinput.master_flat` of :class:`~numina.core.dataframe.DataFrame` class. .. note:: It is not strictly required that the requirements and products names are consistent between recipes, although it is highly recommended. Now we must update `drp.yaml` to insert the full name of the recipes (package and class), as follows .. code-block:: yaml name: CLODIA configurations: default: {} modes: -key: bias name: Bias summary: Bias mode description: > Full description of the Bias mode -key: flat name: Flat summary: Flat mode description: > Full description of the Flat mode -key: image name: Image summary: Image mode description: > Full description of the Image mode pipelines: default: version: 1 recipes: bias: clodiadrp.recipes.bias.Bias flat: clodiadrp.recipes.flat.Flat image: clodiadrp.recipes.image.Image Specialized data products ************************* There is some information that is missing of our current setup. The products of some recipes are the inputs of others. The master bias created by `Bias` is the input that `Flat` and `Image` require. To represent this situation we use specialized data products. We start by adding a new module `products`:: clodiadrp |-- clodiadrp | |-- __init__.py | |-- loader.py | |-- products.py | |-- drp.yaml | |-- recipes | | |-- __init__.py | | |-- bias.py | | |-- flat.py | | |-- image.py |-- pyproject.toml We have two types of images that are products of recipes that can be required by other recipes: **master bias** and **master flat**. We represent this by creating two new types derived from :class:`~numina.types.frame.DataFrameType` (because the new types are images) and :class:`~numina.types.product.DataProductMixin` (because the new types are products that must be handled by both Numina CLI and GTC Control system) classes. .. code-block:: python from numina.types.frame import DataFrameType from numina.types.product import DataProductMixin class MasterBias(DataFrameType, DataProductMixin): pass class MasterFlat(DataFrameType, DataProductMixin): pass Now we must modify our recipes as follows. First `Bias` .. code-block:: python from numina.core import Result, Requirement from numina.types.obsresult import ObservationResultType from numina.core.recipes import BaseRecipe from clodiadrp.products import MasterBias (1) class Bias(BaseRecipe): obresult = Requirement(ObservationResultType) master_bias = Result(MasterBias) (2) ... (3) 1. Import the new type `MasterBias`. 2. Declare that our recipe produces `MasterBias` images. 3. `run` method remains unchanged. Then `Flat`: .. code-block:: python from numina.core import Result, Requirement from numina.types.obsresult import ObservationResultType from numina.core.recipes import BaseRecipe from clodiadrp.products import MasterBias, MasterFlat class Flat(BaseRecipe): obresult = Requirement(ObservationResultType) master_bias = Requirement(MasterBias) (1) master_flat = Result(MasterFlat) (2) ... (3) 1. `MasterBias` is used as a requirement. This guaranties that the images provided here are those created by `Bias` and no other. 2. Declare that our recipe produces `MasterFlat` images. 3. `run` method remains unchanged. And finally `Image`: .. code-block:: python from numina.core import Result, Requirement from numina.core import DataFrameType from numina.types.obsresult import ObservationResultType from numina.core.recipes import BaseRecipe from clodiadrp.products import MasterBias, MasterFlat class Image(BaseRecipe): obresult = Requirement(ObservationResultType) master_bias = Requirement(MasterBias) (1) master_flat = Requirement(MasterFlat) (2) final = Result(DataFrameType) (3) ... (4) 1. `MasterBias` is used as a requirement. This guaranties that the images provided here are those created by `Bias` and no other. 2. `MasterFlat` is used as a requirement. This guaranties that the images provided here are those created by `Flat` and no other. 3. Declare that our recipe produces `Image` images. 4. `run` method remains unchanged.