Handling complex workflows using the workflow builders

The WorkflowBuilder and MultipleWorkflowBuilder classes are designed to manage extended workflows consisting of a conjunction of different AiiDA processes. Input-parameters and logical dependencies can be defined in a protocol which is given as a specifically formated python-dictionary or written in a yaml-file. The objects can determine the current state (accomplished tasks) of a workflow for a given AiiDA parent-node and generate the input-parameters for the next task of the workflow. While the WorkflowBuilder class controls the workflow for a single parent-node the MultipleWorkflowBuilder class consists of several WorkflowBuilder’s instances and can handle multiple parent-nodes using the same workflow protocol for each.

In the following we use the WorkflowBuilder class to examplify the user interface, however, setting the workflow protocol and input parameters works the same for both objects. First, the appropriate AiiDA profile is loaded and an instance of the class is created:

[1]:
from aim2dat.aiida_workflows.workflow_builder import WorkflowBuilder
import aiida

aiida.load_profile("tests")
wf_builder = WorkflowBuilder()
---------------------------------------------------------------------------
ProfileConfigurationError                 Traceback (most recent call last)
Cell In[1], line 4
      1 from aim2dat.aiida_workflows.workflow_builder import WorkflowBuilder
      2 import aiida
----> 4 aiida.load_profile("tests")
      5 wf_builder = WorkflowBuilder()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/configuration/__init__.py:165, in load_profile(profile, allow_switch)
    152 """Load a global profile, unloading any previously loaded profile.
    153
    154 .. note:: if a profile is already loaded and no explicit profile is specified, nothing will be done
   (...)
    161     if another profile has already been loaded and allow_switch is False
    162 """
    163 from aiida.manage import get_manager
--> 165 return get_manager().load_profile(profile, allow_switch)

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:117, in Manager.load_profile(self, profile, allow_switch)
    114     return self._profile
    116 if profile is None or isinstance(profile, str):
--> 117     profile = self.get_config().get_profile(profile)
    118 elif not isinstance(profile, Profile):
    119     raise TypeError(f'profile must be None, a string, or a Profile instance, got: {type(profile)}')

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/configuration/config.py:453, in Config.get_profile(self, name)
    450 if not name:
    451     name = self.default_profile_name
--> 453 self.validate_profile(name)
    455 return self._profiles[name]

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/configuration/config.py:435, in Config.validate_profile(self, name)
    432 from aiida.common import exceptions
    434 if name not in self.profile_names:
--> 435     raise exceptions.ProfileConfigurationError(f'profile `{name}` does not exist')

ProfileConfigurationError: profile `tests` does not exist

The workflow protocol

The workflow protocols consists of three different sections:

  • tasks: Is a dictionary containing the details and dependencies for the tasks that can be run with the current workflow.

  • general_input: defines the preset parameters shared by all work chains.

  • user_input: defines input parameters that are set by the user.

All predefined protocols are found in the folder: “aim2dat/aim2dat/aiida_workflows/protocols/”. The workflow protocols support versions, which the suffix "_v*.*" (* denotes an integer number) a specific protocol version can be chosen. If the suffix is omitted the latest protocol version is chosen. At the moment the following protocols are implemented:

Protocol

Latest version

Description

arithmetic-testing

v1.1

Protocol for testing purposes.

seekpath-standard

v1.0

Protocol for a seek-path analysis.

cp2k-crystal-mofs

v2.0

Protocol to run DFT calculations on MOFs using CP2K.

cp2k-crystal-preopt

v3.1

Protocol to pre-optimize inorganic crystals with loose parameters using CP2K.

cp2k-crystal-standard

v3.2

Standard protocol to run DFT calculations on inorganic crystals using CP2K (doi:10.1063/5.0082710).

cp2k-crystal-standard-keep-angles

v1.1

Standard protocol for inorganic crystals but constraining lattice parameters.

cp2k-surface-standard

v1.0

Protocol to run the surface workflow using CP2K.

cp2k-crystal-testing

v2.0

Protocol to test the workflow for inorganic crystals with loose parameters using CP2K.

cp2k-surface-testing

v1.0

Protocol to test the surface workflow with loose parameters using CP2K.

