用法

注解

本章假设用户已知 :ref:AiiDA基本概念 <topics:calculations:concepts> 和算例函数和算例任务之间的区别,以及何时应该使用。

算例是一个能够创建新数据的例程 (详情参见 例程章节 ) 。 当前,有两种算例的实现,分别是:

该章节提供了关于如何实现这两种算例的详细信息和案例。

算例函数(Calculation functions)

章节 算例函数及其概念 已经描述了其使用目的: 自动在可验证性图中记录算例的运行和运行所需的输入和产生的输出。之后的 The 例程函数章节 详细描述了实现例程函数时候的规则和细节,如同工作函数,规则和实现同时适用于例程函数这一例程函数的子类。但是,算例函数这种 “算例型” 的例程和工作函数这种“工作型” 的例程之间还是有些区别的。 就算例函数的预期使用和限制,还在本节描述。

创建数据

前面曾多次提到: 算例函数,如同所有的“算例型”例程一样,能够 “创建” 数据,但所谓的“创建”具体指什么? 在此处的上下文中,术语“创建”并不简单的描述通过交互式shell或脚本在可验证性图中创建数据节点。 而是指代了通过例程实现的计算中从一些数据产生新的数据的过程。这就是算例函数所实际产生的效果。它以一个或多个数据节点作为输入,并返回以这些输入作为操作对象的一个或多个数据节点作为输出。正如小节 技术细节 所解释的,输出是通过从函数中返回数据节点的方式简单创建的。后端引擎会从函数中检查返回值并将返回节点连接在表示算例函数的算例节点上。为验证输出节点确实是“创建”的,AiiDA引擎会检查这个节点是没有被储存过的。因此, 不要手动储存你所创建的节点 是很重要的,否则引擎会产生一个如下异常 :

# -*- coding: utf-8 -*-
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))

因为返回的节点已经储存,AiiDA引擎会产生一个如下异常 :

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.

该严格限制的原因是,节点需要在被创建后才会被储存,这有别与在函数体中或函数的返回中载入一个已经储存的节点,如 :

# -*- coding: utf-8 -*-
from aiida.engine import calcfunction
from aiida.orm import Int

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

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

载入的节点会从算例函数中产生一个 create 连接,即便该节点不是由该函数创建的。正是为了避免这种模糊性,要求算例函数返回的所有输出节点都是 未储存 的。

注意到工作流函数有完全相反的要求,即所有函数返回的输出必须 已经被储存 ,因为如一个“工作流”型的例程,它是 不能 创建新的数据的。详情请参考 工作流函数小节

算例任务(Calculation jobs)

为描述如何实现算例任务,我们继续章节 算例任务的概念 中展示的例子。例子中我们描述了对两个整数求和并通过简单的bash脚本实现的代码,以及 CalcJob 类如何被用于在AiiDA中运行这段代码。因为该类是 Process 类的子类,则拥有它的全部性质。因此在继续之前阅读章节 泛型例程 也是很有用的。因为那里所描述的所有特性在此处算例任务中同样适用。

定义

要实现一个算例任务,使用者只需要继承 CalcJob 例程类并实现 define() 方法。你可以使用认可python许可的类名。CalcJob 中最重要的方法是 define 类方法。在这个方法中,你定义了输入和算例运行产生的输出。

# -*- coding: utf-8 -*-
from aiida.engine import CalcJob

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=orm.Int, help='The left operand.')
        spec.input('y', valid_type=orm.Int, help='The right operand.')

如上面代码片段所展示的,该类方法接受两个参数 :

  • cls 是指向该类自身的可以访问类的所有类方法的变量

  • spec 是 “计算规格参数和详情” (specification)

警告

请不要忘记在 define 方法的首行加入 super().define(spec) ,这里你需要用你自己的类名。这会调用 define 方法的父类,使得算例任务可以正确工作。

