如何在 ABACUS 中新增一个输入参数(v3.7.0 后)

作者:刘千锐,邮箱:terry_liu@pku.edu.cn

单位:北京大学

最后更新日期:2024/7/10

如果遇到本文档无法解决的问题,欢迎给 ABACUS 提出 Issues,我们将持续更新这个文档

开发者须知

1.1 声明参数并给出初始值

路径 module_parameter/input_parameter.h

首先现在input_parameter.h中添加相关输入参数的定义,如 nelec_delta

bool init_vel = false;          ///< read velocity from STRU or not  liuyu 2021-07-14
double symmetry_prec = 1.0e-6;  ///< LiuXh add 2021-08-12, accuracy for symmetry
bool symmetry_autoclose = true; ///< whether to close symmetry automatically
                                ///< when error occurs in symmetry analysis
double nelec = 0.0;             ///< total number of electrons
double nelec_delta = 0.0;       ///< change in the number of total electrons

在声明的同时给出默认值,如 nelec 默认为 0.0。

注:对于任何类成员变量的 double, int, bool 等一般类型变量,在定义时就建议给一个初始值。如果类成员没有初始值,很可能会有难以察觉的 bug

注:string类型变量的初始值不能为"", 如果想自动设置,可以设置初始值为"auto", "none"等。

1.2 在参数列表中添加参数

路径 module_io/read_input_item_*.cpp (不同的参数分好了类,其对应不同文件,需要加到属于的文件里面),如 nelec 就在read_input_item_general.cpp

添加 nelec 参数的 item

Input_Item item("nelec");

之后依次添加:(*为必填)

注释(annotation*):该注释会被打印在 OUT.*文件夹的 INPUT 中

item.annotation = "input number of electrons";

读入函数(read_value*):如何通过读入的 vector str_value 转化成想要的 parameter 参数,该函数只有在 INPUT 有相应参数的名字时才会执行。

item.read_value = [](const Input_Item& item, Parameter& para) {
            para.input.nelec = std::stoi(item.str_values[0]); 
        };

重新赋值函数(reset_value):如果其他某些参数满足特定条件是,需要对当前参数(nelec)进行修改的函数,该函数一定会执行。书写规范要求:只把修改当前参数的函数部分定义在当前参数的 reset_value,如果要修改其他参数,请定义在其他参数的 reset_value 。

item.reset_value = [](const Input_Item& item, Parameter& para) {
            if(para.input.somecondition)
            {
                para.input.nelec = 0.0;
            }
        };

检验函数(check_value):用于检查目前参数是否合适,如果不合适就报错退出(warning_quit),该函数一定会执行。