The protocol can be loaded by using the property protocol <aim2dat.aiida_workflows.workflow_builder.WorkflowBuilder.protocol>` (same property for both classes), in this case we use a test protocol that is merely based on the arithmetic add_multiply calcfunction. In general workflows can combine any kind of AiiDA processes defining input-parameters and dependencies.

[2]:
wf_builder.protocol = "arithmetic-testing"
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 wf_builder.protocol = "arithmetic-testing"

NameError: name 'wf_builder' is not defined

All tasks of the workflow can be printed with the property tasks <aim2dat.aiida_workflows.workflow_builder.WorkflowBuilder.tasks>`:

[3]:
wf_builder.tasks
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 wf_builder.tasks

NameError: name 'wf_builder' is not defined

Setting up the input parameters and parent node

The provenance of the workflow is defined via the parent node, it is input for all initial tasks of the workflow. Here, we create a new aiida node without history and pass it to the builder-object:

[4]:
from aiida.plugins import DataFactory

Float = DataFactory("core.float")
parent_node = Float(4)

wf_builder.parent_node = parent_node
---------------------------------------------------------------------------
ConfigurationError                        Traceback (most recent call last)
Cell In[4], line 4
      1 from aiida.plugins import DataFactory
      3 Float = DataFactory("core.float")
----> 4 parent_node = Float(4)
      6 wf_builder.parent_node = parent_node

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/base.py:32, in BaseType.__init__(self, value, **kwargs)
     29 except AttributeError:
     30     raise RuntimeError('Derived class must define the `_type` class member')
---> 32 super().__init__(**kwargs)
     34 self.value = value or self._type()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/data.py:49, in Data.__init__(self, source, *args, **kwargs)
     47 def __init__(self, *args, source=None, **kwargs):
     48     """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument."""
---> 49     super().__init__(*args, **kwargs)
     50     if source is not None:
     51         self.source = source

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/node.py:194, in Node.__init__(self, backend, user, computer, **kwargs)
    187 def __init__(
    188     self,
    189     backend: Optional['StorageBackend'] = None,
   (...)
    192     **kwargs: Any,
    193 ) -> None:
--> 194     backend = backend or get_manager().get_profile_storage()
    196     if computer and not computer.is_stored:
    197         raise ValueError('the computer is not stored')

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:254, in Manager.get_profile_storage(self)
    252 profile = self.get_profile()
    253 if profile is None:
--> 254     raise ConfigurationError(
    255         'Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.'
    256     )
    258 # request access to the profile (for example, if it is being used by a maintenance operation)
    259 ProfileAccessManager(profile).request_access()

ConfigurationError: Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.

And we can set additional input-parameters (parameters can be given as python types or AiiDA nodes). A dash and subsequent greater than sign (->) highlight an individual input parameter defined for just one task of the workflow.

[5]:
wf_builder.set_user_input("y", 5)
wf_builder.set_user_input("y->task_4.1", 11.0)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 wf_builder.set_user_input("y", 5)
      2 wf_builder.set_user_input("y->task_4.1", 11.0)

NameError: name 'wf_builder' is not defined

Checking the workflow state

At any time we can check the status of the workflow via the method determine_workflow_state:

[6]:
wf_builder.determine_workflow_state()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 wf_builder.determine_workflow_state()

NameError: name 'wf_builder' is not defined

The builder checks whether any work chains with matching input parameters have been performed on the structure. In this case there are no processes run that conform the workflow protocol.

Executing workflow tasks

The input for the initial task can be created using the ‘builder’-method of the AiiDA work chain or calculation or a dictionary of input-parameters for AiiDA calcfunctions:

[7]:
from aiida.engine import run

wc_builder = wf_builder.generate_inputs("task_1.1")
result = run(**wc_builder)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 3
      1 from aiida.engine import run
----> 3 wc_builder = wf_builder.generate_inputs("task_1.1")
      4 result = run(**wc_builder)

NameError: name 'wf_builder' is not defined

If we check the workflow again, we see that the task ‘task_1.1’ is accomplished and we can continue with the next task:

[8]:
wf_builder.determine_workflow_state()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 wf_builder.determine_workflow_state()

NameError: name 'wf_builder' is not defined