正如其名所示, spec 可以用于指定算例任务的性质。比如,它可以用于定义算例任务所需的输入。在我们的例子中,我们需要接受两个整数作为输入,所以我们通过调用 spec.input() 定义这些 “规格参数”。 第一个参数是输入的名称。该名称之后会被用于在启动算例任务时输入的指定,还会被作为在可验证性图中连接算例节点的数据节点的标签。另外,正如我们在这里所用到的,你还可以指定特定输入允许的参数类型。因为我们在此处希望得到整形的变量,我们指定允许的类型是数据库可储存的 Int 类。

注解

因为我们继承了 CalcJob 类并调用了它的 define 方法,这会同时继承其声明的端口(port)。 如果你查看类的实现,你会发现在基类 CalcJob 中已经定义了输入 code 以接受一个 Code 示例。这会指向用户在运行 CalcJob 时所要使用的计算代码。因此, 请不要 重复声明该输入。

之后我们需要定义我们期望得到的算例输出 :

# -*- coding: utf-8 -*-
from aiida.engine import CalcJob

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=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.')

如同输入,用户需要指定每个输出因有的节点类型。默认的,一个定义了的输出将会是 ‘必给的’ (required),意味着如果算例任务终止但没有得到输出,则例程(process)会被标记为失败。要指明一个输出不是必须的而是可选的,用户可以在 spec.output 调用中使用 required=False 。可以注意到,例程的规格详情以及其 input()output() 方法提供了许多功能。详情可以参考章节 例程规格

准备

通过例程的规格定义,我们能够知晓算例任务所期望的输入和将会产生的输出。剩下的任务就是告知引擎算例任务事实上需要如何运行。为了理解AiiDA引擎为了完成算例任务将会执行的步骤,我们来看一下当用户手动通过任务调度工具执行一个计算任务时会采取的具体步骤 :

  • 在计算资源的可用空间上,准备一个任务运行的工作文件夹

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

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

所以我们要做的就是直到AiiDA引擎对指定算例任务完成这些步骤。由于这些步骤对每个计算是不同的,我们需要通过 prepare_for_submission() 方法来实现这些步骤。在样例中展示的 ArithmeticAddCalculation 实现如下 :

# -*- coding: utf-8 -*-
from aiida.common.datastructures import CalcInfo, CodeInfo
from aiida.engine import CalcJob

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=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.')

    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

在我们逐行研究代码之前,让我们先描述一下这里发生的事情的全貌。该类方法的目的是帮助AiiDA引擎完成如上所述的准备和提交计算任务所需的三个步骤。所需的原始输入文件可以写入沙箱文件夹,该文件夹之后会以 folder 参数传入。

注解

folder 参数指向本地文件系统上的一个临时沙箱文件夹,可用于将输入文件写入。在 prepare_for_submission 方法返回后,引擎将获取这些文件并将它们复制到运行计算的工作目录中。在此基础上,这些文件还将被写入表示计算节点的文件存储库,用作保存详细的额外的可验证性。即使写在那里的信息应该是作为输入节点传递的节点内容的派生,我们仍然显式地存储它。有时,这并非预期的行为,例如出于效率或数据隐私的原因。因此可以使用各种列表来控制它,例如 local_copy_listprovenance_exclude_list

所需的其他所有信息,如拷贝文件的指令和指令参数均通过 CalcInfo 数据结构定义,这个值应该作为方法返回的唯一值。理论上,这是用户在方法 prepare_for_submission应该做的 :

  • 编写运行计算所需的原始输入文件到 folder 沙箱文件夹。

  • 使用 CalcInfo 告诉引擎将被复制到工作目录的文件

  • 使用 CalcInfo 来决定应运行的计算代码,以及使用哪些命令行参数,例如标准输入和输出重定向。

注解

函数 prepare_for_submission 中不需要自己编写任务提交脚本。引擎知道如何编写任务脚本,因为使用的计算代码在配置时已经关联到特定计算资源上,因此其上的任务调度工具也就被记录在计算代码中。这也就告知了AiiDA引擎有关如何编写任务提交脚本比如任务调度指令等的全部信息。

