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:

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:

...
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 work chain section, where the OutputInputWorkChain specifies that the valid_type of the x input is an Int node:

from aiida.orm import Int
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)
        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)

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

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:

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)

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:

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)

Note

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

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:

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:

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 writing work chains 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:

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)

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 exercise in the work chains section 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.