使用方法#

备注

本章假定读者了解 basic concept 的知识,并知道计算功能和计算作业之间的区别,以及何时应使用其中一个或另一个。

计算是 创建 新数据的过程(详见 process section)。目前,有两种实现计算过程的方法:

本节将提供如何实施这两种计算类型的详细信息和最佳实践。

计算功能#

关于 concept of calculation functions 的章节已经阐述了它们的目的:在 provenance graph 中自动记录它们的执行及其输入和输出。随后,section on process functions 详细说明了执行这些功能时适用的规则,所有这些规则都适用于计算功能,计算功能与工作功能一样,都是子类型。不过,计算函数是类似 ‘calculation’ 的流程,而工作函数则是类似 ‘workflow’ 的流程,两者之间存在一些差异。本节将介绍计算函数的预期用途和限制。

创建数据#

计算函数和所有类似 ‘计算(calculation)’ 的过程一样,都会 create 数据,但 create 的确切含义是什么?在这里,’创建’ 一词并不是指在交互式 shell 或脚本中在图形中创建一个新数据 node。而是指通过进程执行的计算,从其他数据中创建新数据。这正是计算函数的作用。它接收一个或多个数据 node 作为输入,并返回一个或多个数据 node 作为输出,其内容基于这些输入。正如 technical section 中解释的那样,只需从函数返回 node 即可创建输出。engine 将检查函数的返回值,并将输出 node 连接到表示计算函数的计算 node。为了验证输出 node 是否确实 ‘创建’,engine 将检查 node 是否被存储。因此,请务必不要存储自己创建的 node,否则 engine 将引发异常,如下例所示:

from aiida.engine import calcfunction
from aiida.orm import Int


@calcfunction
def add(x, y):
    result = Int(x + y).store()
    return result


result = add(Int(1), Int(2))

由于返回的 node 已经存储,engine 将引发以下异常:

ValueError: trying to return an already stored Data node from a @calcfunction, however, @calcfunctions cannot return data.
If you stored the node yourself, simply do not call `store()` yourself.
If you want to return an input node, use a @workfunction instead.

之所以如此严格,是因为在函数体中创建后存储的 node 与已经存储并在函数体中加载后返回的 node 是没有区别的,例如

from aiida.engine import calcfunction
from aiida.orm import Int, load_node


@calcfunction
def add(x, y):
    result = load_node(100)
    return result


result = add(Int(1), Int(2))

加载的 node 也会从计算函数中获得一个 create 链接,尽管它实际上根本不是由计算函数创建的。正是为了防止出现这种含糊不清的情况,计算函数要求所有返回的输出 node 必须 未存储

请注意,工作函数的要求恰恰相反,它返回的所有输出 必须存储 ,因为作为一个类似于 workflow 的进程,它 不能 创建新数据。更多详情请参考 work function section

计算工作#

为了解释如何执行计算作业,我们将继续以 concept of the calculation job 为例。在那里,我们描述了一个以简单 bash 脚本实现的两个整数相加的代码,以及如何使用 CalcJob 类通过 AiiDA 运行该代码。由于它是 Process 类的子类,因此共享其所有属性。在继续阅读之前,请先阅读有关 generic processes 的章节,这将是非常有价值的,因为那里解释的所有概念也适用于计算作业。

定义#

要执行计算作业,只需将 CalcJob 进程类子类化,并执行 define() 方法即可。你可以选择任何有效的 python 类名。 CalcJob 类中最重要的 important 方法是 define 类方法。您可以在这里定义它的输入和输出。

from aiida import engine, orm


class ArithmeticAddCalculation(engine.CalcJob):
    """Implementation of CalcJob to add two numbers for testing and demonstration purposes."""

    @classmethod
    def define(cls, spec):
        super().define(spec)
        spec.input('x', valid_type=orm.Int, help='The left operand.')
        spec.input('y', valid_type=orm.Int, help='The right operand.')

如上图所示,类方法需要两个参数:

  • cls 这是类本身的引用,对于任何类方法都是强制性的

  • spec 是 ‘规范’

警告

不要忘记在 define 方法的第一行添加 super().define(spec) 行,并用计算作业的名称替换类名称。这将调用父类的 define 方法,这是计算作业正常工作所必需的

顾名思义, spec 可用来指定计算作业的属性。例如,它可以用来定义计算作业的输入。在我们的示例中,我们需要传递两个整数作为输入,因此我们在规范中调用 spec.input() 来定义这两个整数。第一个参数是输入的名称。这个名称将在以后启动计算作业时用于指定输入,同时也将用作连接 provenance graph 中数据 node 和计算 node 的链接标签。此外,正如我们在这里所做的,可以指定哪些类型对特定输入有效。由于我们希望输入整数,因此指定有效类型为数据库可存储的 Int 类。

备注

由于我们是 CalcJob 的子类并调用了它的 define 方法,因此它也将继承它所声明的端口。如果查看一下实现,就会发现基类 CalcJob 已经定义了一个接收 Code 实例的输入 code 。当用户启动 CalcJob 时,它将引用用户希望运行的代码。因此,您***不必再次声明此输入。

接下来,我们应该定义我们希望计算产生哪些输出结果:

from aiida import engine, orm


class ArithmeticAddCalculation(engine.CalcJob):
    """Implementation of CalcJob to add two numbers for testing and demonstration purposes."""

    @classmethod
    def define(cls, spec):
        super().define(spec)
        spec.input('x', valid_type=orm.Int, help='The left operand.')
        spec.input('y', valid_type=orm.Int, help='The right operand.')
        spec.output('sum', valid_type=orm.Int, help='The sum of the left and right operand.')

与输入一样,我们也可以指定每个输出的 node 类型。默认情况下,已定义的输出将是 必填,这意味着如果计算作业结束时没有附加输出,进程将被标记为失败。要表示输出是可选的,可以在 spec.output 调用中使用 required=False 。请注意,进程规范及其 input()output() 方法提供了更多的功能。更多详情,请参阅 process specifications 章节。

准备#