现在我们知道 prepare_for_submission 的预期功能,让我们逐行看看 ArithmeticAddCalculation 的实现。此示例所需的输入文件包含作为输入传递的两个整数。 self.inputs 属性根据 define 方法中定义的过程规范返回解析和验证好的输入的属性字典。这意味着用户不必自己验证输入。也就是说,如果输入被标记为必须的,以及某种指定类型,那么当我们运行 prepare_for_submission 时,self.inputs 返回的字典将确保包含正确类型的输入。

计算作业启动,两个输入 xy 会被传入,我们应确保输入文件只是包含一行两个数组的文本文件,这两个数字在一行上,被空间隔开。我们通过在沙箱文件夹中打开到输入文件的文件句柄并将两个 Int 节点的值写入文件来实现此目的。

注解

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

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

  • codes_info: 一个由数据结构 CodeInfo 构成的列表,用以告知任务运行期间要顺序执行的计算代码

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

  • remote_copy_list: 一个元组列表,它指示要将哪些文件从运行任务的机器复制到工作目录

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

在本例中,我们只需要运行一个计算代码,因此 codes_info 列表有一个 CodeInfo 数据结构。这个数据结构需要定义它需要运行哪些代码,这是传递给 CalcJob 的输入之一,并通过它的UUID来实现。通过 stdout_name 属性,我们告诉引擎可执行文件的输出应该重定向到哪里。在本例中,它被设置为 output_filename 选项的值。计算作业中有哪些选项可用,它们做什么以及如何设置这些选项,将在 选项章节 中进行说明。最后,cmdline_params 属性接受一个带有命令行参数的列表,这些命令行参数将放在启动脚本中可执行文件的 后面 。在这里,我们使用它显式地指示可执行文件从存储在选项 input_filename 中的文件名中读取其输入。

注解

因为我们指示可执行文件应该从 self.options.input_filename 读取输入,这也是我们在数据临时储存文件夹中编写输入文件时使用的文件名。

最后,我们必须定义各种“文件列表”,这些“文件列表”告诉我们要将哪些文件从何处复制到何处,以及要检索哪些文件。在这里,我们将简要描述他们的预期目标。实现细节将在 文件列表章节 中详细描述。

本地复制列表用于指示引擎复制可能已经存储在数据库中的文件,例如 SinglefileData 实例节点。您可以定义并传递为 CalcJob 的输入。你当然可以将它们的内容复制到沙箱文件夹中,这也会导致它们被写入工作目录。然而,这种方法的缺点是,写入到沙箱文件夹的所有内容也将被存储在 CalcJobNode 的存储库中,它将代表可验证性图中的 CalcJob 的执行节点。这将导致这些数据节点中包含的数据重复。通过不显式地将它们写入沙箱文件夹,您可以避免这种重复,而不会丢失可验证性,因为数据节点本身当然会被记录在可验证性图中。

远程复制列表有助于避免在运行引擎的计算机与执行计算作业的位置之间进行不必要的文件传输。例如,假设您已经在远程集群上完成了计算作业,现在想要启动第二个计算作业,这需要第一次运行的某些输出文件作为其输入。远程复制列表允许您准确指定要复制到远程工作目录的输出文件,而不必将它们检索到引擎的机器之间。

最后,检索列表允许您指示引擎在作业终止后应从工作目录中检索哪些文件。这些文件将被下载到本地机器,存储在 FolderData`数据节点中,并作为输出附加到``CalcJobNode`retrieved 链接标签 。

注解

我们没有明确地将 retrieve 文件夹数据节点定义为上面显示的示例 ArithmeticAddCalculation 实现中的输出。这是因为它已经由 CalcJob 基类定义。正如 code 输入一样, retrieve 输出对于所有计算作业实现都是通用的。

文件列表

本地复制列表

本地副本列表采用长度为3的元组,每个元组表示要复制的文件,通过以下项定义:

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

  • source relative path: 节点存储库中文件的相对路径

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

作为一个例子,考虑一个 CalcJob 实现,它接收一个名为 pseudopotentialSinglefileData 节点作为输入,以复制其可以指定的内容:

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

根据定义, SinglefileData 节点只包含一个文件,其相对路径由 filename 属性返回。相反,如果需要从 FolderData 传输特定文件,则可以指定文件的显式键,如下所示:

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

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

