Skip to content

Commit

Permalink
✨ NEW: Add section on input validation (#457)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbercx committed Oct 6, 2022
1 parent 8d1bb22 commit 9e723fe
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from aiida.orm import Int, Float
from aiida.engine import WorkChain


class OutputInputWorkChain(WorkChain):
"""Toy WorkChain that simply passes the input as an output."""

@classmethod
def define(cls, spec):
"""Specify inputs, outputs, and the workchain outline."""
super().define(spec)

spec.input("x", valid_type=(Int, Float))
spec.outline(cls.result)
spec.output("workchain_result", valid_type=Int)

def result(self):
"""Pass the input as an output."""

# Declaring the output
self.out("workchain_result", self.inputs.x)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from aiida.orm import Int
from aiida.engine import WorkChain, calcfunction


@calcfunction
def addition(x, y):
return x + y


def validate_inputs(inputs, _):
"""Validate the top-level inputs."""
if inputs["x"].value * inputs["y"].value < 0:
return "The `x` and `y` inputs cannot be of the opposite sign."


class AddWorkChain(WorkChain):
"""WorkChain to add two integers."""

@classmethod
def define(cls, spec):
"""Specify inputs, outputs, and the workchain outline."""
super().define(spec)

spec.input("x", valid_type=Int)
spec.input("y", valid_type=Int)
spec.inputs.validator = validate_inputs

spec.outline(cls.result)
spec.output("workchain_result", valid_type=Int)

def result(self):
"""Sum the inputs and parse the result."""

# Call `addition` using the two inputs
addition_result = addition(self.inputs.x, self.inputs.y)

# Declaring the output
self.out("workchain_result", addition_result)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from aiida.orm import Int
from aiida.engine import WorkChain


def validate_x(node, _):
"""Validate the ``x`` input, making sure it is positive."""
if not node.value > 0:
return "the `x` input must be a positive integer."


class OutputInputWorkChain(WorkChain):
"""Toy WorkChain that simply passes the input as an output."""

@classmethod
def define(cls, spec):
"""Specify inputs, outputs, and the workchain outline."""
super().define(spec)

spec.input("x", valid_type=Int, validator=validate_x)
spec.outline(cls.result)
spec.output("workchain_result", valid_type=Int)

def result(self):
"""Pass the input as an output."""

# Declaring the output
self.out("workchain_result", self.inputs.x)
26 changes: 12 additions & 14 deletions docs/sections/writing_workflows/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,22 @@ Writing workflows
------
:column: col-lg-6

.. link-button:: errors
.. link-button:: validation
:type: ref
:text: Dealing with errors
:text: Input validation
:classes: btn-light text-left stretched-link font-weight-bold
^^^^^^^^^^^^

This module explains how to deal with errors in AiiDA workflows, and how to automatically recover from issues that occur for your calculations.
Here we explain how to write a *validator* that can check inputs before running a calculation or workflow.

+++++++++++++
.. list-table::
:widths: 50 50
:class: footer-table
:header-rows: 0

* - |time| 60 min
- |aiida| :aiida-orange:`Advanced`
* - |time| 20 min
- |aiida| :aiida-blue:`Intermediate`


.. panels::
Expand All @@ -161,26 +161,24 @@ Writing workflows
:footer: bg-light border-0

------
:column: col-lg-6
:column: col-lg-12

.. link-button:: https://filedn.com/lsOzB8TTUIDz2WkFj8o6qhp/memes/mossfire.gif
:type: url
:text: Input validation
.. link-button:: errors
:type: ref
:text: Dealing with errors
:classes: btn-light text-left stretched-link font-weight-bold
^^^^^^^^^^^^

**Under construction** 🔨

Here we explain how to write a *validator* that can check inputs before running a calculation or workflow.
This module explains how to deal with errors in AiiDA workflows, and how to automatically recover from issues that occur for your calculations.

+++++++++++++
.. list-table::
:widths: 50 50
:class: footer-table
:header-rows: 0

* - |time| 20 min
- |aiida| :aiida-blue:`Intermediate`
* - |time| 60 min
- |aiida| :aiida-orange:`Advanced`

.. toctree::
:hidden:
Expand Down
152 changes: 152 additions & 0 deletions docs/sections/writing_workflows/validation.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,155 @@
(workflows-validation)=

# Input validation

When running calculation jobs or work chains, it's often easy to make mistakes when setting up the inputs.
This is especially true for more complex work chains which often have a hierarchy of multiple levels (e.g. the `PwBandsWorkChain` for Quantum ESPRESSO).
If the user has provided incorrect inputs and runs the process, it will most likely fail (or potentially worse: silently provide an incorrect result).
Better would be to catch these issues before the process is actually run or submitted to the daemon by _validating_ the inputs.

In this section we will learn about how AiiDA allows you to validate process inputs.

## Type validation

You might have already noticed that AiiDA is able to validate the node _type_ of an input.
If you pass anything but a `StructureData` to the `structure` input of the `PwCalculation`, for example:

:::{code-block} ipython
In [1]: code = load_code('pw@localhost')

In [2]: builder = code.get_builder()

In [3]: builder.structure = Int(10)
:::

This will raise an error that the input for `structure` is not of the right type:

:::{code-block} ipython
...
ValueError: invalid attribute value value 'structure' is not of the right type.
Got '<class 'aiida.orm.nodes.data.int.Int'>', expected
'<class 'aiida.orm.nodes.data.structure.StructureData'>'
:::

The reason is that when the `structure` input is defined for the `PwCalculation` spec, its `valid_type` is set to `StructureData`.
This has already been explained at the start of the {ref}`work chain section<workflows-workchain-define>`, where the `OutputInputWorkChain` specifies that the `valid_type` of the `x` input is an `Int` node:

```{literalinclude} include/code/workchain/my_first_workchain_1_output_input.py
:language: python
:emphasize-lines: 13
```

Indeed, trying to pass anything but an `Int` node to the `x` input will fail with same error as above:

```{code-block} ipython
In [1]: from outputinput import OutputInputWorkChain
In [2]: builder = OutputInputWorkChain.get_builder()
In [3]: builder.x = Float(1)
...
ValueError: invalid attribute value value 'x' is not of the right type. Got '<class 'aiida.orm.nodes.data.float.Float'>', expected '<class 'aiida.orm.nodes.data.int.Int'>'
```

But what if you want the work chain to accept _both_ `Int` and `Float` nodes?
In this case, you can simply pass a tuple with all node types that are valid to the `valid_type` argument:

```{literalinclude} include/code/validation/float_int_output_input.py
:language: python
:emphasize-lines: 13
```

Give it a try!
Now the `OutputInputWorkChain` will accept both node types without issue.

## Value validation

### Single inputs

What if we want to also make sure that the _value_ of a certain input is correct?
Imagine the input represents the maximum number of iterations you want to do in a calculation, and hence must be a positive value.
In this case, AiiDA allows you to specify a _validator_ for an input.
For example, we can add a `validator` to the `x` input of the `OutputInputWorkChain`:

```{literalinclude} include/code/validation/validated_output_input.py
:language: python
:emphasize-lines: 4-7, 18
```


:::{margin}
</br></br></br></br></br></br></br></br>

{{ aiida }} **Ports and Port namespaces**

You can read about the ports and port namespace concepts in the [AiiDA documentation](https://aiida.readthedocs.io/projects/aiida-core/en/latest/topics/processes/usage.html?highlight=port#ports-and-port-namespaces).

:::

:::{margin}
</br>

{{ python }} **The underscore `_` character**

The underscore character has quite a lot interesting use cases in Python!
You can find out more about them [here](https://www.datacamp.com/tutorial/role-underscore-python).

:::

:::{note}

You may be wondering about the `_` input argument in the validator function:

```{code-block}
def validate_x(node, _):
```

The reasons for this are rather technical, but in short every validator function _must_ have a signature with two input arguments: the node and the port or port namespace of the input.
For port namespaces where a certain port has been removed when exposing the inputs in a work chain that wraps the process, the validation of this port can then be skipped.

However, for the simple validation we are doing here, the port input is not needed, and we can simply add an underscore `_` so the signature of the validator still has two inputs but the second is ignored.

:::

After adding the validator, passing a positive valued `Int` still works fine:

:::{code-block} ipython
In [1]: from outputinput import OutputInputWorkChain

In [2]: builder = OutputInputWorkChain.get_builder()

In [3]: builder.x = Int(1)
:::

But a negative `Int` will not pass the validation:

:::{code-block} ipython
In [4]: builder.x = Int(-1)
...
ValueError: invalid attribute value the `x` input must be a positive integer.
:::

It's as simple as that!
Write a function that validates the input and pass this to the `validator` keyword argument when defining the input on the `spec`.

### Top-level validation

In some cases, validation of one input may depend on the value of another input.
Imagine that for the `AddWorkChain` in the {ref}`writing work chains<workflows-workchain-creating-data>` section we want to make sure that the `x` and `y` inputs have the same sign.
In this case we cannot simply add a validator to one of the inputs, since we won't have access to the value of the other input inside the validator function.
However, we can also add validation to the top-level namespace of a process:

```{literalinclude} include/code/validation/validated_add_workchain.py
:language: python
:emphasize-lines: 9-12, 24
```

Note that the first input argument of the top-level validator method (called `inputs` here), is simply a dictionary that maps the input labels to the corresponding nodes.

## Exercise

Take the `MultiplyAddWorkChain` from the {ref}`exercise in the work chains section<workflows-workchain-adding-complexity>` and adapt/add some validation:

* Allow the `x` and `y` inputs to _also_ be `Float` nodes.
* Make sure `z` is not zero.
* Make sure the sum of `x` and `y` is not zero.
6 changes: 6 additions & 0 deletions docs/sections/writing_workflows/workfunction.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ In [3]: ase_structure = structure.get_ase()

Let's have a look at what structure we found:

:::{margin}

The structure you found can of course be different!

:::

```{code-block} ipython
In [4]: ase_structure
Out[4]: Atoms(symbols='NaNbO3', pbc=True, cell=[3.9761497211, 3.9761497211, 3.9761497211], masses=...)
Expand Down

0 comments on commit 9e723fe

Please sign in to comment.