现在,我们已经通过流程规范定义了计算作业所期望的输入和将创建的输出。剩下的最后一项任务是指导 engine 如何实际运行计算作业。为了了解 engine 要完成这项任务需要做些什么,让我们考虑一下通过调度程序手动准备运行计算作业时通常要做的事情:

  • 在运行任务的计算机上的某个抓取空间中准备一个工作目录

  • 创建可执行文件所需的原始输入文件

  • 创建一个包含调度指令的启动脚本,加载环境变量,最后使用某些命令行参数调用可执行文件。

因此,我们现在需要做的就是指示 engine 如何完成这些特定计算任务。由于这些指令将取决于计算,因此我们将使用 prepare_for_submission() 方法来实现。我们在示例中考虑的 ArithmeticAddCalculation 实现如下:

from aiida.common.datastructures import CalcInfo, CodeInfo
from aiida.engine import CalcJob
from aiida.orm import Int


class ArithmeticAddCalculation(CalcJob):
    """Implementation of CalcJob to add two numbers for testing and demonstration purposes."""

    @classmethod
    def define(cls, spec):
        super().define(spec)
        spec.input('x', valid_type=Int, help='The left operand.')
        spec.input('y', valid_type=Int, help='The right operand.')
        spec.output('sum', valid_type=Int, help='The sum of the left and right operand.')

    def prepare_for_submission(self, folder):
        """Write the input files that are required for the code to run.

        :param folder: an `~aiida.common.folders.Folder` to temporarily write files on disk
        :return: `~aiida.common.datastructures.CalcInfo` instance
        """
        input_x = self.inputs.x
        input_y = self.inputs.y

        # Write the input file based on the inputs that were passed
        with folder.open(self.options.input_filename, 'w', encoding='utf8') as handle:
            handle.write(f'{input_x.value} {input_y.value}\n')

        codeinfo = CodeInfo()
        codeinfo.code_uuid = self.inputs.code.uuid
        codeinfo.stdout_name = self.options.output_filename
        codeinfo.cmdline_params = ['-in', self.options.input_filename]

        calcinfo = CalcInfo()
        calcinfo.codes_info = [codeinfo]
        calcinfo.local_copy_list = []
        calcinfo.remote_copy_list = []
        calcinfo.retrieve_list = []

        return calcinfo

在逐行阅读代码之前,让我们先描述一下这里发生的大事。本方法的目的是帮助 engine 完成上述准备提交计算作业所需的三个步骤。所需的原始输入文件可以写入作为 folder 参数传递的沙盒文件夹中。

备注

folder 参数指向本地文件系统上的临时沙盒文件夹,可用于写入输入文件。 prepare_for_submission 方法返回后,engine 将把这些内容复制到运行计算的工作目录中。此外,这些文件还将写入代表计算的 node 的文件存储库,作为 provenance 的额外措施。尽管写入的信息应该是作为输入 node 传递的 node 内容的派生形式,但由于它是派生形式,我们还是会明确存储这些信息。有时,出于效率或数据隐私等原因,这种行为并不可取,因此可以使用 local_copy_listprovenance_exclude_list 等各种列表来控制。

所有其他所需的信息,如复制哪些文件的指令和使用哪些命令行选项,都是通过 CalcInfo 数据结构定义的,它应作为唯一的值从方法中返回。原则上,这就是 prepare_for_submission 方法**应该做的事情:

  • 将计算运行所需的原始输入文件写入 folder 沙盒文件夹。

  • 使用 CalcInfo 指示 engine 将哪些文件复制到工作目录中

  • 使用 CalcInfo 命令行参数(如标准输入和输出重定向)来指示应运行哪些代码。

备注

prepare_for_submission 不必自己编写提交脚本。engine 知道如何编写脚本,因为要使用的代码已在特定计算机上进行了配置,其中定义了要使用的调度程序。这就为 engine 提供了如何编写启动脚本的所有必要信息,例如要编写哪些调度程序指令。

既然我们已经知道了 prepare_for_submission 的预期功能,下面就让我们逐行看看 ArithmeticAddCalculation 是如何实现的。本示例计算所需的输入文件将包括作为输入传递的两个整数。根据 define 方法中定义的流程规范, self.inputs 属性会返回一个属性字典,其中包含经过解析和验证的输入。这意味着您不必亲自验证输入。也就是说,如果一个输入被标记为必填,并具有某种类型,那么当我们进入 prepare_for_submission 时,就可以保证 self.inputs 返回的字典将包含该输入,并且类型正确。

根据计算作业启动时传递的两个输入 x and y ,我们现在应该生成输入文件,即一个简单的文本文件,将这两个数字写在一行中,并用空格隔开。为此,我们将在沙盒文件夹中打开一个指向输入文件的文件句柄,并将两个 Int node 的值写入该文件。

备注

这个输入文件的格式恰好是我们在本例中使用的 bash script 所期望的格式。当然,输入文件的确切数量及其内容将取决于为其编写计算作业的代码。

写入输入文件后,我们现在必须创建一个 CalcInfo 的实例,该实例应从方法中返回。该数据结构将准确地指示 engine 执行代码所需的操作,例如应将哪些文件复制到执行代码的远程计算机。在这个简单的示例中,我们定义了四个简单的属性:

  • codes_infoCodeInfo 数据结构列表,用于说明在工作中要连续运行哪些代码

  • local_copy_list :指示将哪些文件从本地计算机复制到工作目录的图元列表

  • remote_copy_list :指示将哪些文件从运行任务的计算机复制到工作目录的图元列表

  • retrieve_list :一个图元列表,指示在任务完成后,应从工作目录中检索哪些文件并将其存储在本地存储库中

See Controlling order of file copying for details on the order in which input files are copied to the working directory. In this example we only need to run a single code, so the codes_info list has a single CodeInfo datastructure. This datastructure needs to define which code it needs to run, which is one of the inputs passed to the CalcJob, and does so by means of its UUID. Through the stdout_name attribute, we tell the engine where the output of the executable should be redirected to. In this example this is set to the value of the output_filename option. What options are available in calculation jobs, what they do and how they can be set will be explained in the section on options. Finally, the cmdline_params attribute takes a list with command line parameters that will be placed after the executable in the launch script. Here we use it to explicitly instruct the executable to read its input from the filename stored in the option input_filename.