有人可能会想这个列表的目的是什么,因为可以很容易地使用普通的API把文件写到 folder 沙箱文件夹。的确,通过这种方式文件将被复制到工作目录中,但是,它 将复制到计算节点的存储库中。由于在本例中,它仅仅是一个已经是一个输入节点的一部分的文件的直接一对一的副本(以未更改的形式),因此这种复制是不必要的,并给文件存储库增加了无用的权重。使用 local_copy_list 可以防止这种不必要的文件内容重复。如果特定输入节点的内容是隐私敏感的,并且不能在存储库中复制,也可以使用它。

可验证性排除列表

local_copy_list 允许指示引擎将文件从输入文件写入工作目录,而不 同时 复制到计算节点的文件存储库。正如在相应的部分中所讨论的,为了避免重复,或者当节点的数据是专有的或隐私敏感的,且不能在文件存储库中的任意位置重复时,这是有用的。然而, local_copy_list 的限制是,它只能针对单个文件的整体,不能用于写入 folder 沙箱文件夹的任意文件。为了完全控制 folder 中的文件永久存储在计算节点文件存储库中,我们引入了 provenance_exclude_list。这个 CalcInfo 属性是一个文件路径列表,相对于 folder 沙箱文件夹的基本路径,它 不存储 在文件存储库中。

下面是 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']

有了这个规范,计算节点存储库的最终内容将包括:

├─ 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')]

请注意,源路径可以指向目录,在这种情况下,其内容将以整体递归方式复制。

检索列表

检索列表是一个指令列表,指示一旦计算作业终止,引擎应该检索哪些文件和文件夹。每个指令应该为以下两种格式之一:

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

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

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

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

  • 目标相对路径 :检索到的文件夹中的目录的相对路径,源路径文件的内容将被复制到其中。字符串 '.' 表示检索到的文件夹的顶层。

  • 深度 :复制时要在源路径中的嵌套层次数,从最深的文件开始。

为了说明各种可能情况,考虑以下远端工作目录中的文件层次结构示例:

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

下面,您将看到要检索的文件和文件夹的各种用例的示例。每个示例都以 retrieve_list 的格式开始,然后是在检索到的文件夹中创建的最终文件层次结构的示意图。

显式文件或文件夹

检索单个顶层文件或文件夹(及其所有内容),且其中最终的文件夹结构并不重要。

retrieve_list = ['file_a.txt']

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

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

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

retrieve_list = ['path/file_b.txt']

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

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

以下示例展示了如何控制检索到的文件的文件层次结构。通过改变元组的 depth 参数,可以控制远程文件夹层次结构的哪一部分被保留。在这个例子中,远程文件夹层次结构的最大深度是 3。下面的例子表明,通过指定 3,可以保持准确的文件夹结构:

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

└─ path
    └─ sub
       └─ file_c.txt

对于 depth=2,只保留了两层嵌套(包括文件本身),因此 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=0 文件复制时将不保留 path/sub 子目录。

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

└─ file_c.txt

要保持子目录结构,可以像前面的示例一样设置depth参数。

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

临时文件检索列表

回想一下,如 准备部分 中所述,引擎在’检索列表’之后检索的所有文件都存储在 retrieve 文件夹数据节点中。这意味着您为完成的计算作业检索的任何文件都将存储在您的存储库中。如果要检索大文件,这可能会导致存储库显着增长。但是,通常,您可能只需要这些检索到的文件中包含的部分信息。为了解决这个常见问题,有“检索临时列表”的概念。检索临时列表的规范与普通列表的规范相同 检索文件列表 。唯一的区别是,与检索列表的文件不同,它将永久存储在检索到的 FolderData 节点中,检索临时列表的文件将存储在临时沙箱文件夹中。然后将此文件夹传递给 解析器 ,如果为计算作业指定了一个。然后,解析器实现可以解析这些文件并将相关信息存储为输出节点。解析器终止后,引擎将注意使用临时检索的文件自动清理沙箱文件夹。 “检索临时列表”的合同本质上是文件在解析期间可用,并且将在之后立即销毁。

