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
andy
inputs to also beFloat
nodes.Make sure
z
is not zero.Make sure the sum of
x
andy
is not zero.
Solution
from aiida.orm import Int, Float
from aiida.engine import WorkChain, calcfunction
@calcfunction
def addition(x, y):
return x + y
@calcfunction
def multiplication(x, y):
return x * y
def validate_z(node, _):
"""Validate the `z` input."""
if node.value == 0:
return "The value of `z` can not be zero."
def validate_inputs(inputs, _):
"""Validate the top-level inputs."""
if inputs["x"] + inputs["y"] == 0:
return "The sum of `x` and `y` can not be zero."
class MultiplyAddWorkChain(WorkChain):
"""WorkChain to multiply two integers and add a third."""
@classmethod
def define(cls, spec):
"""Specify inputs, outputs, and the workchain outline."""
super().define(spec)
spec.input("x", valid_type=(Int, Float))
spec.input("y", valid_type=(Int, Float))
spec.input("z", valid_type=Int, validator=validate_z)
spec.inputs.validator = validate_inputs
spec.outline(cls.multiply, cls.add, cls.result)
spec.output("workchain_result", valid_type=(Int, Float))
spec.output("product", valid_type=(Int, Float))
def multiply(self):
"""Multiply two integers."""
multiplication_result = multiplication(self.inputs.x, self.inputs.y)
# Passing to context to be used by other functions
self.ctx.product = multiplication_result
def add(self):
"""Add two numbers."""
# Call `addition` using a variable from the context and one of the inputs
addition_result = addition(self.ctx.product, self.inputs.z)
# Passing to context to be used by other functions
self.ctx.summation = addition_result
def result(self):
"""Parse the result."""
# Declaring the output
self.out("workchain_result", self.ctx.summation)
self.out("product", self.ctx.product)
Note how the valid_type
of the outputs also has to be more flexible!