item.check_value = [](const Input_Item& item, const Parameter& para) {
            if(para.input.nelec < 0)
            {
                ModuleBase::WARNING_QUIT("ReadInput", "nelec should be no less than 0.0");
            }

获取最终打印值函数(get_final_value*):用于给 stringstream final_value 赋值,最终 final_value.str()会被打印在 OUT.*的 INPUT 文件中,该函数一定会执行。

item.get_final_value = [](Input_Item& item, const Parameter& para) {
            item.final_value << para.input.nelec;
        };

以及如果是并行版,需要添加广播函数(bcastfuncs*):如何调用 Parallel_Common 进行 bcast 的函数,该函数一定会执行。该函数不一定只传输该参数,还可以传输其他引入的非 INPUT 参数,例如 gamma_only_local 这个变量不是 INPUT 参数,但是其由 gamma_only 和 basis_type 共同决定取值,这种额外引入的参数也需要在 gamma_only 或 basis_type 处添加他的 bcast 函数。

#ifdef __MPI
        bcastfuncs.push_back([](Parameter& para) {
            Parallel_Common::bcast_bool(para.input.nelec); 
        });
#endif

最后将 item 添加到参数列表中:

this->add_item(item);

总结:

{
        Input_Item item("nelec");
        item.annotation = "input number of electrons";
        item.read_value = [](const Input_Item& item, Parameter& para) {
            para.input.nelec = std::stoi(item.str_values[0]); 
        };
        item.reset_value = [](const Input_Item& item, Parameter& para) {
            if(para.input.somecondition)
            {
                para.input.nelec = 0.0;
            }
        }; 
        item.check_value = [](const Input_Item& item, const Parameter& para) {
            if(para.input.nelec < 0)
            {
                ModuleBase::WARNING_QUIT("ReadInput", "nelec should be no less than 0.0");
            }
        };
        item.get_final_value = [](Input_Item& item, const Parameter& para) {
            item.final_value << para.input.nelec;
        };  
#ifdef __MPI
        bcastfuncs.push_back([](Parameter& para) {
            Parallel_Common::bcast_bool(para.input.nelec); 
        });
#endif
        this->add_item(item);
    }

以上是完整的参数添加实例,可以根据需要自己修改函数,然而实际上大部分参数的 bcastfuncs、get_final_value、read_value 函数填写是几乎一致的,他们有共同的形式,为了书写方便,可以利用 module_io/read_input_tool.h 定义的宏函数来进行书写的简化:

1. 对于形如

bcastfuncs.push_back([](Parameter& para) {
            Parallel_Common::bcast_bool(para.input.nelec); 
        });

的 bcastfuncs 函数书写可以分别使用 add_double_bcast, add_int_bcast, add_bool_bcast, add_string_bcast, add_doublevec_bcast, add_intvec_bcast, add_stringvec_bcast 函数进行代替

2. 对于形如

item.get_final_value = [](Input_Item& item, const Parameter& para) {
            item.final_value << para.input.nelec;
        };  
        bcastfuncs.push_back([](Parameter& para) {
            Parallel_Common::bcast_bool(para.input.PARAMETER); 
        });

的 get_final_value 与 bcastfuncs 函数的合并书写可以使用 sync_double, sync_int, sync_bool, sync_string, sync_doublevec, sync_intvec, sync_stringvec 函数进行代替

3. 对于形如

item.read_value = [](const Input_Item& item, Parameter& para) {
            para.input.nelec = std::stoi(item.str_values[0]); 
        };
        item.get_final_value = [](Input_Item& item, const Parameter& para) {
            item.final_value << para.input.nelec;
        };
        bcastfuncs.push_back([](Parameter& para) {
            Parallel_Common::bcast_bool(para.input.nelec); 
        });

的 read_value, get_final_value 与 bcastfuncs 函数的合并书写可以使用 read_sync_double, read_sync_string, read_sync_int, read_sync_bool 函数进行代替

因此添加 nelec 的代码可以简化成:

{
        Input_Item item("nelec");
        item.annotation = "input number of electrons";
        read_sync_double(nelec);
        item.reset_value = [](const Input_Item& item, Parameter& para) {
            if(para.input.somecondition)
            {
                para.input.nelec = 0.0;
            }
        }; 
        item.check_value = [](const Input_Item& item, const Parameter& para) {
            if(para.input.nelec < 0)
            {
                ModuleBase::WARNING_QUIT("ReadInput", "nelec should be no less than 0.0");
            }
        };
        this->add_item(item);

1.2 参数的使用

目前已经舍弃使用 INPUT 与 GloablV::,请勿在里面添加新变量!

需要用到参数的地方,请以只读的方式使用

int nbands = PARAM.inp.nbands; //只读的访问元素

而其不能作为修改的对象,这时编译不能通过:

PARAM.inp.nbands = 0//会报错

1.3 添加测试

A. 读入测试

路径:source/module_io/test/read_input_pteset.cpp

       <u>source/module_io/test/support/INPUT</u>

在 INPUT 中添加这一参数:

nelec                  10 #input number of electrons

TEST_F(InputParaTest, ParaRead) 测试这一读入是否正确,例如:

EXPECT_EQ(PARAM.inp.nelec, 10);

B. 特殊场景的 reset_value 和 check_value 测试

路径:source/module_io/test/read_input_item_test.cpp

TEST_F(InputTest, Item_test) 添加特定参数的 reset_value 函数和 check_value 的覆盖性测试。

1.4 添加文档

路径:docs/advanced/input_files/input-main.md

每个新参数的 PR必须包含相应的文档,否则不会被接收。请在 input-main.md 中添加参数描述。

设计文档参考

参考:林霈泽博士提的方案 github.com

2.1 背景

v3.7.0 之前添加参考如何在 ABACUS 中新增一个输入参数(截至 v3.5.3)

2.1.1 原 input.cpp

当添加一个新的 INPUT 参数,我们需在 input 类里做以下事情:

A. 首先在 Default 函数中给初始值;

B. 再在 read 函数中的 if 中加一个判断分支:

if (strcmp("suffix", word) == 0) // out dir
{
     read_value(ifs, suffix);
}

C. 再在 Bcast 函数中添加 bcast

D. (大部分)在 GlobalV 定义一个相同功能的变量,并给个初始值(实际上这个初始值没有任何用,还容易让人误解到底这个和 input.cpp 哪个是初始值)

E. 再在 input_conv.cpp 中转化成 GlobalV 的变量

F. 再在 write_input.cpp 添加一行代码使其可以输出到 OUT.ABACUS 文件夹中 INPUT

痛点:

  1. 每加一个参数,对于 ABACUS 主代码需“翻山越岭”地添加代码,不易管理
  2. input.cpp 的变量和 GlobalV 的变量大部分重复,没有必要,且容易误解
  3. 大量的 if 分支 改进思路:
  4. 将相同的参数代码集中起来
  5. input 类只实现读的功能,不存储参数,参数由另一个类存储

2.1.2 GlobalV

其实代码有全局变量不可怕,可怕的是全局变量会在运行中改变,而部分 GlobalV 就是这样,因此需要限制他人在初始化参数之后改变的行为。

目前很多也使用了 public 的类静态成员变量,这其实和全局变量是一样的。

2.2 重构设计

2.2.1 Parameter 类

该类只存储 ABACUS 运行的参数,将代替 GlobalV 的功能,该类将成员设成私有变量,只给 ReadInput 类修改的权限,其他类只有访值权限,没有修改权限:

class Parameter
{
  public:
    Parameter(){};
    ~Parameter(){};
  public:
    // We can only read the value of input, but cannot modify it.
    const Input_para& inp = input;
    // We can only read the value of mdp, but cannot modify it.
    const MD_para& mdp = input.mdp;
    // We can only read the value of other parameters, but cannot modify it.
    const System_para& globalv = system;

    // Set the rank & nproc
    void set_rank_nproc(const int& myrank, const int& nproc);
    // Set the start time
    void set_start_time(const std::time_t& start_time);

  private:
    // Only ReadInput can modify the value of Parameter.
    friend class ModuleIO::ReadInput;
    // INPUT parameters
    Input_para input;
    // System parameters
    System_para system;
};

2.2 Input_Item 类

用于存储 input 参数的信息,每一个参数用 input_item 的一个对象存储,不同 input_item 用 vector 打包。

class Input_Item
{
  public:
    Input_Item(const std::string& label_in)
    {
        label = label_in;
    }

    std::string label;                   ///< label of the input item
    std::vector<std::string> str_values; ///< string values of the input item
    std::stringstream final_value;       ///< final value for writing to output INPUT file
    std::string annotation; ///< annotation of the input item

    // ====== !!! These functions are complete.        ======
    // ====== !!! Do not add any more functions here.  ======
    /// read value if INPUT file has this item
    std::function<void(const Input_Item&, Parameter&)> read_value = [](const Input_Item& item, Parameter& param) {};
    /// check value 
    std::function<void(const Input_Item&, const Parameter&)> check_value = nullptr;
    /// reset some values
    std::function<void(const Input_Item&, Parameter&)> reset_value = nullptr;
    /// get final_value function for output INPUT file
    std::function<void(Input_Item&, const Parameter&)> get_final_value = nullptr;
    // ====== !!! Do not add any more functions here.  ======
};

四个存储的变量:

label: 参数的名字

annotation: 参数的注释,用于生成 INPUT 的注释

str_values: 读入的 raw 数据,一个参数可以有多个数据,因此用 vector 存储,例如:kspacing 3 3 3

final_value: 最终的取值,用于生成 INPUT

四个 std::function 成员:

read_value(必填): 当从 INPUT 读入参数后,需要进行的赋值操作

reset_value(选填): 根据其读入的值,可能会修改其他参数的值时填入,例如 calculation 默认是 scf,当你读入 calculation 是 nscf 后,则需要将 init_chg 改成 file

check_value(选填):根据其读入的值进行判断参数是否合适,例如:读入 ecut 为-100,则需要 warning_quit()

get_final_value(大部分必填):此函数一定会执行(无论是否从 INPUT 读入这个参数),需要在此函数中给出 final_value 的赋值函数,这样用于打印 INPUT 文件

2.2 ReadInput 类

此类只负责读入 INPUT 文件,并赋值给 Parameter 类的对象,此类不负责存储参数

class ReadInput
{
 public:
     /**
     * @brief read in parameters from input file
     *
     * @param param parameters of ABACUS
     * @param filename_in read INPUT file name
     */
    void read_parameters(Parameter& param, const std::string& filename_in);

    /**
     * @brief write out parameters to output file
     *
     * @param param parameters of ABACUS
     * @param filename_out write output file name
     */
    void write_parameters(const Parameter& param, const std::string& filename_out);
  private:
    std::vector<std::pair<std::string, Input_Item>> input_lists;
    //----These functions are done only when INPUT file has them.------
    // read value if INPUT file has this item
    std::vector<Input_Item*> readvalue_items;

    /// bcast all values function
    /// if no MPI, this function will resize the vector
    std::vector<std::function<void(Parameter&)>> bcastfuncs;

主要两个 public 函数:

read_parameters: 读入 INPUT 文件,赋值给 param

write_parameters: 打印 INPUT 文件

3 个 vector:

input_lists: 用于存储所有的 Input_Item,用于遍历调用 resetvalue, getfinalvalue

readvalue_items: 用于存储读入 INPUT 中相应 Input_Item 指针,并进行读入赋值操作

bcastfuncs: 用于所有参数的 bcast

原则上bcastfuncs也可以用 vector 存储,一方面由于其不依赖 Input_Item,并且可能会 bcast 非 input 列表参数,另一方面但为了访存效率,用 vector存储更好

2.2.1 read_parameters 函数

对于 ReadInput 的 read_parameter,其执行的顺序是:

先确定 INPUT 对应哪些 Input_Item,然后将 INPUT 中读入的信息存入 item 的 str_values 中,之后依次执行

  1. readvalue_items
  2. inputlist->resetvalue
  3. inputlist->checkvalue
  4. bcastfuncs

2.2.2 write_parameters 函数

如果要打印 INPUT,顺序执行 input_lists 的getfinalvalue

然后

for (auto& item: this->input_lists)
{
   Input_Item* p_item = &(item.second);
   if (p_item->getfinalvalue == nullptr)
            continue;
   p_item->getfinalvalue(*p_item, param);
   ModuleBase::GlobalFunc::OUTP(ofs, p_item->label, p_item->final_value.str(), p_item->annotation);
}
Copyright © mcresearch.gitee.io 2023 all right reserved,powered by Gitbook该文章修订时间: 2024-08-11 09:55:38

results matching ""

    No results matching ""