把文件藏在远端

1.6.0 新版功能.

stash 选项允许用户指定由计算作业创建并存储在远程某处的某些文件。如果这些文件需要存储的时间比作业运行时通常定时清理的暂存(scratch)空间长,但是文件较大需要保存在远程机器上,而不需要检索,那么这就很有用。例如,有些文件是重新开始计算所必需的,但是由于太大而无法在本地文件存储库中检索和永久存储。

要存储的文件是通过工作目录中的相对文件路径的 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,
            }
        }
    }
}

注解

将来,可能会实现其他的存储方法,比如将所有文件放在(压缩的)tarball中,甚至将文件存储在磁带上。

重要

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

存储的文件由一个输出节点表示,该节点通过标签 remote_stash 作为 RemoteStashFolderData 节点附加到计算节点。就像 remote_folder 节点一样,它表示远程机器上的位置或文件,因此相当于一个 “符号链接”。

重要

AiiDA实际上并不拥有远程存储中的文件,因此内容可能会在某个时候消失。

选项

除了所有进程都有的常见的元数据输入,例如 labeldescriptionCalcJob 有一个额外的 options 输入。这些选项允许微妙地更改计算作业的行为,例如完成计算作业后应该使用哪个解析器,以及特殊的调度器指令。可用选项的完整列表在下面记录为 CalcJob 接口的一部分:

calcjobaiida.engine.processes.calcjobs.CalcJob

Implementation of the CalcJob process.