备注

由于我们指示可执行文件从 self.options.input_filename 读取输入,因此这也是我们在沙盒文件夹中编写输入文件时使用的文件名。

最后,我们必须定义各种 ``file lists``,这些 ``file lists`` 可以告诉我们从哪里复制哪些文件到哪里,以及检索哪些文件。在此,我们将简要介绍它们的预期目标。具体实现细节将在 file lists section 中详细介绍。

本地复制列表用于指示 engine 复制您可能已经存储在数据库中的文件,例如 SinglefileData nodes 实例,您可以将其定义并作为 CalcJob 的输入传递。当然,你也可以将它们复制到 folder 沙盒文件夹中,这样也可以将它们写入工作目录。但这种方法的缺点是,写入沙盒文件夹的所有内容也将存储在 CalcJobNode 的存储库中,而 CalcJobNode 将代表 provenance graph 中 CalcJob 的执行。这将导致这些数据 node 中所含数据的重复。如果不将它们明确写入沙盒文件夹,就可以避免这种重复,同时不会丢失 provenance,因为数据 node 本身当然会记录在 provenance graph 中。

远程复制列表可以避免在 engine 运行的机器和执行计算作业的机器之间进行不必要的文件传输。例如,假设您已经在远程集群上完成了一个计算作业,现在想要启动第二个计算作业,而第二个计算作业需要将第一次运行的某些输出文件作为输入。通过远程拷贝列表,您可以准确指定要拷贝到远程工作目录的输出文件,而无需在这之间将它们检索到 engine 的机器上。

最后,检索列表允许您指示 engine 在任务结束后从工作目录中检索哪些文件。这些文件将下载到本地计算机,存储在 FolderData 数据 node 中,并作为输出附加到 CalcJobNode ,链接标签为 retrieved

备注

在上图的 ArithmeticAddCalculation 实现示例中,我们没有将 retrieved 文件夹数据 node 明确定义为输出。这是因为 CalcJob 基类已对此进行了定义。与 code 输入一样, retrieved 输出也是所有计算作业实现的共同点。

文件列表#

本地副本列表#

本地复制列表包含长度为 3 的元组,每个元组代表一个要复制的文件或目录,通过以下项目定义:

  • node uuid:存储库中包含文件的 node,通常是 SinglefileDataFolderData node

  • source relative path:node 资源库中文件或目录的相对路径

  • target relative path:要复制文件或目录内容的工作目录中的相对路径

举例来说, CalcJob 执行程序在接收到名称为 pseudopotentialSinglefileData node 输入后,可以指定复制其内容:

calc_info.local_copy_list = [(self.inputs.pseudopotential.uuid, self.inputs.pseudopotential.filename, 'pseudopotential.dat')]

根据定义, SinglefileData node 只包含一个文件,其相对路径由 filename 属性返回。如果需要从 FolderData 传输一个特定文件,可以像这样指定文件的显式密钥:

calc_info.local_copy_list = [(self.inputs.folder.uuid, 'internal/relative/path/file.txt', 'relative/target/file.txt')]

请注意,相对源路径和目标路径中的文件名不必相同。这完全取决于文件在 node 资源库中的存储方式,以及哪些文件需要写入工作目录。

要复制源 node 目录的内容,只需将其定义为 source relative path。例如,假设我们有一个作为 folder 输入传递的 FolderData node,其存储库虚拟层次结构如下:

├─ sub
│  └─ file_b.txt
└─ file_a.txt

如果需要复制整个内容,请按如下方式指定 local_copy_list

calc_info.local_copy_list = [(self.inputs.folder.uuid, '.', None)]

这里的 '.' 表示需要复制全部内容。或者,也可以指定一个子目录,例如

calc_info.local_copy_list = [(self.inputs.folder.uuid, 'sub', None)]

最后,target relative path 可用于将源代码库的内容写入工作目录中的特定子目录。例如,下面的语句

calc_info.local_copy_list = [(self.inputs.folder.uuid, 'sub', 'relative/target')]

将导致计算工作目录中的文件层次结构如下:

└─ relative
   └─ target
       └─ file_b.txt

我们可能会想,既然可以使用普通 API 将文件写入 folder 沙盒文件夹,那么列表的目的何在?的确,通过这种方式,文件会被复制到工作目录,但同时也会被复制到计算库 node。由于在这种情况下,它只是对文件的直接一对一复制,而文件已经是输入 node 的一部分(未改动的形式),因此这种复制是不必要的,而且会增加文件库的无用重量。使用 local_copy_list 可以防止文件内容不必要的重复。如果特定输入 node 的内容对隐私敏感,且不能在存储库中复制,也可以使用该功能。

Provenance 排除列表#

The local_copy_list allows one to instruct the engine to write files from the input files to the working directory, without them also being copied to the file repository of the calculation node. As discussed in the corresponding section, this is useful in order to avoid duplication or in case where the data of the nodes is proprietary or privacy sensitive and cannot be duplicated arbitrarily everywhere in the file repository. However, the limitation of the local_copy_list is that the it can only target single files in its entirety and cannot be used for arbitrary files that are written to the folder sandbox folder. To provide full control over what files from the folder are stored permanently in the calculation node file repository, the provenance_exclude_list is introduced. This CalcInfo attribute is a list of filepaths, relative to the base path of the folder sandbox folder, which are not stored in the file repository.

请看以下由 prepare_for_submission 实现写入 folder 沙盒的文件结构:

├─ sub
│  ├─ file_b.txt
│  └─ personal.dat
├─ file_a.txt
└─ secret.key

显然,我们不希望 personal.datsecret.key 文件最终永久保存在文件库中。这可以通过定义

calc_info.provenance_exclude_list = ['sub/personal.dat', 'secret.key']

有了这一规范,计算 node 资源库的最终内容将包括

├─ sub
│  └─ file_b.txt
└─ file_a.txt

远程复制列表#

远程复制列表包含长度为 3 的元组,每个元组代表一个要在远程机器上复制的文件,计算将在远程机器上进行,具体定义如下:

  • computer uuid:这是源文件所在的 Computer 的 UUID。目前,远程拷贝列表只能拷贝运行任务的同一台机器上的文件。

  • source absolute path:远程计算机上源文件的绝对路径

  • target relative path:要将文件复制到的工作目录中的相对路径