Alternatively, we can run or submit the task straightaway using the methods :meth:run_task <aim2dat.aiida_workflows.workflow_builder.WorkflowBuilder.run_task> or :meth:submit_task <aim2dat.aiida_workflows.workflow_builder.WorkflowBuilder.submit_task>. The difference between the two methods is that the first uses AiiDA’s run method which starts the process in the foreground and blocks the interface while the latter uses AiiDA’s submit method which passes the process to the daemon that is running in the background.

[9]:
wf_builder.run_task("task_1.2")
wf_builder.run_task("task_1.3")
wf_builder.run_task("task_2.1")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 wf_builder.run_task("task_1.2")
      2 wf_builder.run_task("task_1.3")
      3 wf_builder.run_task("task_2.1")

NameError: name 'wf_builder' is not defined

Visualizing the provenance graph of the workflow

Using the AiiDA built-in features the provenance graph of the workflow can be plotted:

[10]:
wf_builder.graph_attributes = {"graph_attr": {"size": "6!,6"}}
graph = wf_builder.generate_provenance_graph()
graph.graphviz
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 wf_builder.graph_attributes = {"graph_attr": {"size": "6!,6"}}
      2 graph = wf_builder.generate_provenance_graph()
      3 graph.graphviz

NameError: name 'wf_builder' is not defined

The MultipleWorkflowBuilder class

The main difference between the WorkflowBuilder and the MultipleWorkflowBuilder class is that the latter hosts a list of parent-nodes and WorkflowBuilder instances:

[11]:
from aim2dat.aiida_workflows.workflow_builder import MultipleWorkflowBuilder

mwf_builder = MultipleWorkflowBuilder()
mwf_builder.protocol = "arithmetic-testing"

for n in range(0, 5):
    mwf_builder.add_parent_node(Float(n))
---------------------------------------------------------------------------
ConfigurationError                        Traceback (most recent call last)
Cell In[11], line 4
      1 from aim2dat.aiida_workflows.workflow_builder import MultipleWorkflowBuilder
      3 mwf_builder = MultipleWorkflowBuilder()
----> 4 mwf_builder.protocol = "arithmetic-testing"
      6 for n in range(0, 5):
      7     mwf_builder.add_parent_node(Float(n))

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:157, in _BaseWorkflowBuilder.protocol(self, value)
    155 if input_cat in protocol:
    156     for input_p, input_details in protocol[input_cat].items():
--> 157         _validate_input_details(input_details)
    158         input_p_sp = input_p.split("->")
    159         if len(input_p_sp) > 1:

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:62, in _validate_input_details(input_details)
     60     input_details["aiida_node"] = False
     61 if input_details["aiida_node"] and "value" in input_details:
---> 62     input_details["value"] = create_aiida_node(input_details["value"])
     63 else:
     64     input_details["value"] = None

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/utils.py:272, in create_aiida_node(value, node_type)
    270     aiida_node = aiida_orm.Int(value)
    271 elif node_type == "float" or (check_node_type and isinstance(value, float)):
--> 272     aiida_node = aiida_orm.Float(value)
    273 elif node_type == "str" or (check_node_type and isinstance(value, str)):
    274     aiida_node = aiida_orm.Str(value)

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/base.py:32, in BaseType.__init__(self, value, **kwargs)
     29 except AttributeError:
     30     raise RuntimeError('Derived class must define the `_type` class member')
---> 32 super().__init__(**kwargs)
     34 self.value = value or self._type()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/data.py:49, in Data.__init__(self, source, *args, **kwargs)
     47 def __init__(self, *args, source=None, **kwargs):
     48     """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument."""
---> 49     super().__init__(*args, **kwargs)
     50     if source is not None:
     51         self.source = source

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/node.py:194, in Node.__init__(self, backend, user, computer, **kwargs)
    187 def __init__(
    188     self,
    189     backend: Optional['StorageBackend'] = None,
   (...)
    192     **kwargs: Any,
    193 ) -> None:
--> 194     backend = backend or get_manager().get_profile_storage()
    196     if computer and not computer.is_stored:
    197         raise ValueError('the computer is not stored')

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:254, in Manager.get_profile_storage(self)
    252 profile = self.get_profile()
    253 if profile is None:
--> 254     raise ConfigurationError(
    255         'Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.'
    256     )
    258 # request access to the profile (for example, if it is being used by a maintenance operation)
    259 ProfileAccessManager(profile).request_access()

ConfigurationError: Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.

The user input parameters can be set likewise to the WorkflowBuilder class:

[12]:
mwf_builder.set_user_input("y", 2.0)
mwf_builder.set_user_input("y->task_4.1", 3.0)
---------------------------------------------------------------------------
ConfigurationError                        Traceback (most recent call last)
Cell In[12], line 1
----> 1 mwf_builder.set_user_input("y", 2.0)
      2 mwf_builder.set_user_input("y->task_4.1", 3.0)

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:1165, in MultipleWorkflowBuilder.set_user_input(self, input_port, value)
   1154 def set_user_input(self, input_port, value):
   1155     """
   1156     Set a user input parameter of the workflow for all parent nodes.
   1157
   (...)
   1163         Value of the input parameter.
   1164     """
-> 1165     super().set_user_input(input_port, value)
   1166     for wf_builder in self._wf_builders:
   1167         wf_builder._user_input = self._user_input.copy()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:324, in _BaseWorkflowBuilder.set_user_input(self, input_port, value)
    322     input_details = self._user_input[input_port]
    323 if input_details["aiida_node"] and not hasattr(value, "uuid"):
--> 324     value = create_aiida_node(value)
    325 if input_details["validation"] is not None:
    326     self._validate_work_chain_input(input_port, value, input_details["validation"])

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/utils.py:272, in create_aiida_node(value, node_type)
    270     aiida_node = aiida_orm.Int(value)
    271 elif node_type == "float" or (check_node_type and isinstance(value, float)):
--> 272     aiida_node = aiida_orm.Float(value)
    273 elif node_type == "str" or (check_node_type and isinstance(value, str)):
    274     aiida_node = aiida_orm.Str(value)

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/base.py:32, in BaseType.__init__(self, value, **kwargs)
     29 except AttributeError:
     30     raise RuntimeError('Derived class must define the `_type` class member')
---> 32 super().__init__(**kwargs)
     34 self.value = value or self._type()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/data.py:49, in Data.__init__(self, source, *args, **kwargs)
     47 def __init__(self, *args, source=None, **kwargs):
     48     """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument."""
---> 49     super().__init__(*args, **kwargs)
     50     if source is not None:
     51         self.source = source

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/node.py:194, in Node.__init__(self, backend, user, computer, **kwargs)
    187 def __init__(
    188     self,
    189     backend: Optional['StorageBackend'] = None,
   (...)
    192     **kwargs: Any,
    193 ) -> None:
--> 194     backend = backend or get_manager().get_profile_storage()
    196     if computer and not computer.is_stored:
    197         raise ValueError('the computer is not stored')

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:254, in Manager.get_profile_storage(self)
    252 profile = self.get_profile()
    253 if profile is None:
--> 254     raise ConfigurationError(
    255         'Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.'
    256     )
    258 # request access to the profile (for example, if it is being used by a maintenance operation)
    259 ProfileAccessManager(profile).request_access()

ConfigurationError: Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.

The status information as well as process nodes and workflow results is therefore given as pandas dataframes:

[13]:
mwf_builder.return_workflow_states()
[13]:

Different tasks can be started for all parent-nodes within one function call via the :meth:run_task <aim2dat.aiida_workflows.workflow_builder.MultipleWorkflowBuilder.run_task> or :meth:submit_task <aim2dat.aiida_workflows.workflow_builder.MultipleWorkflowBuilder.submit_task> functions:

[14]:
mwf_builder.run_task("task_1.1")
mwf_builder.return_workflow_states()
[14]:

The tasks can be started for a subset of the parent-nodes by using the interval parameter:

[15]:
mwf_builder.run_task("task_1.2", interval=[0, 3])
mwf_builder.return_workflow_states()
[15]:

Several tasks can be started consecutively by setting a task queue:

[16]:
mwf_builder.add_to_task_queue("task_1.2", run_type="run")
mwf_builder.add_to_task_queue("task_1.3", run_type="run")
mwf_builder.add_to_task_queue("task_2.1", run_type="run")
mwf_builder.add_to_task_queue("task_2.2", run_type="run")
mwf_builder.add_to_task_queue("task_3.1", run_type="run")
mwf_builder.add_to_task_queue("task_4.1", run_type="run")
mwf_builder.execute_task_queue()

Additional information can be returned via the functions `return_process_nodes <aim2dat.aiida_workflows.workflow_builder.MultipleWorkflowBuilder.return_process_nodes>`__ and `return_results <aim2dat.aiida_workflows.workflow_builder.MultipleWorkflowBuilder.return_results>`__:

[17]:
mwf_builder.return_process_nodes()
[17]:
[18]:
mwf_builder.return_results()
[18]:

Storing and loading workflows

Both, the WorkflowBuilder and the MultipleWorkflowBuilder have the methods to_file and from_file implemented which allows to store the workflow protocol and process nodes in a yaml-file. This feature can be also used to share the workflow information by exporting/importing the process nodes as well (see the AiiDA documentation for more details).

[19]:
mwf_builder.to_file("test_workflow.yaml")

mwf_builder2 = MultipleWorkflowBuilder.from_file("test_workflow.yaml")
mwf_builder2.return_workflow_states()
---------------------------------------------------------------------------
ConfigurationError                        Traceback (most recent call last)
Cell In[19], line 3
      1 mwf_builder.to_file("test_workflow.yaml")
----> 3 mwf_builder2 = MultipleWorkflowBuilder.from_file("test_workflow.yaml")
      4 mwf_builder2.return_workflow_states()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:273, in _BaseWorkflowBuilder.from_file(cls, file_name)
    271 content = load_yaml_file(file_name)
    272 wf_builder = cls()
--> 273 wf_builder.protocol = content["protocol"]
    274 if "parent_node" in content:
    275     if hasattr(wf_builder, "_wf_builders"):

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:157, in _BaseWorkflowBuilder.protocol(self, value)
    155 if input_cat in protocol:
    156     for input_p, input_details in protocol[input_cat].items():
--> 157         _validate_input_details(input_details)
    158         input_p_sp = input_p.split("->")
    159         if len(input_p_sp) > 1:

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/workflow_builder.py:62, in _validate_input_details(input_details)
     60     input_details["aiida_node"] = False
     61 if input_details["aiida_node"] and "value" in input_details:
---> 62     input_details["value"] = create_aiida_node(input_details["value"])
     63 else:
     64     input_details["value"] = None

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/aiida_workflows/utils.py:272, in create_aiida_node(value, node_type)
    270     aiida_node = aiida_orm.Int(value)
    271 elif node_type == "float" or (check_node_type and isinstance(value, float)):
--> 272     aiida_node = aiida_orm.Float(value)
    273 elif node_type == "str" or (check_node_type and isinstance(value, str)):
    274     aiida_node = aiida_orm.Str(value)

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/base.py:32, in BaseType.__init__(self, value, **kwargs)
     29 except AttributeError:
     30     raise RuntimeError('Derived class must define the `_type` class member')
---> 32 super().__init__(**kwargs)
     34 self.value = value or self._type()

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/data/data.py:49, in Data.__init__(self, source, *args, **kwargs)
     47 def __init__(self, *args, source=None, **kwargs):
     48     """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument."""
---> 49     super().__init__(*args, **kwargs)
     50     if source is not None:
     51         self.source = source

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/nodes/node.py:194, in Node.__init__(self, backend, user, computer, **kwargs)
    187 def __init__(
    188     self,
    189     backend: Optional['StorageBackend'] = None,
   (...)
    192     **kwargs: Any,
    193 ) -> None:
--> 194     backend = backend or get_manager().get_profile_storage()
    196     if computer and not computer.is_stored:
    197         raise ValueError('the computer is not stored')

File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:254, in Manager.get_profile_storage(self)
    252 profile = self.get_profile()
    253 if profile is None:
--> 254     raise ConfigurationError(
    255         'Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.'
    256     )
    258 # request access to the profile (for example, if it is being used by a maintenance operation)
    259 ProfileAccessManager(profile).request_access()

ConfigurationError: Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.