Inputs:

  • code, Code, required – The Code to use for this job.
  • metadata, Namespace
    Namespace Ports
    • call_link_label, str, optional, non_db – The label to use for the CALL link if the process is called by another process.
    • computer, Computer, optional, non_db – When using a “local” code, set the computer on which the calculation should be run.
    • description, str, optional, non_db – Description to set on the process node.
    • dry_run, bool, optional, non_db – When set to True will prepare the calculation job for submission but not actually launch it.
    • label, str, optional, non_db – Label to set on the process node.
    • options, Namespace
      Namespace Ports
      • account, str, optional, non_db – Set the account to use in for the queue on the remote computer
      • additional_retrieve_list, (list, tuple), optional, non_db – List of relative file paths that should be retrieved in addition to what the plugin specifies.
      • append_text, str, optional, non_db – 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, non_db – 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, non_db – Set a dictionary of custom environment variables for this calculation
      • import_sys_environment, bool, optional, non_db – If set to true, the submission script will load the system environment variables
      • input_filename, str, optional, non_db – Filename to which the input for the code that is to be run is written.
      • max_memory_kb, int, optional, non_db – Set the maximum memory (in KiloBytes) to be asked to the scheduler
      • max_wallclock_seconds, int, optional, non_db – Set the wallclock in seconds asked to the scheduler
      • mpirun_extra_params, (list, tuple), optional, non_db – 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, optional, non_db – Filename to which the content of stdout of the code that is to be run is written.
      • parser_name, str, optional, non_db – Set a string for the output parser. Can be None if no output plugin is available or needed
      • prepend_text, str, optional, non_db – Set the calculation-specific prepend text, which is going to be prepended in the scheduler-job script, just before the code execution
      • priority, str, optional, non_db – Set the priority of the job to be queued
      • qos, str, optional, non_db – Set the quality of service to use in for the queue on the remote computer
      • queue_name, str, optional, non_db – Set the name of the queue on the remote computer
      • resources, dict, required, non_db – 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, non_db – Filename to which the content of stderr of the scheduler is written.
      • scheduler_stdout, str, optional, non_db – 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.
        Namespace Ports
        • source_list, (tuple, list), optional, non_db – Sequence of relative filepaths representing files in the remote directory that should be stashed.
        • stash_mode, str, optional, non_db – Mode with which to perform the stashing, should be value of `aiida.common.datastructures.StashMode.
        • target_base, str, optional, non_db – 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, non_db – Filename to which the job submission script is written.
      • withmpi, bool, optional, non_db – Set the calculation to use mpi
    • store_provenance, bool, optional, non_db – If set to False provenance will not be stored in the database.

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.

启动

启动一个算例任务和启动任何其他列程类没有什么不同,所以请参考 启动列程 一节。我们唯一应该注意的是,算例任务通常相当耗时。我们上面使用的简单示例当然会运行得很快,但是提交给调度器的典型的算例很可能需要花费更长的时间。因此,建议 提交 算例,而不是运行它们。通过将它们提交给守护进程,你就可以直接释放python解释器的占用,整个进程将在必须执行的各种 传输任务 之间进行检查。当然,当您希望本地运行算例以进行测试或演示时除外。

裸运行

当涉及到启动所有其他进程时,算例任务有一个额外的可选特性。未正确配置的算例任务可能会浪费计算资源,所以在实际提交作业之前,可能需要检查插件将写入的输入文件。通过简单地在输入的元数据中指定所谓的 “裸运行” 是可能的。如果你正在使用过程构建器,只需简单使用:

builder.metadata.dry_run = True

当您现在启动列程构建器时,引擎将执行正常算例运行的整个过程,只是它不会实际地将作业上传并提交到远程计算机。但是, prepare_for_submission 方法将被调用。它写入输入文件夹的输入将存储在临时文件夹名为 submit_test ,该文件夹将在当前工作目录中创建。每次执行裸运行时,将在 submit_test 文件夹中创建一个新的子文件夹,您可以在不覆盖以前结果的情况下执行多次裸运行。

此外,下列规则也适用:

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

  • 如果你调用 run_get_node() ,你会得到一个未存储的 CalcJobNode 作为一个节点。在这种情况下,未存储的 CalcJobNode (让我们称它为 node )将有一个额外的属性 node.dry_run_info 。这是一个字典,它包含关于裸运行输出的附加信息。特别地,它将有以下键:

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

    • script_filename: AiiDA在文件夹中生成的提交脚本的文件名,例如: `` _aiidassubmit .sh``

  • 如果使用 submit() 来提交裸运行,将只是转发运行,您将获得未存储的节点(与上面相同的属性)。

警告

默认情况下,可验证性的存储是开启的,这也适用于裸运行。如果您不希望在裸运行期间创建任何节点,只需将元数据输入 store_provenance 设置为 False

结果解析

前面的部分详细解释了外部可执行文件的执行是如何通过 CalcJob 类包装的,从而使其可由AiiDA的引擎运行。从在远程机器上准备输入文件的第一步,到检索相关文件并将它们存储在 FolderData 节点,它被附加为 retrieved 输出。这是 CalcJob 终止的 必需的 步骤,但通常我们 希望 解析原始输出并将它们作为可查询的输出节点附加到计算作业节点。要在检索到计算作业的输出后自动触发对它的解析,需要指定 解析器名称选项 。如果引擎发现指定了这个选项,它将加载相应的解析器类,该解析器类应是 Parser 的子类。并重载了它的 parse() 方法。

为了解释 Parser 类和 parse 方法的接口,让我们使用 ArithmeticAddParser 作为例子。这个解析器被设计来解析简单的bash脚本产生的输出,该脚本由前面讨论的 ArithmeticAddCalculation 包装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# -*- coding: utf-8 -*-
from aiida.orm import Int
from aiida.parsers.parser import Parser


class ArithmeticAddParser(Parser):

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

        try:
            with output_folder.open(self.node.get_option('output_filename'), 'r') as handle:
                result = self.parse_stdout(handle)
        except (OSError, IOError):
            return self.exit_codes.ERROR_READING_OUTPUT_FILE

        if result is None:
            return self.exit_codes.ERROR_INVALID_OUTPUT

        self.out('sum', Int(result))

    @staticmethod
    def parse_stdout(filelike):
        """Parse the sum from the output of the ArithmeticAddcalculation written to standard out

        :param filelike: filelike object containing the output
        :returns: the sum
        """
        try:
            result = int(filelike.read())
        except ValueError:
            result = None

        return result

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

  • 打开并加载由算例任务生成并已被引擎检索的输出文件的内容

  • 从作为输出节点附加在原始创建的数据节点上

  • 在输出警告情况下,记录可读的警告消息

  • 可选地返回 :ref:`退出状态码 <topics:processes:concepts:exit_codes>`来表示计算结果不成功

以不同形式添加原始输出数据作为输出节点的好处是,在这种形式下,内容变成可查询的。这允许查询产生具有特定值的特定输出的计算,这成为对大型数据库进行后处理和分析的一种非常强大的方法。

解析器的 retrieved 属性将返回 FolderData 节点,该节点应该由包含所有检索文件的引擎附加,如在 算例任务的准备步骤 中使用 检索列表 指定的那样。此检索到的文件夹可用于打开和读取其中包含的文件的内容。在本例中,应该有一个单独的输出文件,它是通过对两个整数求和结果标准输出重定向的bash脚本的编写的。解析器打开这个文件,读取它的内容,并试图解析其中的和:

12
13
14
15
16
        try:
            with output_folder.open(self.node.get_option('output_filename'), 'r') as handle:
                result = self.parse_stdout(handle)
        except (OSError, IOError):
            return self.exit_codes.ERROR_READING_OUTPUT_FILE

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

parse_stdout 方法只是一个辅助函数,用于将数据的实际解析与主解析器代码分离开来。在这种情况下,解析变得简单,我们可以将其保存在主解析方法中,但这只是为了说明,以清晰起见,您可以完全自由地在 parse 方法中组织代码。如果设法解析计算产生的和,则将其封装在相应的 Int 数据节点类中,并通过 out 方法将其注册为输出:

21
        self.out('sum', Int(result))

注意,如果我们没有遇到任何问题,我们不需要返回任何东西。引擎会将此解释为计算成功完成。现在您可能会提出这样的问题:“应该解析原始数据的哪一部分,应该将其存储在哪种类型的数据节点中?” 一般来说,这不是一个容易回答的问题,因为它在很大程度上取决于计算产生的原始输出的类型,以及您希望可查询的部分。但是,我们可以给你一些方针:

  • 将您可能想要查询的数据存储在轻量级数据节点中,例如 DictListStructureData 。这些节点的内容作为属性存储在数据库中,这确保可以查询它们。

  • 更大的数据集,比如大型(多维)数组,最好存储在 ArrayData 或它的一个子类。如果将所有这些数据存储在数据库中,它将变得不必要地臃肿,因为您不太可能查询这些数据。相反,这些数组类型的数据节点将其大部分内容存储在存储库中。通过这种方式,您仍然可以保留数据和计算的可验证性,同时保持您的数据库精简和快速!

任务调度程序错误

除了输出解析器,调度器插件还可以通过实现 parse_output() 方法来提供对作业调度器生成的输出的解析。如果调度器插件实现了此方法,则解析调度器生成的输出(写入到stdout和stderr文件描述符中)以及详细作业信息命令的输出。如果解析器检测到一个已知的问题,例如内存不足(OOM)错误,则将在算例节点上设置相应的退出码。输出解析器(如果在输入中定义)可以检查节点上的退出状态,并决定保留它或使用另一种可能更有用的退出代码覆盖它。

class SomeParser(Parser):

    def parse(self, **kwargs):
        """Parse the contents of the output files retrieved in the `FolderData`."""
        if self.node.exit_status is not None:
            # If an exit status is already set on the node, that means the
            # scheduler plugin detected a problem.
            return

注意,在上面给出的示例中,如果检测到调度器检测到问题,解析器立即返回。由于它返回 None ,调度程序的退出码将被保留,并将是算例的最终退出码。但是,解析器不需要立即返回。它仍然可以尝试解析一些检索到的输出(如果有的话)。如果它发现了比通用调度程序错误更具体的问题,它总是可以返回自身的退出代码来覆盖它。解析器甚至可以返回 ``ExitCode(0)` 来将计算标记为成功完成,尽管已知调度器存在问题。下表总结了调度器解析器和输出解析器返回退出代码的可能场景,以及在节点上设置的最终结果退出代码是什么:

场景

调度程序结果

结果检索结果

最终结果

所有解析器均没有找到问题

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)