calc_info.remote_copy_list[(self.inputs.parent_folder.computer.uuid, 'output_folder', 'restart_folder')]

请注意,源路径可以指向一个目录,在这种情况下,其内容将被全部递归复制。

检索列表#

检索列表是一个指令列表,说明计算作业结束后,engine 应检索哪些文件和文件夹。每条指令有两种格式:

  • 字符串,表示远程工作目录中的相对文件路径

  • 长度为 3 的元组,用于控制检索文件夹中的检索文件或文件夹的名称

检索列表可以包含任意数量的指令,并且可以同时使用两种格式。第一种格式显然是最简单的,但需要知道要检索的文件或文件夹的确切名称,此外,在检索时将忽略任何子目录。如果不知道确切的文件名,需要使用 glob patterns,或者需要(部分)保留原始文件夹结构,则应使用元组格式,其格式如下:

  • source relative path:要检索的文件或目录与远程工作目录的相对路径。

  • target relative path`:将源内容复制到的检索文件夹中目录的相对路径。字符串 ``.’`` 表示检索文件夹的顶层。

  • depth:从最深的文件开始复制时,要保持的源路径嵌套层数。

为了说明各种可能性,请看远程工作目录中的以下文件层次结构示例:

├─ path
|  ├── sub
│     ├─ file_c.txt
│     └─ file_d.txt
|  └─ file_b.txt
└─ file_a.txt

下面是要检索的文件和文件夹的各种用例。每个示例都以 retrieve_list 的格式开头,然后以 schematic 描述在检索的文件夹中创建的最终文件层次结构。

明确文件或文件夹#

检索单个顶层文件或文件夹(及其所有内容),最终文件夹结构不是 important。

retrieve_list = ['file_a.txt']

└─ file_a.txt
retrieve_list = ['path']

├── sub
│   ├─ file_c.txt
│   └─ file_d.txt
└─ file_b.txt
显式嵌套文件或文件夹#

检索位于远程工作目录子目录中的单个文件或文件夹(及其所有内容),最终文件夹结构不 important。

retrieve_list = ['path/file_b.txt']

└─ file_b.txt
retrieve_list = ['path/sub']

├─ file_c.txt
└─ file_d.txt
显式嵌套文件或文件夹保持(部分)层次结构#

下面的示例展示了如何控制检索文件的文件层次结构。通过更改元组中的 depth 参数,可以控制远程文件夹层次结构的哪一部分被保留。在给出的示例中,远程文件夹层次结构的最大深度为 3. The following example shows that by specifying 3 ,因此会保留确切的文件夹结构:

retrieve_list = [('path/sub/file_c.txt', '.', 3)]

└─ path
    └─ sub
       └─ file_c.txt

对于 0EEEE13EE,只保留两级嵌套(包括文件本身),因此 path 文件夹被丢弃。

retrieve_list = [('path/sub/file_c.txt', '.', 2)]

└─ sub
   └─ file_c.txt

目录也是如此。通过为第一个元素指定一个目录,将检索到该目录的所有内容。如果使用 depth=1 ,则只保留文件夹层次结构的第一层 sub

retrieve_list = [('path/sub', '.', 1)]

└── sub
    ├─ file_c.txt
    └─ file_d.txt
模式匹配#

如果事先不知道确切的文件或文件夹名称,可以使用 glob 模式。在下面的示例中,将检索 path/sub 目录中与 *c.txt 匹配的所有文件。

为保持文件夹结构,将 depth 设置为 None

retrieve_list = [('path/sub/*c.txt', '.', None)]

└─ path
    └─ sub
       └─ file_c.txt

另外, depth 也可用于指定应保留的嵌套层数。例如, depth=0 指示复制匹配的文件,但不包含任何子文件夹:

retrieve_list = [('path/sub/*c.txt', '.', 0)]

└─ file_c.txt

depth=2 将在最终文件路径中保留两级:

retrieve_list = [('path/sub/*c.txt', '.', 2)]

└── sub
    └─ file_c.txt
特定目标目录#

检索文件夹中检索文件的最终文件夹层次结构不仅由远程工作目录的层次结构决定,还可以通过指令图元的第二和第三元素进行控制。最后的 depth 元素控制源代码的层次结构,其中第二个元素指定检索文件夹中的基本路径,远程文件应检索到该路径中。例如,要检索嵌套文件,保持远程层次结构并将其本地存储在 target 目录中,可以采用以下方法:

retrieve_list = [('path/sub/file_c.txt', 'target', 3)]

└─ target
    └─ path
        └─ sub
           └─ file_c.txt

这同样适用于要检索的文件夹:

retrieve_list = [('path/sub', 'target', 1)]

└─ target
    └── sub
        ├─ file_c.txt
        └─ file_d.txt

请注意,此处的 target 并非用于重命名检索到的文件或文件夹,而是表示将源文件复制到其中的目录路径。目标相对路径也与源相对路径中的 glob 模式兼容:

retrieve_list = [('path/sub/*c.txt', 'target', 0)]

└─ target
    └─ file_c.txt

检索临时列表#

请注意,正如 ‘prepare’ section 中所述,engine 根据 ‘检索列表’ 检索的所有文件都存储在 retrieved 文件夹数据 node 中。这意味着您为已完成的计算作业检索的任何文件都将存储在您的存储库中。如果要检索大文件,这会导致存储库大幅增长。但通常情况下,您可能只需要这些检索文件中包含的部分信息。为了解决这个常见问题,我们提出了 ‘检索临时列表’ 的概念。检索临时列表的规格与普通 retrieve list 相同,但它被添加到 calc_inforetrieve_temporary_list 属性下:

calcinfo = CalcInfo()
calcinfo.retrieve_temporary_list = ['relative/path/to/file.txt']

唯一不同的是,检索列表中的文件将永久保存在检索的 FolderData node 中,而检索临时列表中的文件将保存在临时沙盒文件夹中。如果为计算任务指定了 retrieved_temporary_folder 关键字参数,则该文件夹将传递给 parserparse 方法:

def parse(self, **kwargs):
    """Parse the retrieved files of the calculation job."""

    retrieved_temporary_folder = kwargs['retrieved_temporary_folder']

然后,解析器就可以解析这些文件,并将相关信息存储为输出 nodes。

重要

kwargs['retrieved_temporary_folder'] 的类型是简单的 str ,表示临时文件夹的 absolute 文件路径。您可以使用 os 标准库模块访问其内容,或将其转换为 pathlib.Path 文件。

解析器结束后,engine 会自动清理沙盒文件夹中临时获取的文件。 retrieve_temporary_list 的基本概念是,文件在解析过程中可用,解析结束后立即销毁。

Controlling order of file copying#

Added in version 2.6.

Input files can come from three sources in a calculations job:

  1. Sandbox: files that are written by the CalcJob plugin in the prepare_for_submission() to the sandbox folder

  2. Local: files of input nodes that are defined through the local_copy_list

  3. Remote: files of RemoteData input nodes that are defined through the remote_copy_list

By default, these files are copied in the order of sandbox, local, and finally remote. The order can be controlled through the file_copy_operation_order attribute of the CalcInfo which takes a list of FileCopyOperation instances, for example:

class CustomFileCopyOrder(CalcJob)

    def prepare_for_submission(self, _):
        from aiida.common.datastructures import CalcInfo, CodeInfo, FileCopyOperation

        code_info = CodeInfo()
        code_info.code_uuid = self.inputs.code.uuid
        calc_info = CalcInfo()
        calc_info.codes_info = [code_info]
        calc_info.file_copy_operation_order = [
            FileCopyOperation.LOCAL,
            FileCopyOperation.REMOTE,
            FileCopyOperation.SANDBOX,
        ]
        return calc_info

藏在遥控器上#

stash 选项命名空间允许用户指定将计算作业创建的某些文件和/或文件夹存放在运行作业的远程计算机上。如果这些文件和/或文件夹需要在定期清理划痕空间的机器上存储较长时间,但又需要保留在远程机器上而无法检索,那么这个选项就非常有用。例如,重新启动计算所需的文件,但这些文件太大,无法检索并永久保存在本地文件库中。

需要存储的文件/文件夹通过 stash.source_list 选项指定其在工作目录中的相对文件路径。使用 COPY 模式时,目标路径定义了要将文件复制到的另一个位置(与计算位于同一文件系统中),例如通过 stash.target_base 选项进行设置:

from aiida.common.datastructures import StashMode

inputs = {
    'code': ....,
    ...
    'metadata': {
        'options': {
            'stash': {
                'source_list': ['aiida.out', 'output.txt'],
                'target_base': '/storage/project/stash_folder',
                'stash_mode': StashMode.COPY.value,
            }
        }
    }
}

备注

今后还可能采用其他匿藏方法,例如将所有文件放在一个(压缩的)压缩包中,甚至将文件匿藏在磁带上。

重要

如果为计算作业定义了 stash 选项命名空间,守护进程将在检索文件前执行存储操作。这意味着存储操作会在解析输出文件(发生在检索步骤之后)之前进行,因此文件的存储与解析器分配给计算作业的最终退出状态无关。这可能会导致后来被视为失败的计算文件被存储。

藏匿的文件和文件夹由输出 node 表示,该输出通过标签 remote_stash 连接到计算 node,即 RemoteStashFolderData node。与 remote_folder node 一样,它代表远程计算机上的位置或文件,因此相当于 ``symbolic link``。

重要

AiiDA 并不实际控制远程存储库中的文件,因此文件内容可能会在某些时候消失。

选项#

除了所有进程都有的 labeldescription 等常用元数据输入外, CalcJob 还增加了一个名为 options 的输入。这些选项可以微妙地改变计算作业的行为,例如,计算作业完成后应使用哪个解析器,以及特殊的调度程序指令。作为 CalcJob 接口的一部分,可用选项的完整列表记录如下:

calcjobaiida.engine.processes.calcjobs.CalcJob

Implementation of the CalcJob process.

Inputs:

  • code, (AbstractCode, NoneType), optional – The Code to use for this job. This input is required, unless the remote_folder input is specified, which means an existing job is being imported and no code will actually be run.
  • metadata, Namespace
    • call_link_label, str, optional, is_metadata – The label to use for the CALL link if the process is called by another process.
    • computer, (Computer, NoneType), optional, is_metadata – When using a “local” code, set the computer on which the calculation should be run.
    • description, (str, NoneType), optional, is_metadata – Description to set on the process node.
    • disable_cache, (bool, NoneType), optional, is_metadata – Do not consider the cache for this process, ignoring all other caching configuration rules.
    • dry_run, bool, optional, is_metadata – When set to True will prepare the calculation job for submission but not actually launch it.
    • label, (str, NoneType), optional, is_metadata – Label to set on the process node.
    • options, Namespace
      • account, (str, NoneType), optional, is_metadata – Set the account to use in for the queue on the remote computer
      • additional_retrieve_list, (list, tuple, NoneType), optional, is_metadata – List of relative file paths that should be retrieved in addition to what the plugin specifies.
      • append_text, str, optional, is_metadata – Set the calculation-specific append text, which is going to be appended in the scheduler-job script, just after the code execution
      • custom_scheduler_commands, str, optional, is_metadata – Set a (possibly multiline) string with the commands that the user wants to manually set for the scheduler. The difference of this option with respect to the prepend_text is the position in the scheduler submission file where such text is inserted: with this option, the string is inserted before any non-scheduler command
      • environment_variables, dict, optional, is_metadata – Set a dictionary of custom environment variables for this calculation
      • environment_variables_double_quotes, bool, optional, is_metadata – If set to True, use double quotes instead of single quotes to escape the environment variables specified in environment_variables.
      • import_sys_environment, bool, optional, is_metadata – If set to true, the submission script will load the system environment variables
      • input_filename, (str, NoneType), optional, is_metadata – Filename to which the input for the code that is to be run is written.
      • max_memory_kb, (int, NoneType), optional, is_metadata – Set the maximum memory (in KiloBytes) to be asked to the scheduler
      • max_wallclock_seconds, (int, NoneType), optional, is_metadata – Set the wallclock in seconds asked to the scheduler
      • mpirun_extra_params, (list, tuple), optional, is_metadata – Set the extra params to pass to the mpirun (or equivalent) command after the one provided in computer.mpirun_command. Example: mpirun -np 8 extra_params[0] extra_params[1] … exec.x
      • output_filename, (str, NoneType), optional, is_metadata – Filename to which the content of stdout of the code that is to be run is written.
      • parser_name, (str, NoneType), optional, is_metadata – Set a string for the output parser. Can be None if no output plugin is available or needed
      • prepend_text, str, optional, is_metadata – Set the calculation-specific prepend text, which is going to be prepended in the scheduler-job script, just before the code execution
      • priority, (str, NoneType), optional, is_metadata – Set the priority of the job to be queued
      • qos, (str, NoneType), optional, is_metadata – Set the quality of service to use in for the queue on the remote computer
      • queue_name, (str, NoneType), optional, is_metadata – Set the name of the queue on the remote computer
      • rerunnable, (bool, NoneType), optional, is_metadata – Determines if the calculation can be requeued / rerun.
      • resources, dict, required, is_metadata – Set the dictionary of resources to be used by the scheduler plugin, like the number of nodes, cpus etc. This dictionary is scheduler-plugin dependent. Look at the documentation of the scheduler for more details.
      • scheduler_stderr, str, optional, is_metadata – Filename to which the content of stderr of the scheduler is written.
      • scheduler_stdout, str, optional, is_metadata – Filename to which the content of stdout of the scheduler is written.
      • stash, Namespace – Optional directives to stash files after the calculation job has completed.
        • source_list, (tuple, list, NoneType), optional, is_metadata – Sequence of relative filepaths representing files in the remote directory that should be stashed.
        • stash_mode, (str, NoneType), optional, is_metadata – Mode with which to perform the stashing, should be value of aiida.common.datastructures.StashMode.
        • target_base, (str, NoneType), optional, is_metadata – The base location to where the files should be stashd. For example, for the copy stash mode, this should be an absolute filepath on the remote computer.
      • submit_script_filename, str, optional, is_metadata – Filename to which the job submission script is written.
      • withmpi, (bool, NoneType), optional, is_metadata – Set the calculation to use mpi
    • store_provenance, bool, optional, is_metadata – If set to False provenance will not be stored in the database.
  • monitors, Namespace – Add monitoring functions that can inspect output files while the job is running and decide to prematurely terminate the job.
  • remote_folder, (RemoteData, NoneType), optional – Remote directory containing the results of an already completed calculation job without AiiDA. The inputs should be passed to the CalcJob as normal but instead of launching the actual job, the engine will recreate the input files and then proceed straight to the retrieve step where the files of this RemoteData will be retrieved as if it had been actually launched through AiiDA. If a parser is defined in the inputs, the results are parsed and attached as output nodes as usual.

Outputs:

  • remote_folder, RemoteData, required – Input files necessary to run the process will be stored in this folder node.
  • remote_stash, RemoteStashData, optional – Contents of the stash.source_list option are stored in this remote folder after job completion.
  • retrieved, FolderData, required – Files that are retrieved by the daemon will be stored in this node. By default the stdout and stderr of the scheduler will be added, but one can add more by specifying them in CalcInfo.retrieve_list.

rerunnable 选项使调度程序能够在计算失败(例如由于 node 故障或无法启动作业)时重新启动计算。它与 SLURM 中的 --requeue 选项和 SGE、LSF 和 PBS 中的 -r 选项相对应。必须满足以下两个条件,AiiDA 才能正常工作:

  • 调度程序会为重新启动的作业分配相同的作业 ID

  • 如果代码已经部分运行过,则会产生相同的结果(并非每个调度程序都会产生这种情况)

由于这取决于调度程序、其配置和所使用的代码,我们无法断言它何时会工作–请自行测试!我们已在使用 SLURM 的集群上进行了测试,但这并不保证其他 SLURM 集群也会以同样的方式运行。

控制 MPI#

Message Passing Interface (MPI)是一种标准化、可移植的消息传递标准,设计用于并行计算架构。AiiDA 支持在启用或不启用 MPI 的情况下运行计算作业。有许多设置可以用来控制何时以及如何使用 MPI。

Computer#

每个计算作业都在计算资源上执行,计算资源由 Computer 类的实例建模。如果计算机支持使用 MPI 运行,则使用的命令存储在安装的 mpirun_command attribute, which is retrieved and set using the get_mpirun_command() and get_mpirun_command(), respectively. For example, if the computer has OpenMPI 中,可以设置为 mpirun 。如果 Computer 没有指定 MPI 命令,那么为计算作业启用 MPI 将不起作用。

Code#

Added in version 2.3.

当创建代码时,你可以通过设置 with_mpi 属性为 TrueFalse ,告诉AiiDA它应该作为MPI程序运行。从 AiiDA 2.3 起,这是控制 MPI 行为的 推荐 方式。属性可以通过 Python API 设置为 AbstractCode(with_mpi=with_mpi) 或通过 verdi code create CLI 命令的 --with-mpi / --no-with-mpi 选项设置。如果代码可以使用或不使用 MPI 运行,则可以跳过设置 with_mpi 属性。它将默认为 None ,由 CalcJob 插件或用户输入来决定使用或不使用 MPI。

CalcJob 实现#

CalcJob 实现通过从 prepare_for_submission() 方法返回的 CalcInfo 对象指示 AiiDA 如何运行代码。每运行一个代码(通常只有一个), CodeInfo 对象就会被添加到 CalcInfo.codes_info 属性的列表中。如果插件开发人员知道被封装的可执行文件 始终 是 MPI 程序(没有可用的序列版本)或 从未 是 MPI 程序,他们可以将 CodeInfowithmpi 属性分别设置为 TrueFalse 。请注意,这一设置完全是可选的;如果代码可以在两种情况下运行,最好不要设置,而是由 Codemetadata.options.withmpi 输入决定。

备注

在执行运行单一代码的 CalcJob 时,可考虑通过 metadata option 指定启用或禁用 MPI。这可以通过更改进程规范中的默认值来实现:

class SomeCalcJob(CalcJob):

    @classmethod
    def define(cls, spec):
        super().define(spec)
        spec.inputs['metadata']['options']['withmpi'].default = True

与使用 CodeInfo.withmpi 属性相比,它的优势在于元数据选项的默认值可以通过程序从流程规范中反省,因此对用户来说更直观。

当然,这种方法不适用于运行多个代码的计算作业,因为这些代码在是否需要 MPI 方面各不相同。在这种情况下,应该使用 CodeInfo.withmpi 属性。

CalcJob 输入#

最后,可以使用 withmpi metadata option 按实例控制 MPI 设置。如果要启用或禁用 MPI,请将该选项分别显式设置为 TrueFalse 。例如,下面的指令将在启用 MPI 的情况下运行计算作业中的所有代码:

inputs = {
    ...,
    'metadata': {
        'options': {
            'withmpi': True
        }
    }
}
submit(CalcJob, **inputs)

CalcJob 基本实现中,该选项的默认设置为 False ,但如果明确定义,该选项将被覆盖。

备注

withmpi 选项设置的值将应用于所有代码。如果计算作业运行多个代码,且每个代码需要不同的 MPI 设置,则不应使用此选项,而应控制 MPI 为 through the code input

解决冲突#

如上所述,MPI 可以在多个级别上为计算作业启用或禁用:

  • Code 输入

  • CalcJob 实现

  • metadata.options.withmpi 输入

如果这些值中的任何一个被明确设置为 TrueFalse F,则 MPI 将被启用或禁用。如果指定了多个值,但它们并不等同,则会出现 RuntimeError 。根据冲突情况,必须更改 Codemetadata.options.withmpi 输入。如果没有明确定义任何值,则使用默认值 metadata.options.withmpi

启动#

启动计算作业与启动其他进程类并无不同,请参考 launching processes 章节。唯一需要注意的是,计算作业通常会耗费大量时间。我们上面使用的微不足道的示例当然会运行得很快,但提交给调度程序的典型计算作业很可能需要更长的时间,而不仅仅是几秒钟。因此,我们强烈建议 提交 计算作业,而不是运行它们。将计算作业提交给守护进程后,就可以直接释放解释器,并在必须执行的各种 transport tasks 之间对进程进行检查点。当然,为测试或演示目的而在本地运行计算作业的情况除外。

试运行#

与其他所有进程相比,计算作业在启动时还有一个额外的功能。由于配置不正确的计算作业可能会浪费计算资源,因此在实际提交作业之前,可能需要检查插件将写入的输入文件。只需在输入的元数据中指定,就可以进行所谓的 干运行。如果使用流程生成器,则只需

builder.metadata.dry_run = True

现在,当您启动流程生成器时,engine 将执行正常计算作业运行的整个流程,但实际上它不会上传作业并将其提交到远程计算机。但是,将调用 prepare_for_submission 方法。写入输入文件夹的输入将存储在当前工作目录下创建的名为 submit_test 的临时文件夹中。每次执行模拟运行时,都会在 submit_test 文件夹中创建一个新的子文件夹,这样就可以执行多次模拟运行,而不会覆盖之前的结果。

此外,以下情况也适用:

  • 当调用 run() 进行设置了 dry_run 标志的计算时,返回的结果总是空字典 {}

  • 如果调用 run_get_node() ,将返回未存储的 CalcJobNode ,即 node。在这种情况下,未存储的 CalcJobNode (我们称之为 node )将有一个附加属性 node.dry_run_info 。这是一个字典,包含有关干运行输出的附加信息。特别是,它将包含以下键:

    • foldersubmit_test 文件夹内创建文件所在文件夹的绝对路径,例如 /home/user/submit_test/20190726-00019

    • script_filename: the filename of the submission script that AiiDA generated in the folder, e.g.: _aiidasubmit.sh

  • 如果向 submit() 函数发送干运行,则该函数将被转发为运行,并返回未存储的 node(属性如上)。

警告

默认情况下,provenance 的存储是启用的,这也适用于模拟运行。如果不想在模拟运行时创建任何 node,只需将元数据输入 store_provenance 设置为 False

解析#

前面的章节详细解释了如何用 CalcJob 类封装外部可执行文件,使其可由 AiiDA 的 engine 运行。从第一步在远程机器上准备输入文件,到检索相关文件并将其存储在 FolderData node 中,并作为 retrieved 输出附加。这是 CalcJob 终止的最后 必要 步骤,但我们通常 喜欢 解析原始输出,并将其作为可查询的输出 node 附加到计算作业 node。要在检索到计算作业的输出后自动触发解析,可以指定 parser name option 。如果 engine 发现指定了该选项,它将加载相应的解析器类(应为 Parser 的子类)并调用其 parse() 方法。

为了解释 Parser 类的接口和 parse 方法,让我们以 ArithmeticAddParser 为例。该解析器旨在解析由简单的 bash 脚本产生的输出,该脚本由前几节讨论的 ArithmeticAddCalculation 封装。

 1from aiida.orm import Int
 2from aiida.parsers.parser import Parser
 3
 4
 5class ArithmeticAddParser(Parser):
 6    def parse(self, **kwargs):
 7        """Parse the contents of the output files retrieved in the `FolderData`."""
 8        output_folder = self.retrieved
 9
10        try:
11            with output_folder.open(self.node.get_option('output_filename'), 'r') as handle:
12                result = self.parse_stdout(handle)
13        except (OSError, IOError):
14            return self.exit_codes.ERROR_READING_OUTPUT_FILE
15
16        if result is None:
17            return self.exit_codes.ERROR_INVALID_OUTPUT
18
19        self.out('sum', Int(result))
20
21    @staticmethod
22    def parse_stdout(filelike):
23        """Parse the sum from the output of the ArithmeticAddcalculation written to standard out
24
25        :param filelike: filelike object containing the output
26        :returns: the sum
27        """
28        try:
29            result = int(filelike.read())
30        except ValueError:
31            result = None
32
33        return result

要创建新的解析器实现,只需创建一个子类 Parser 的新类即可。像往常一样,任何有效的 python 类名都可以,但约定俗成的做法是始终使用后缀 Parser ,并使用与设计解析器的计算作业相同的名称。例如,在这里我们为 ArithmeticAddCalculation 实现一个解析器,因此我们将其命名为 ArithmeticAddParser ,只是将后缀 Calculation 替换为 Parser 。唯一需要实现的方法是 parse() 方法。其签名应包括 **kwargs ,原因稍后会清楚地说明。 parse 方法的目标非常简单:

  • 打开并加载 engine 提取的由计算任务生成的输出文件内容

  • 从原始数据中创建数据 node,作为输出 node 附上

  • 在输出令人担忧的情况下,记录人类可读的警告信息

  • 可选择返回 exit code 表示计算结果不成功

将不同形式的原始输出数据添加为输出 nodes 的优势在于,这种形式的内容可以查询。这样就可以查询产生特定值的特定输出的计算结果,这对于大型数据库的后处理和分析来说是一种非常强大的方法。

解析器的 retrieved 属性将返回 engine 本应附加的 FolderData node,其中包含所有检索到的文件,如 preparation step of the calculation job 中的 retrieve list 所指定。可以使用检索到的文件夹打开并读取其中包含的文件内容。在本例中,应该有一个输出文件,该文件是通过重定向添加两个整数的 bash 脚本的标准输出而写成的。解析器将打开该文件,读取其内容并尝试解析和:

12                result = self.parse_stdout(handle)
13        except (OSError, IOError):
14            return self.exit_codes.ERROR_READING_OUTPUT_FILE
15
16        if result is None:

请注意,该解析操作被包裹在一个 try-except 块中,以捕获无法读取输出文件时抛出的异常。如果无法捕获异常,engine 将捕获异常,并将相应计算的进程状态设置为 Excepted 。请注意,在解析过程中抛出的任何未捕获异常都会发生这种情况。相反,我们会捕获这些异常并返回一个退出代码,通过 self.exit_codes 属性引用其标签(如本例中的 ERROR_READING_OUTPUT_FILE )即可获取该退出代码。该调用将检索当前正在解析的 CalcJob 上定义的相应退出代码。从解析器返回此退出代码将立即停止解析,并指示 engine 在此计算作业的 node 上设置其退出状态和退出信息。

parse_stdout 方法只是一个小的实用程序函数,用于将数据的实际解析与主解析器代码分开。在本例中,解析过程非常简单,我们不妨将其保留在主方法中,但这只是为了说明,为了清晰起见,你完全可以在 parse 方法中自由组织代码。如果我们设法解析了计算产生的总和,我们就将其封装在相应的 Int 数据 node 类中,并通过 out 方法将其注册为输出:

21    @staticmethod

请注意,如果我们没有遇到任何问题,则不必返回任何信息。engine 会将此理解为计算已成功完成。现在您可以提出一个问题:``what part of the raw data should I parse and in what types of data nodes should I store it?``.一般来说,这个问题不容易回答,因为这在很大程度上取决于计算产生的原始输出类型以及您希望查询的部分。不过,我们可以为您提供一些指导:

  • 将可能需要查询的数据存储在轻量级数据 node 中,如 DictListStructureData 。这些 node 的内容作为属性存储在数据库中,从而确保可以对其进行查询。

  • 大型数据集,如大型(多维)数组,最好存储在 ArrayData 或其子类中。如果将所有这些数据都存储在数据库中,数据库就会变得不必要地臃肿,因为查询这些数据的可能性很小。相反,这些数组类型的数据 node 会将其大部分内容存储在存储库中。这样,您仍然可以保留数据以及计算的 provenance 数据,同时保持数据库的精简和快速!

调度程序错误#

除了输出解析器,调度程序插件还可以通过实施 parse_output() 方法,对作业调度程序生成的输出进行解析。如果调度程序插件执行了该方法,则会解析调度程序生成的输出(写入 stdout 和 stderr 文件描述符以及详细作业信息命令的输出)。如果解析器检测到已知问题,如内存不足(OOM)或超时(OOW)错误,计算作业 node 将已设置相应的退出代码。输出解析器(如果在输入中定义)可以检查 node 上的退出状态,并决定保留该退出状态或使用其他可能更有用的退出代码覆盖该退出状态。

class SomeParser(Parser):

    def parse(self, **kwargs):
        """Parse the contents of the output files retrieved in the `FolderData`."""

        # It is probably best to check for explicit exit codes.
        if self.node.exit_status == self.exit_codes.ERROR_SCHEDULER_OUT_OF_WALLTIME.status:
            # The scheduler parser detected an OOW error.
            # By returning `None`, the same exit code will be kept.
            return None

        # It is also possible to just check for any exit status to be set as a fallback.
        if self.node.exit_status is not None:
            # You can still try to parse files before exiting the parsing.
            return None

请注意,在上面的示例中,如果解析器检测到调度程序检测到问题,就会立即返回。由于它返回 None,调度程序的退出代码将被保留,并成为计算作业的最终退出代码。但是,解析器不必立即返回。它仍可尝试解析检索到的部分输出(如果有的话)。如果它发现了比一般调度程序错误更具体的问题,可以返回一个自己的退出代码来覆盖它。解析器甚至可以返回 ExitCode(0) ,将计算标记为成功完成,尽管调度程序已确定存在问题。下表总结了调度程序解析器和输出解析器返回退出代码的可能情况,以及在 node 上设置的最终退出代码:

情景

日程安排结果

检索结果

最终结果

两个解析器都没有发现问题。

None

None

ExitCode(0)

调度程序解析器发现了一个问题,但输出解析器没有覆盖。

ExitCode(100)

None

ExitCode(100)

只有输出解析器发现了问题。

None

ExitCode(400)

ExitCode(400)

调度程序解析器发现问题,但输出解析器用更具体的错误代码覆盖。

ExitCode(100)

ExitCode(400)

ExitCode(400)

调度程序发现了问题,但输出解析器覆盖说,尽管如此,计算仍应视为成功完成。

ExitCode(100)

ExitCode(0)

ExitCode(0)