Code-03 蒙特卡洛编程指南
简介
虽然 ALPS 应用程序中已经提供了若干蒙特卡洛(MC)程序,但 ALPS 的主要目的是帮助开发者以尽可能简单、快捷的方式编写自己的 MC 程序。所有 MC 算法都要执行一些共同的任务,而这些任务无需每次都重新编写。本文的目的是让你相信 ALPS 库为此提供了一个简单的框架,并通过示例演示如何使用它们。这既包括经典蒙特卡洛算法,也包括量子蒙特卡洛算法。
ALPS 源代码中的 example/scheduler 目录包含一个针对伊辛模型的示例模拟代码,你可以根据自己的需要对其进行修改。下面我们将讨论这些示例。
在开发新的 MC 应用程序时,ALPS 库相比自己编写的工具有哪些优势?
- 自动计算误差棒和自相关时间
- 自动并行化
- 自动输出结果(XML 格式),并附带相应的提取工具
- 自动处理符号(用于存在符号问题的量子蒙特卡洛模拟)
- 便捷的检查点保存与模拟重启
- 便捷的输入参数管理
- 便捷地添加新的测量
- 便捷的随机数管理
- (可选)便捷的晶格管理
- (可选)便捷的量子哈密顿量管理(用于量子蒙特卡洛)
- …
所有这些只需要少量的编程/修改即可获得,下面我们就来介绍。
开始编写你自己的 ALPS MC 程序
正在编写中……本教程既未完成也未经校对……欢迎就本页提出任何意见/建议
首先,使用 ALPS 库意味着你要使用 C++ 作为编程语言。从现在起,我们假设你想编写一个新的 MC 应用程序,并且已经知道自己将使用什么样的内部数据结构,以及单个蒙特卡洛步所采用的算法。
要使用 ALPS 库,你需要创建自己的 C++ 类,它派生自 ALPS 的一个内部类。我们将这个类称为 MyMonteCarlo,并创建一个头文件 MyMonteCarlo.hpp。
#ifndef MYMC_HPP
#define MYMC_HPP
#include <alps/scheduler.h>
#include <alps/alea.h>
#include <alps/scheduler/montecarlo.h>
#include <alps/osiris.h>
#include <alps/osiris/dump.h>
#include <alps/expression.h>
using namespace std;
class MyMonteCarlo : public alps::scheduler::MCRun{
public :
MyMonteCarlo(const alps::ProcessList &,const alps::Parameters &,int);
static void print_copyright(std::ostream &);
void save(alps::ODump &) const;
void load(alps::IDump &);
void dostep();
bool is_thermalized() const;
double work_done() const;
private :
// your own internal data here ...
};
#endif
下面我们来看这些代码行的含义。
首先,你需要包含几个 ALPS 头文件(如需使用 ALPS 的其他功能,还会在这里加入更多头文件)。
然后,我们通过从一个 ALPS 类派生来定义
MyMonteCarlo类。上面的头文件意味着你将使用MCRun这个 ALPS 类,它是最简单的一个。如果你想使用 ALPS 的晶格功能,请将类定义行替换为:class MyMonteCarlo : public alps::scheduler::LatticeMCRun<>{
并在头文件中加入
#include <alps/lattice.h>
如果你还想使用模型库(对晶格量子模型很有用),则改用:
class MyMonteCarlo : public alps::scheduler::LatticeModelMCRun<>{
- 在 public 部分中,
MyMonteCarlo(const alps::ProcessList&,const alps::Parameters&,int)是你的类的构造函数,用于初始化所有参数。 print_copyright(std::ostream&)是一个简单的函数,用于在每次模拟开始时输出你希望输出的任何有用信息。- save 和 load 函数非常有用,你将在其中描述在每个检查点需要保存到磁盘(从磁盘加载)哪些内容,以便重启模拟
- dostep 函数是你的 MC 主函数:每个 MC 步都会执行这个函数。
is_thermalized函数会告诉 ALPS 库模拟的热化部分何时结束(即测量序列何时可以开始)work_done函数会告诉 ALPS 库模拟已经完成的百分比。
剩下的工作就是将这些函数与你的程序正确对接。这将在一个名为(比如)MyMonteCarlo.cpp 的文件中完成
构建你自己的 ALPS MC 程序
版权函数
我们从文件 MyMonteCarlo.cpp 中最简单的函数 print_copyright() 开始
#include "MyMonteCarlo.hpp";
/************************************ ALPS functions **********************************************/
// Copyright statement
void MyMonteCarlo::print_copyright(std::ostream & out)
{
out << "My own ALPS Monte Carlo program v. 0.1\n"
<< " copyright (c) 2006 by Myself,\n"
<< " available from the author on request\n\n";
}
蒙特卡洛步的管理
现在我们集中讨论 MC 步的管理。一种典型的情形是:先执行固定步数用于热化,再执行固定步数用于测量部分。很多时候,人们还希望在每两次测量之间执行一定数量的蒙特卡洛步。因此,在内部数据结构中(在 MyMonteCarlo.hpp 中)定义以下变量可能会很有用:
private :
// your own internal data here ...
int Nb_Steps;
int Nb_Thermalisation_Steps;
int Each_Measurement;
int Steps_Done_Total;
int Measurements_Done;
void do_update();
void do_measurements();
// the rest of your own internal data here ...
};
这里 Nb_Thermalisation_Steps 是你要求的热化步数,Nb_Steps 是你要求的热化之后的步数。Each_Measurement 是每两次测量之间的步数。这三个数将在稍后的构造函数中初始化。Steps_Done_Total 存储当前已完成的 MC 步数(包括热化步)。最后,Measurements_Done 存储两次测量之间已完成的中间步数。do_update() 和 do_measurements() 函数(需要你自己定义)分别执行单个 MC 步和一组测量。
根据所有这些定义,可以在 MyMonteCarlo.cpp 中简单地定义类的成员函数如下
bool MyMonteCarlo::is_thermalized() const
{ return (Steps_Done_Total >= Nb_Thermalisation_Steps); }
如果当前 MC 步数大于所要求的热化总步数,该函数返回 1,否则返回 0。第二个函数
double MyMonteCarlo::work_done() const
{ return (is_thermalized() ? (Steps_Done_Total-Nb_Thermalisation_Steps)/double(Nb_Steps) :0.); }
在模拟尚未热化时返回 0,否则返回一个介于 0 和 1 之间的数,对应于所要求的测量步中已完成的百分比。最后,dostep() 函数如下所示
void MyMonteCarlo::dostep()
{ do_update(); // you'll have to define what this function does later
++Steps_Done_Total; // increment the number of steps done
if (is_thermalized()) // do measurements only if simulation thermalized
{ if (++Measurements_Done==Each_Measurement) // do a measurement every Each_Measurement
{ Measurements_Done=0;
do_measurements(); // you'll have to define what this function does later
}
}
}
当然,在某个时候你必须定义程序在一个 MC 步中真正要做的事情,这将在 do_update() 函数中完成。这一点我无法帮你!在我们的示例中,测量被归入 do_measurements() 函数,下面将对其进行介绍。
save 和 load 函数
ALPS 使得将内部数据以检查点形式保存到磁盘变得十分容易。假设在你的内部数据结构中,需要为几个整数和一个 double 数组设置检查点,以便能够重启模拟
private :
// the rest of your own internal data here ...
int Number_of_Spins;
int MyOwnVariable;
std::vector<double> SpinArray;
...
};
这可以非常简单地通过如下代码实现
void MyMonteCarlo::save(alps::ODump& dump) const
{ dump << Number_of_Spins << MyOwnVariable << SpinArray;}
而 load 函数不难猜到是
void MyMonteCarlo::load(alps::IDump& dump) const
{ dump >> Number_of_Spins >> MyOwnVariable >> SpinArray;}
注意,大多数常用类型(int、double、bool 等)都可以用这种方式转储,大多数标准容器(例如 vector 或 set)也同样适用。 现在假设你定义了自己的一个小型内部结构,
struct Vertex
{ int vertex_type;
std::vector<double> coordinates;
int SomeOtherVariable; }
并且你想在蒙特卡洛类中保存它的一个实例。
private :
// the rest of your own internal data here ...
Vertex MyVertex;
...
};
那么,你只需告诉 ALPS 在你的结构中需要保存和加载哪些内容
alps::ODump& operator<<(alps::ODump& dump, const Vertex& v)
{ return dump << v.vertex_type << v.coordinates; }
alps::IDump& operator>>(alps::IDump& dump, Vertex& v)
{ return dump >> v.vertex_type >> v.coordinates;}
之后就可以轻松地将 MyVertex 加入 ALPS 主 save 和 load 函数中:
void MyMonteCarlo::save(alps::ODump& dump) const
{ dump << Number_of_Spins << MyOwnVariable << SpinArray << MyVertex;}
如果你使用了上面介绍的蒙特卡洛步管理方式,还需要在 save/load 函数中加入以下内容:
void MyMonteCarlo::save(alps::ODump& dump) const
{ dump << Number_of_Spins << MyOwnVariable << SpinArray << MyVertex;
dump << Nb_Steps << Measurements_Done; }
do_measurements() 函数
在这个函数中,你将执行测量,并将测量结果交给 ALPS 处理。这非常简单。
void MyMonteCarlo::do_measurements()
{ double Energy; std::valarray<double> Correl(L);
// do the measurements in your code (update the Energy and Correl variable) ...
...
// give them to ALPS
measurements["Energy"] << Energy;
measurements["Spin Correlations"] << Correl;
}
就这样?现在你可能会问:ALPS 怎么知道 “Energy” 或 “Spin Correlations” 是什么?它又如何区分标量测量(例如这里的 Energy)和矢量测量(例如 Correl)?这两个问题将在下面类的构造函数部分中解答。
另一个问题可能是:为什么矢量观测量使用的是 std::valarray 而不是 std::vector?这是出于 ALPS 内部的原因,但请记住,对于矢量观测量,ALPS 要求每次测量中的矢量始终具有相同的大小(在本例中,如果 Correl 对象的大小不总是相同——这里为 L——measurements[“Spin Correlations”] 将会失败)。
构造函数
在构造函数中,基本上需要完成三件事:
- 从给定的参数中读取并初始化模拟所需的参数
- 初始化其他内部变量,例如自旋构型
- 定义上面 do_measurements() 函数中用到的观测量
构造函数如下所示
MyMonteCarlo::MyMonteCarlo(const alps::ProcessList& where,const alps::Parameters& params,int node) : alps::scheduler::MCRun(where,params,node),
Nb_Steps(params.value_or_default("SWEEPS",1000)),
Nb_Thermalisation_Steps(static_cast<alps::uint32_t>(params["THERMALIZATION"])),
Number_of_Spins(alps::evaluate<alps::uint32_t>(params["L"],params)),
T(params.defined("T") ? static_cast<double>(params["T"]) : 1./static_cast<double>(params["beta"])),
Steps_Done_Total(0),
SpinArray(Number_of_Spins,0), //Initialize the std::vector with size Number_of_Spins and set all values to 0
//...
{
for (std::vector<int>::iterator iter=SpinArray.begin(); iter!=SpinArray.end(); ++iter)
*iter=random_int(-1,1);
//...
measurements << alps::RealObservable("Energy"); //With binning
measurements << alps::SimpleRealObservable("Magnetization"); //Without binning
alps::RealVectorObservable::label_type correlationlabels;
// set correlationlabels using push_back() ...
measurements << alps::RealVectorObservable("Spin Correlations",correlationlabels);
}
可以看到,从 params 对象中读取参数有多种方式。定义观测量只需提供其类型和标签即可。
注意,包含 alps/scheduler/montecarlo.h 之后,你还可以使用 ALPS 提供的
random_int(int a, int b); //from [a,b]
random_int(int n); //from [0,n)
random_real(double a, double b); //from (a,b)
random_real(); //from (0,1)
随机数函数。
运行你的 ALPS 代码
为了运行你的代码,最好在 MyMonteCarlo.h 中加入以下 typedef:
typedef alps::scheduler::SimpleMCFactory<MyMonteCarlo> MyMonteCarloFactory;
在一个简单的 main.C 中(例如 .../alps/example/scheduler/main.C),现在可以调用
int main(int argc, char** argv)
{
// ...
return alps::scheduler::start(argc,argv,MyMonteCarloFactory());
// ...
}
编译和链接之后,使用 ./MyMonteCarlo parm.in.xml 启动模拟,就这么简单。
使用 ALPS 的晶格功能
如果你不仅希望 ALPS 替你完成调度和测量处理,还希望它负责晶格操作,可以使用 LatticeMCRun 类。按如下方式定义你的蒙特卡洛代码:
typedef alps::scheduler::LatticeMCRun<>::graph_type graph_type;
class MyLatticeMonteCarlo : public alps::scheduler::LatticeMCRun<graph_type> { ... }
在执行模拟时,你需要通过参数文件告诉你的类要使用哪种晶格,例如
LATTICE = "square lattice"
或
LATTICE = "honeycomb lattice"
ALPS 预定义了许多晶格,你可以在 /alps/2.x.x/lib/xml/lattices.xml 中找到它们。如何定义你自己的晶格,请参见此处。
简介
基本上,ALPS 会创建一个表示晶格的 boost::graph 对象。这一强大的数据结构可以方便而高效地在图的顶点和边之间移动。注意,在 ALPS 的术语中,我们称之为格点(site)、键(bond)、近邻(neighbor),而不是 Boost 中的 vertex、edge、adjacent_vertex。要访问某个特定格点,该数据结构使用 site_descriptor 数据类型。除非你另行定义,晶格的 site_descriptor 为 int 类型(更确切地说,是 alps::uint32_t 类型)。这使你可以将 site_descriptor 用作存储系统构型的(私有)数组(或 vector 等)的索引。换言之,晶格中数据的内容(自旋值、占据数等)需要你自己负责!ALPS 的晶格功能负责的是方便地提供某个格点的近邻的 site_descriptor,或其出射键的 bond_descriptor。
此外,它们还允许借助所谓的 site_iterator 遍历格点、键和近邻。site_iterator 只是一个列表元素,它带有一个指向 site_descriptor 的指针和一个指向下一个 site_iterator 的指针。bond_iterator 的工作方式与之相同。因此,所有格点的全体可以表示为一个 std::pair<site_iterator, site_iterator>,由指向所有格点列表中第一个和最后一个元素的指针组成。这正是 sites() 的返回值。
一个综合示例
本节给出一个代码示例,它应当涵盖了大部分重要功能。这是对伊辛自旋系统能量测量的多种实现,分别为
遍历所有键
遍历所有格点,再遍历每个格点的近邻
遍历所有格点,再遍历每个格点的出射边
在构造函数中,自旋矢量的初始化可以写成
std::vector<int> spins(num_sites());
site_iterator s_iter;
for (s_iter = sites().first; s_iter!=sites().second; ++s_iter)
spins[*s_iter]=random_int(0,1);
这里可以看到 LatticeMCRun 类的两个函数:num_sites()(返回格点数)和 sites()(返回由 site_iterator 组成的 std::pair)。我们可以将 iter 所指向的 site_descriptor 用作自旋矢量的索引。
作为 do_step() 函数一部分的能量测量可以写成
double E = 0.0;
int index1, index2;
for (bond_iterator b_iter=bonds().first; b_iter!=bonds().second; ++b_iter) {
index1 = source(*b_iter);
index2 = target(*b_iter);
E += (spins[index1]==spins[index2])? J : -J ; // where J is the coupling constant
}
//...
measurements["Energy"] << E/num_sites();
在这个版本中,我们类似于遍历格点那样遍历所有键,并可以通过 source(bond_descriptor b) 和 target(bond_descriptor b) 访问键两端的两个格点,它们返回一个 site_descriptor。
另一种包含近邻处理的方式是:
double E = 0.0;
for (site_iterator s_iter=sites().first; s_iter!=sites().second; ++s_iter) {
neighbor_iterator n_iter;
for (n_iter=neighbors(*s_iter).first; n_iter!=neighbors(*s_iter).second; ++n_iter) {
E += (spins[*s_iter]==spins[*n_iter])? 0.5*J : -0.5*J ; // where J is the coupling constant, factor 1/2 because of double-counting
}
}
第三种方式遍历格点的出射键:
double E = 0.0;
for (site_iterator s_iter=sites().first; s_iter!=sites().second; ++s_iter) {
neighbor_bond_iterator nb_iter;
for (nb_iter=neighbor_bonds(*s_iter).first; nb_iter!=neighbor_bonds(*s_iter).second; ++nb_iter) {
E += (spins[source(*nb_iter)]==spins[target(*nb_iter)])? 0.5*J : -0.5*J ;
}
}
最后,如果你想访问一个随机格点或随机键,可以使用 site(int site_no) 和 bond(int bond_no):
site_descriptor randomsite = site(random_int(num_sites() ) );
bond_descriptor randombond = bond(random_int(num_bonds() ) );