Bose-Hubbard model mean field theory

Bose-Hubbard 模型的平均场解

Bose-Hubbard 模型 {#bose-hubbard-模型}

Bose-Hubbard 模型 是一维的无自旋玻色子格点模型。

哈密顿量为

$$ H = -t \sum\_{\langle i,j \rangle} b^{\dagger}\_{i}b\_{j} + \frac{U}{2} \sum\_{i} n\_{i} (n\_{i} -1) - \mu \sum\_{i} n\_{i} $$

其中 $-t$ 是最近邻跃迁强度, $U$ 是相互作用强度, $\mu$ 是化学势。

平均场哈密顿量

平均场近似为 $$ a^{\dagger}\_{i} a\_{j} = \langle a^{\dagger}\_{i} \rangle a\_{j} + a^{\dagger}\_{i} \langle a\_{j} \rangle - \langle a^{\dagger}\_{i} \rangle \langle a\_{j} \rangle $$

代入就得到单点平均场哈密顿量 $$ H\_{i}^{\mathrm{MF}} = \frac{U}{2} n\_{i} (n\_{i} -1) - \mu n\_{i} - \psi (a\_{i} + a\_{i}^{\dagger}) + |\psi| $$

其中 $\psi = \langle a^{\dagger}\_{i} \rangle = \langle a\_{i} \rangle $ 是超流序参量。

求解

由于是玻色模型,所以需要先确定合适的截断尺寸。步骤就是先计算小的截断尺寸的基态能量,再逐渐加大截断数直到收敛。

确定截断之后就把 $\psi$ 做自变量,寻找能使基态能量最小的 $\psi$ ,迭代自洽收敛的效果很好,直接做优化则很难找到全局最优。

得到不同参数的 $\psi$ 再计算 $\langle n \rangle$ 两个对比,就能看到超流相和 Mott 绝缘体相之间的变化,以及化学势导致的不同 Mott 平台。

参考文献

  1. 首先给出 Bose-Hubbard 模型的平均场解的是 (Sheshadri et al. 1993) . Sheshadri, K., H. R. Krishnamurthy, R. Pandit, and T. V. Ramakrishnan. 1993. “Superfluid and Insulating Phases in an Interacting-Boson Model: Mean-Field Theory and the RPA.” Europhysics Letters (EPL) 22 (4). IOP Publishing:257–63.
In [1]:
import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as sop
import scipy.sparse as sp
import scipy.sparse.linalg as slin
from tqdm.notebook import tqdm
In [2]:
def dag(x):
    return x.T.conj()


def annihilation(M, tp="dense"):
    if tp == "dense":
        a = np.diag(np.sqrt(np.arange(1, M)), k=1)
    elif tp == "sparse":
        a = sp.dia_matrix((np.sqrt(np.arange(1, 10)), 1), shape=(M, M))
    return a
In [3]:
def make_Hi(U, t, mu, psi, M, tp):
    a = annihilation(M, tp)
    ad = dag(a)
    n = ad @ a
    if tp == "dense":
        mI = np.eye(M)
    elif tp == "sparse":
        mI = sp.eye(M)
    Hi = U / 2 * n @ (n - mI) - mu * n + psi * (a + ad) + np.abs(psi) ** 2 * mI
    return Hi


def eig_Hi(Hi, tp):
    if tp == "dense":
        eigvals, eigvecs = np.linalg.eigh(Hi)
        e = eigvals[0]
        v = eigvecs[:, 0].reshape((-1, 1))
    elif tp == "sparse":
        el, v = slin.eigsh(Hi, which="SA", k=1)
        e = el[0]
    return e, v


def calc_psi(v, tp):
    M = v.shape[0]
    a = annihilation(M, tp)
    return (dag(v) @ a @ v)[0, 0]


def find_M(U, mu, t, psi, tp):
    e_old = None
    for M in range(5, 50):
        Hi = make_Hi(U, t, mu, psi, M, tp)
        e, v = eig_Hi(Hi, tp)
        # psi=calc_psi(v,tp)
        # print(M,e)
        if e_old is not None:
            if np.abs(e - e_old) < 1e-6:
                break
        e_old = e
    return M


def find_psi(U, mu, t, M, tp):
    psi = np.random.rand()
    e_old = None
    for i in range(30):

        Hi = make_Hi(U, t, mu, psi, M, tp)
        e, v = eig_Hi(Hi, tp)
        psi = calc_psi(v, tp)
        # print(e,e_old)
        if e_old is None:
            e_old = e
        else:
            if np.abs(e - e_old) < 1e-6:
                break
            else:
                e_old = e

    return e, psi, v


def find_psi_variational(U, mu, t, M, tp):
    def _f(psi, U, t, mu, M, tp):
        Hi = make_Hi(U, t, mu, psi, M, tp)
        e, v = eig_Hi(Hi, tp)
        return e

    sol = sop.minimize(f, np.random.rand(), args=(U, mu, t, M, tp))

    Hi = make_Hi(U, t, mu, sol.x[0], M, tp)
    e, v = eig_Hi(Hi, tp)
    return e, sol.x[0], v


def calc_n(v, tp):
    M = v.shape[0]
    a = annihilation(M, tp)
    ad = dag(a)
    n = ad @ a
    return (dag(v) @ n @ v)[0, 0]
In [4]:
tp = "dense"
In [5]:
t = 1

Ul = np.linspace(0, 20, 40)
mul = np.linspace(0, 40, 150)

UU, mumu = np.meshgrid(Ul, mul, indexing="ij")
ee = np.zeros_like(UU)
psipsi = np.zeros_like(UU)
nn = np.zeros_like(UU)
In [6]:
for i in tqdm(range(len(Ul))):
    for j in range(len(mul)):
        U = UU[i, j]
        mu = mumu[i, j]
        M = find_M(U, mu, t, 0.5, tp)
        e, psi, v = find_psi(U, mu, t, M, tp)
        ee[i, j] = e
        psipsi[i, j] = psi
        nn[i, j] = calc_n(v, tp)
  0%|          | 0/40 [00:00<?, ?it/s]

这就是巨正则系统的相图,右边圈起来的地方就是 Mott 绝缘体相

In [7]:
pp = psipsi.copy()
pp[np.abs(pp) < 1e-2] = 0
pp[np.abs(pp) >= 1e-2] = 1
plt.contour(UU, mumu, np.abs(pp))
Out[7]:
<matplotlib.contour.QuadContourSet at 0x7f6448fae670>
No description has been provided for this image
In [8]:
mul = np.linspace(-4, 40, 150)
el = np.zeros_like(mul)
psil = np.zeros_like(mul)
nl = np.zeros_like(mul)
t = 1
U = 20
for i in range(len(mul)):
    mu = mul[i]
    M = find_M(U, mu, t, 0.5, tp)
    e, psi, v = find_psi(U, mu, t, M, tp)
    # e, psi, v = find_psi_variational(U, mu, t, M, tp)
    el[i] = e
    psil[i] = psi
    nl[i] = calc_n(v, tp)

对某个特定参数,能够看到在 Mott 平台中超流序参量都是 0,在Mott平台之间超流序参量才不是 0.

In [9]:
plt.plot(mul, np.abs(psil))
plt.plot(mul, nl)
Out[9]:
[<matplotlib.lines.Line2D at 0x7f6448c9efd0>]
No description has been provided for this image

Performant Python

Python 性能

计算机结构

简单来说,计算机由计算单元、储存单元和二者之间的通信构成。

计算单元

计算单元一般是 CPU 和 GPU 等。

计算单元重要的两个参数是

  1. 每周期指令 (instructions per cycle, IPC): 每个时钟周期执行的指令数
  2. 时钟速度:每秒的时钟周期

如果提高时钟速度了,当然运行速度会变快。另一种方法是增加每个指令的计算量,通过向量化的方式,可以在一个指令中同时处理多个数据,即 SIMD(single instruction multiple data).

由于物理和技术限制,时钟速度的增加基本停止了,所以增加同一时间内运算量的方法就是增加更多的 CPU, 即并行化。

Amdahl'a law

经验定律:并行程序的加速上限取决于不能并行化的最小单元。

存储单元

存储单元包括:机械硬盘、固态硬盘、RAM、L1/L2 等。主要的区别就是速度。一般来说速度越快容量越小。

存储单元速度通常比计算单元慢,所以主要的优化手段就是异步 I/O 和预先缓存,保证计算设备进行计算的时候数据已经准备好了。

总线

计算单元和存储单元通过总线连接。 总线最重要的参数是总线宽度和总线频率。

从源码安装 python

  • openssl 通过 --with-openssl 指定
  • sqlite 通过修改 setup.py 中的 sqlite_inc_paths
  • libffi 通过 CPPFLAGS, LDFLAGS 指定

    参考编译选项

  ./configure --prefix=$HOME/app/python --enable-optimizations --with-openssl=$HOME/app/openssl --enable-shared PKG_CONFIG_LIBDIR=$PKG_CONFIG_PATH LIBFFI_INCLUDEDIR=/vol7/home/zhongpg/app/libffi/include --with-system-ffi=/vol7/home/zhongpg/app/libffi/lib64 CPPFLAGS="-I /vol7/home/zhongpg/app/libffi/include" LDFLAGS="-L/vol7/home/zhongpg/app/libffi/lib64"

从源码安装 numpy, scipy

  • numpy 依赖
    • cython: pip install cython
  • scipy 依赖
    • pybind11: pip install "pybind11[global]"
    • pythran: pip install pythran
    • 执行 git submodule update --init 来自动下载一些包

设置 site.cfg

执行安装

  python3 setup.py build -j 10 install

PETSc

PETSc

PETSc 是用于开发并行计算程序的基础库,完全建立在 MPI 上。

安装与编译

基本结构

PETSc 模块

  1. index set (IS):向量的指标
  2. vectors (Vec)
  3. matrices (Mat): 稀疏矩阵
  4. Krylov subspace methods (KSP): 线性方程组求解
  5. preconditioners (PC): 预条件器和直接求解器
  6. nonlinear solvers(SNES)
  7. timesteppers for solving time-dependent PDES(TS): 时间演化求解器
  8. managing data structures(DM): 管理数据结构
  9. scalable optimization algorithms(Tao)

环境变量

  1. 所有 PETSc 程序都依赖环境变量 $PETSC_DIR 找到 PETSc 的目录
  2. 还需要环境变量 $PETSC_ARCH 找到安装路径

命令行

PETSc 程序需要用 MPI 执行,通过 mpiexec 或者 PETSc 提供的脚本 $PETSC_DIR/lib/petsc/bin/petscmpiexec

常用的命令行选项

  • -help
  • -version
  • -log_view 显示性能摘要
  • -fp_trap 浮点异常时退出
  • -malloc_dump 内存追踪
  • -malloc_debug 内存debug
  • -start_in_debugger [noxterm, gdb, lldb]
  • -on_error_attach_debugger
  • -info

主函数

  • PETSc 主函数需要调用 PetscInitialize() 作为开始,来初始化 PETSc 和 MPI
  • 程序退出时调用 PetscFinalize(), 来结束 PETSc 和 MPI

示例如下:

    #include "petsc.h"

    /* 自定义帮助信息 */
    static char help[] = "Solves a tridiagonal linear system with KSP.\n\n";

    int
    main(int argc, char** args)
    {

        /* 初始化 */
        PetscErrorCode ierr;
        PetscMPIInt size;
        ierr = PetscInitialize(&argc, &args, (char*)0, help);
        if (ierr)
            return ierr;
        ierr = MPI_Comm_size(PETSC_COMM_WORLD, &size);
        CHKERRMPI(ierr);
        if (size != 1)
            SETERRQ(PETSC_COMM_WORLD, PETSC_ERR_WRONG_MPI_SIZE, "This is a uniprocessor example only!");

        /* 主程序 
         * ......
         */

        /* 退出 */
        ierr = PetscFinalize();
        return ierr;
    }

错误处理

所有 PETSc 函数都返回整数值 ierr 作为消息,使用 CHECKRRQ(ierr) 宏来处理错误。

向量和并行数据

向量是 PETSc 最基本的结构,用于保存计算结果等

创建和组装向量

PETSc 提供两种向量:顺序(Seq) 和并行(MPI)

  1. VecCreateSeq(PETSC_COMM_SELF,PetscInt m,Vec *x);
    1. 创建顺序向量
  2. VecCreateMPI(MPI_Comm comm,PetscInt m,PetscInt M,Vec *x);
    1. 创建并行向量
  3. VecCreate(MPI_Comm comm,Vec *x);
    1. 创建向量, 自动选择顺序或并行
    2. MPI_Comm comm 是 MPI 通信器
  4. VecCreateSeqWithArray(PETSC_COMM_SELF,PetscInt bs,PetscInt n,PetscScalar *array,Vec *V);
  5. VecCreateMPIWithArray(MPI_Comm comm,PetscInt bs,PetscInt n,PetscInt N,PetscScalar *array,Vec *vv);
    1. 根据输入的数据创建向量
  6. VecSetSizes(Vec x, PetscInt m, PetscInt M);
    1. 设置向量的大小
    2. m 是可选的局域大小,可以设为 PETSC_DECIDE
    3. M 是总大小
  7. VecSetType()
  8. VecSetFromOptions()
    1. 设置向量的类型
  9. VecSet(Vec x,PetscScalar value);
    1. 将向量所有值都设成 value
  10. VecSetValues(Vec x,PetscInt n,PetscInt *indices,PetscScalar *values,INSERT_VALUES);
    1. 设置向量的部分值
    2. PetscInt n 要设置的值的个数
    3. PetscInt *indices 设置的值的指标
    4. PetscScalar *values 设置的值
    5. InsertMode iota 设置值的方式:
      1. INSERT_VALUES 代替旧值
      2. ADD_VALUES 将旧值加上新值
  11. VecAssemblyBegin(Vec x);
    1. 开始组装向量, 在 VecSetValues() 之后调用
  12. VecAssemblyEnd(Vec x);
    1. 结束组装向量,在 VecAssemblyBegin() 之后调用
  13. VecView(Vec x,PetscViewer v);
    1. 打印输出向量
  14. VecDuplicate(Vec old, Vec *new);
    1. 创建一个新的向量,其类型与旧的相同
  15. VecDuplicateVecs(Vec old,PetscInt n,Vec **new);
    1. 创建多个向量,与原向量类型相同
  16. VecDestroy(Vec *x);
    1. 销毁向量
  17. VecDestroyVecs(PetscInt n,Vec **vecs);
    1. 销毁多个向量
    Vec x;
    ierr = VecCreate(PETSC_COMM_WORLD, &x);
    CHKERRQ(ierr);
    ierr = PetscObjectSetName((PetscObject)x, "test");
    CHKERRQ(ierr);
    ierr = VecSetSizes(x, PETSC_DECIDE, H_len);
    CHKERRQ(ierr);
    ierr = VecSetFromOptions(x);
    CHKERRQ(ierr);
    Vec y;
    ierr = VecDuplicate(x, &y);

    int ix[1] = { 0 };
    double complex vx[1] = { 1 };
    ierr = VecSetValues(x, 1, ix, vx, INSERT_VALUES);
    CHKERRQ(ierr);
    ierr = VecAssemblyBegin(x);
    CHKERRQ(ierr);
    ierr = VecAssemblyEnd(x);
    CHKERRQ(ierr);

    struct timespec t0_mvm;
    timespec_get(&t0_mvm, TIME_UTC);

    ierr = MatMult(H, x, y);
    CHKERRQ(ierr);

    ierr = VecDestroy(&x);
    CHKERRQ(ierr);
    ierr = VecDestroy(&y);
    CHKERRQ(ierr);

基本向量操作

加减乘除之类的操作,参考函数表

  1. VecGetOwnershipRange(Vec vec,PetscInt *low,PetscInt *high);
    1. 获取当前局域操作的范围
    2. low 是第一个元素
    3. high 是最后一个再 +1
  2. VecGetArray(Vec v,PetscScalar **array);
    1. 获取当前局域的数组的所有权
    2. 这个函数直接获得了裸数据的指针
  3. VecRestoreArray(Vec v, PetscScalar **array);
    1. 恢复当前局域的数组的所有权
  4. VecGetArrayRead(Vec v, const PetscScalar **array);
    1. 只读地获取当前局域的数组
  5. VecRestoreArrayRead(Vec v, const PetscScalar **array);
    1. 恢复获取的只读数组的所有权
  6. VecGetLocalSize(Vec v,PetscInt *size);
    1. 获取当前局域的大小
  7. VecGetSize(Vec v,PetscInt *size);
    1. 获取向量的总大小

索引和排序

PETSc 提供了一组工具用于处理向量指标的顺序,称作 Application Orderings(AO)

矩阵

PETSc 支持多种矩阵类型,包括稠密矩阵、稀疏矩阵等

创建和组装矩阵

  1. MatCreate(MPI_Comm comm,Mat *A);
    1. 创建矩阵
    2. 默认的矩阵类似是 稀疏 AIJ
  2. MatSetSizes(Mat A,PETSC_DECIDE,PETSC_DECIDE,PetscInt M,PetscInt N);
    1. 设置矩阵大小
    2. 第二、三个参数是局域的行列大小
  3. MatSetType(A,MatType matype);
    1. 设置矩阵类型
    2. 矩阵类型 MatType 有很多
  4. MatSetValues(Mat A,PetscInt m,PetscInt *im,PetscInt n,PetscInt *in,PetscScalar *values,INSERT_VALUES);
    1. 设置矩阵的值
    2. 规则类似 Vec
  5. MatSetOption(Mat A,MAT_ROW_ORIENTED,PETSC_FALSE);
    1. 设置加入值的方式,行先或列先等
  6. MatAssemblyBegin(A,MAT_FINAL_ASSEMBLY);
    1. 开始组装矩阵,在所有的 MatSetValues() 调用完成后使用
  7. MatAssemblyEnd(A,MAT_FINAL_ASSEMBLY);
    1. 完成组装矩阵,在 MatAssemblyBegin() 之后调用
  8. MatGetOwnershipRange(Mat A,PetscInt *first_row,PetscInt *last_row);
    1. 获取矩阵的一部分
    Mat A;
    ierr = MatCreate(PETSC_COMM_WORLD, &A);
    CHKERRQ(ierr);
    ierr = MatSetSizes(A, PETSC_DECIDE, PETSC_DECIDE, A_len, A_len);
    CHKERRQ(ierr);
    ierr = MatSetFromOptions(A);
    CHKERRQ(ierr);
    ierr = MatSetUp(A);
    CHKERRQ(ierr);

    PetscScalar value[3] = {1,2,3};
    PetscInt col[3] = {0,1,2};
    PetscInt row[1] = {1};
    ierr = MatSetValues(A, 1, row, 2, col, value, INSERT_VALUES);
    CHKERRQ(ierr);
    ierr = MatAssemblyBegin(A, MAT_FINAL_ASSEMBLY);
    CHKERRQ(ierr);
    ierr = MatAssemblyEnd(A, MAT_FINAL_ASSEMBLY);
    CHKERRQ(ierr);

    MatView(A,PETSC_VIEWER_STDOUT_WORLD);


    ierr = MatDestroy(&A);
    CHKERRQ(ierr);

矩阵类型

PETSc默认的矩阵类型是 AIJ(也就是 CSR)

  1. 稀疏矩阵
  2. Limited-Memory Variable Metric (LMVM) Matrices
  3. 稠密矩阵
  4. 块矩阵

基本矩阵操作

参考函数表

无矩阵的矩阵

  1. MatCreateShell(MPI_Comm comm,PetscInt m,PetscInt n,PetscInt M,PetscInt N,void *ctx,Mat *mat);
    1. 创建矩阵的结构,无需添加矩阵元
    2. M, N 是矩阵总维数
    3. m, n 是局域维数
    4. ctx 是用户定义的矩阵操作
  2. MatShellSetOperation(Mat mat,MatOperation MATOP_MULT, (void(*)(void)) PetscErrorCode (*UserMult)(Mat,Vec,Vec));
    1. 注册用户定义的矩阵运算
    2. MATOP_MULT 指的是矩阵乘向量
    /* 定义每个矩阵都需要用到的一些数据 */
    typedef struct A_data {} A_data_t;

    /* 自定义的矩阵乘向量运算函数, y = A x */
    PetscErrorCode
    A_MatVec(Mat A, Vec x, Vec y)
    {

        /* 获取数据 */
        A_data_t* A_data;
        PetscErrorCode ierr;
        MatShellGetContext(A, &A_data);


        /* 获得本地的 x 向量数据 */
        PetscInt x_start, x_end;
        ierr = VecGetOwnershipRange(x, &x_start, &x_end);
        CAKERRQ(ierr);
        const PetscScalar* x_arr;
        ierr = VecGetArrayRead(x, &x_arr);
        CAKERRQ(ierr);

        /* y 向量要先置零 */
        ierr = VecSet(y, 0);
        CAKERRQ(ierr);

        /* 设置计算后 y 的值 */
        {
            PetscScalar new_value;
            ierr = VecSetValues(y, 1, &new_value, ADD_VALUES);
            CAKERRQ(ierr);
        }
        ierr = VecAssemblyBegin(y);
        CAKERRQ(ierr);
        ierr = VecAssemblyEnd(y);
        CAKERRQ(ierr);

        /* 结束计算要返还 x 的所有权 */
        ierr = VecRestoreArrayRead(x, &x_arr);
        CAKERRQ(ierr);
        return 0;
    }


    /* 创建一个临时向量,来获取 PETSc 自动分配的局域大小, 这里是方矩阵,长方形矩阵要分别获取两边的尺寸 */
    int A_local_size = 0;
    {
        Vec x;
        VecCreateMPI(PETSC_COMM_WORLD,PETSC_DECIDE,A_len,&x);
        VecGetLocalSize(x,&A_local_size);
        VecDestroy(&x);
    }

    /* 创建无矩阵的矩阵 A,必须设置每个局域块 A 的大小 */
    Mat A;
    A_data_t A_data = {};
    ierr = MatCreateShell(PETSC_COMM_WORLD, A_local_size, A_local_size, A_len, A_len, &A_data, &A);
    CAKERRQ(ierr);

    /* 为 A 绑定矩阵乘向量运算 */
    ierr = MatShellSetOperation(Afree, MATOP_MULT, (void (*)(void))A_MatVec);
    CAKERRQ(ierr);

线性求解器

使用线性求解器 KSP 解线性方程 \(Ax=b\)

  1. KSPCreate(MPI_Comm comm,KSP *ksp);
    1. 初始化 KSP
  2. KSPSetOperators(KSP ksp,Mat Amat,Mat Pmat);
    1. 设置矩阵
  3. KSPSetFromOptions(KSP ksp);
    1. 设置求解器选项
  4. KSPSolve(KSP ksp,Vec b,Vec x);
    1. 运行求解器
  5. KSPDestroy(KSP ksp);
    1. 运算结束销毁求解器

petsc4py

基本使用

   # 初始化
   import sys
   import petsc4py
   petsc4py.init(sys.argv)
   from petsc4py import PETSc


   # 矩阵
   A = PETSc.Mat().create(comm=comm)
   A.setSizes((m*n,m*n))
   A.setSizes(((m,M),(n,N))) # m,n: local, M,N: global
   A.setFromOptions()

   A.setValues(3, 4, 5.0, addv=True)
   A.assemblyBegin(A.AssemblyType.FINAL)
   A.assemblyEnd(A.AssemblyType.FINAL)

   # 向量
   u = PETSc.Vec().create(comm=comm)
   u.setSizes(m*n)
   u.setSizes((n,N), bsize=1) # n: local, N: global
   u.setFromOptions()

   # 矩阵乘向量
   b = u.duplicate()
   u.set(1.0)
   b = A(u)

无矩阵的矩阵

   class H():
       def __init__(self):
           pass

       def mult(self, mat, X, Y):
           Y = sth(X)

   shell = H()
   A = PETSc.Mat().createPython(
       [x_len, y_len], comm=comm)
   A.setPythonContext(shell)
   A.setUp()

遍历向量

   x = PETSc.Vec().create(comm=comm)
   x.setSizes(n)
   x.setFromOptions()

   rstart,rend = x.getOwnershipRange()
   nlocal = x.getLocalSize()

   x_arr: np.ndarray = x.getArray(readonly=True)

其它问题

复数支持

只能通过编译选项 --with-scalar-type=complexPetscScalar 设成复数

编译选项

参考

debug 模式:

   ./configure PETSC_ARCH=arch-complex-debug --with-scalar-type=complex

release 模式:

   ./configure PETSC_ARCH=arch-complex-release --with-cc=gcc --with-cxx=g++ --with-fc=gfortran --with-debugging=0 COPTFLAGS='-O3 -march=native -mtune=native' CXXOPTFLAGS='-O3 -march=native -mtune=native' FOPTFLAGS='-O3 -march=native -mtune=native' --with-scalar-type=complex --download-mpich

64 位支持:

   --with-64-bit-indices

python 支持:

   –with-petsc4py=1

混合线程

PETSc 是纯 MPI 框架,不提供混合线程的函数,同时所有函数也不保证线程安全。

在指定 MPI 通信器上使用 PETSc

在调用 PetscInitialize() 之前设置

   PETSC_COMM_WORLD = MPI_COMM_WORLD; // 或其它 comm

只使用 c 编译

依赖越复杂,莫名其妙的问题就越多,只需要 c 的话就简单不少。

./configure PETSC_ARCH=arch-complex-release --with-cc=mpicc --with-cxx=0 --with-fc=0 --with-debugging=0 COPTFLAGS='-O3 -march=native -mtune=native'  --with-scalar-type=complex --with-64-bit-indices  --with-make-dir=$HOME/app/make --with-blas-lapack-dir=/opt/intel/composer_xe_2015.1.133/mkl/lib/intel64

Data Structures for Sparse Matrix

稀疏矩阵的数据结构

常见结构

  • COO: Coordinate or triplet
  • CSR: Compressed Sparse Row
  • CSC: Compressed Sparse Column
  • DIA: Diagonal
  • BSR: Block Sparse Row

COO: coordinate

  1. 也叫做 triplet 格式
  2. 最简单和基本的格式,通常用做向其它格式转换的入门格式
  3. 顺序是任意的
  4. 保存三列数据:值、行指标、列指标
A:
   [[ 1,  0,  0,  2,  0],
    [ 3,  4,  0,  5,  0],
    [ 6,  0,  7,  8,  9],
    [ 0,  0, 10, 11,  0],
    [ 0,  0,  0,  0, 12]]

data:   [1, 3, 6, 4, 7, 10, 2, 5, 8, 11, 9, 12]
row(i): [0, 0, 1, 1, 1,  2, 2, 2, 2,  3, 3,  4]
col(j): [0, 3, 0, 1, 3,  0, 2, 3, 4,  2, 3,  4]

A[ i[k], j[k] ] = data[k]

CSR: Compressed Sparse Row

  1. 按行压缩,即coo格式中的数据值、列指标不变,但行指标改成指向这一行开始的列的指标
  2. 一般来说,Fortran 里使用这种格式
A:
   [[ 1,  0,  0,  2,  0],
    [ 3,  4,  0,  5,  0],
    [ 6,  0,  7,  8,  9],
    [ 0,  0, 10, 11,  0],
    [ 0,  0,  0,  0, 12]]

data:   [1, 3, 6, 4, 7, 10, 2, 5, 8, 11, 9, 12]
row(i): [0, 2, 5, 9, 11, 12]
col(j): [0, 3, 0, 1, 3,  0, 2, 3, 4,  2, 3,  4]

CSC: Compressed Sparse Column

  1. 按列压缩,与 CSR 类似,不过行列换一下
  2. c 中常用这种格式
A:
   [[ 1,  0,  0,  2,  0],
    [ 3,  4,  0,  5,  0],
    [ 6,  0,  7,  8,  9],
    [ 0,  0, 10, 11,  0],
    [ 0,  0,  0,  0, 12]]

data:   [1, 3, 6, 4, 7, 10, 2, 5, 8, 11, 9, 12]
row(i): [0, 1, 2, 1, 2,  3, 0, 1, 2,  3, 2,  4]
col(j): [0, 3, 4, 6, 10, 12]

DIA: The Diagonal format

  1. 按对角线压缩,保存偏移值,负的偏移忽略头元素,正的偏移忽略尾元素
A:
   [[ 1.,  0.,  2.,  0.,  0.],
    [ 3.,  4.,  0.,  5.,  0.],
    [ 0.,  6.,  7.,  0.,  8.],
    [ 0.,  0.,  9., 10.,  0.],
    [ 0.,  0.,  0., 11., 12.]]

data:
   [[ 3.,  6.,  9., 11., nan],
    [ 1.,  4.,  7., 10., 12.],
    [nan, nan,  2.,  5.,  8.]]

offsets: [-1,  0,  2]

BSR: Block matrices

  1. 类似 CSR,只不过每个行列指的是一个小的矩阵,这些小矩阵大小都相等

scipy 格式

  1. 很容易使用,参考 scipy - sparse

suitesparse 格式

MKL 格式

Sparse Matrix

稀疏矩阵

定义

  1. 稀疏矩阵指的是那些有很多元素为 0 的矩阵
  2. 一般而言 \(m\times n\) 的矩阵, 其非零元素的个数在 \(O(\mathrm{min}(m,n))\) 就可以作为稀疏矩阵来处理,一般来说就是每行或每列的元素个数是常数
  3. 很多非零元是 \(O(\mathrm{log}(n))\) 的矩阵不能作为稀疏矩阵

稀疏矩阵的性质

  1. 稀疏矩阵 \(A\) 的逆 \(A^{ -1}\) 一般来说是稠密的
  2. 稀疏矩阵 \(A\) 的 LU 分解 \(L\) 和 \(U\) 矩阵可以也是稀疏的

Cayley-Hamilton 定理

矩阵满足其特征方程。

例:考虑矩阵 \(A = \begin{pmatrix} 1&2 \\ 3&4 \end{pmatrix}\) ,其特征方程为

\begin{equation*} \mathrm{det} \left( \lambda I - A \right) = \mathrm{det} \begin{pmatrix} \lambda - 1 & 2 \\ 3 & \lambda - 4 \end{pmatrix} = \lambda^2 - 5 \lambda - 2 = 0 \end{equation*}

那么有

\[ A^{2} - 5 A - 2 = 0 \]

稀疏矩阵的图表示

稀疏矩阵可以用图来表示,进而可以用图论的方法分析。

图的定义 图 \(G\) 定义为一个集合 \(G = (V,E)\), \(E \subset V \times V\) ,其中 \(V\) 是顶点 (vertex) 的集合, \(E\) 是边 (edge) 的集合。

  1. 图是无向图,当且仅当矩阵是对称的。
  2. 矩阵的图表示,就是将行和列作为顶点的标号,非零矩阵元表示顶点间的连线。

稀疏矩阵与偏微分方程

典型的偏微分方程数值模拟的过程是:

  1. 物理问题
  2. 非线性偏微分方程
  3. 离散化
  4. 线性化近似
  5. 变成稀疏矩阵线性代数问题

可以想象成将自变量作为矩阵指标,离散化的偏微分方程每一步的作用结果就是利用周围的几个点的数值得到下一步当前点的值,这个过程可以写成一个稀疏矩阵。

C Library

C 语言标准库

C89有 15 个头文件, C99 新增了 9 个,一共 24个。

标识符命名限制

  1. 由一个下划线和一个大写字母或两个下划线开头的标识符是标准库保留的
  2. 由一个下划线开头的是文件作用域保留的
  3. 标准库中的外部链接标识符都是保留的

输入/输出

绝大部分内容都在 <stdio.h> 头中

流(stream) 表示任意输入/输出的源或目的地,一般程序都是从一个流获得输入,再通过另一个流输出。

文件指针

C中访问流是通过文件指针实现的,类型为 FILE *

标准流

  1. stdin 标准输入
  2. stdout 标准输出
  3. stderr 标准错误

文件类型

c中支持两种文件类型

  1. 文本文件,可分为若干行,并在文件末尾可以有一个特殊标记
  2. 二进制文件

文件操作

打开文件

    FILE *fopen(const char * restrict filename, const char * restrict mode);
  1. 当文件无法打开时返回 NULL
  2. 模式中有 "b" 说明是二进制文件

模式说明

  1. "r" 读
  2. "w" 写
  3. "a" 追加
  4. "r+" 从文件头开始读和写
  5. "w+" 读和写文件,覆盖
  6. "a+" 读和写文件,追加

    常用模式

           if ((fp = fopen(filename, "r")) == NULL) {}
    

关闭文件

    int fclose(FILE *stream);
  1. 成功关闭返回 0,否则返回错误代码 EOF

为流重新指定文件

    FILE *freopen(const char * restrict filename,
                  const char * restrict mode,
                  FILE * restrict stream);

临时文件

    /* 创建临时文件 */
    FILE *tmpfile(void);

    /* 获取临时文件的名字 */
    char *tmpnam(char *s);

文件缓冲

    /* 清洗缓冲区 */
    int fflush(FILE *stream);

    /* 设置缓冲流 */
    int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);

删除文件

    /* 删除文件 */
    int remove(const char *filename);

    /* 重命名文件 */
    int rename(const char *old, const char *new);

数学与数值计算

  1. <float.h> 定义浮点类型的范围和精度,其中没有类型和函数
  2. <limits.h> 定义整数类型的取值范围,其中没有类型和函数
  3. <math.h> 定义数学计算的函数
  4. <stdint.h> 整数类型
  5. <inttypes.h> 整数类型的格式转换
  6. <complex.h> 复数
  7. <tgmath.h> 泛型数学
  8. <fenv.h> 浮点环境

错误

当发生错误时,大多数函数会将一个错误码存储到 errno 变量中。此外,如果函数返回值大于double类型最大值会返回 HUGE_VAL 值。

  1. 定义域错误。当函数的参数超出定义域,会将 EDOM 存储到 errno
  2. 取值范围错误。当函数返回值超出double范围时,会将 ERANGE 存储到 errno

紧缩(constraction)

C99中新增加了融合乘加 (fused multiply-add) 函数,即

   a = b * c + d;
   a = fma(b, c, d);

这种合并可能会速度更快一点,编译器是否自动进行紧缩可以由 #pragma STDC FP_CONTRACT ON/OFF/DEFAULT 来控制。

字符、字符串与国际化

  1. <ctype.h> 处理字符
  2. <string.h> 处理字符串
  3. <locale.h> 本地化
  4. <iso646.h> 拼写替换
  5. <wchar.h> 多字节和宽字符工具
  6. <wctype.h> 宽字符分类和映射工具

<string.h>

提供了5种函数

  1. 复制: memcpy, memmove, strcpy, strncpy
  2. 拼接: strcat, strncat
  3. 比较: memcmp, strcmp, strcoll, strncmp, strxfrm
  4. 搜索: memchr, strchr, strcpn, strpbrk, strrchr, strspn, strstr, strtok
  5. 其它(初始化、长度): memset, strlen

错误处理

  1. <assert.h> 诊断
  2. <errno.h> 错误
  3. <signal.h> 信号处理
  4. <setjmp.h> 非局部跳转

<assert.h> 诊断

assert 是一个宏。当参数值为 0 时,assert 会向 stderr 写消息,并调用 abort 函数中止程序。

   void assert(scalar expression);

<errno.h> 错误

错误代码存储在 errno 变量中,每次使用都要把它置零

errno 主要作用是说明错误类型,而不是发生错误

<signal.h> 信号处理

信号宏

UNIX 系统提供了更多信号宏,都是 SIG开头

  1. SIGABRT: 异常终止(可能来自abort)
  2. SIGFPE: 算术错误(除0或溢出)
  3. SIGILL: 无效指令
  4. SIGINT: 中断
  5. SIGSEGV: 无效存储访问
  6. SIGTERM: 终止请求

signal 函数

    void (*signal(int sig, void (*func)(int)))(int);

    signal(SIG, handler);

指定信号的处理函数

信号处理函数

  1. SIGDFL: 按默认方式处理,实现定义,大多数时候是终止程序
  2. SIGIGN: 忽略信号

raise 函数

    int raise(int sig);

产生一个信号,0表示调用成功。

<setjmp.h> 非局部跳转

   /* 设置跳转位置 */
   int setjmp(jmp_buf env);

   /* 跳转到 val 位置 */
   void longjmp(jmp_buf env, int val);

可以实现跨函数的跳转

可变参数

<stdarg.h> 头

  /* 可变参数类型,其中保存所有的可变参数 */
  va_list ap;

  /* 将 src 中的参数复制到 dest 中 */
  void va_copy(va_list dest, va_list src);

  /* 开始读取参数列表, parmN 为保存参数个数的变量名 */
  void va_start(va_list ap, parmN);

  /* 获取一个类型为 type 的参数,类似出栈 */
  type va_arg(va_list ap; type);

  /* 清理参数列表, 每次函数结束前都要用 */
  void va_end(va_list ap);

日期和时间

<time.h> 中提供三种类型

  1. clockt 按时钟度量
  2. timet 日历时间
  3. struct tm 分解时间

时间处理函数

   clock_t clock(void);

   double difftime(time_t time1, time_t time0);

   time_t mktime(struct tm *timeptr);

   time_t time(time_t *timer);

时间转换函数

   char *asctime(const struct tm *timeptr);

   char *ctime(const time_t *timer);

   struct tm *gmtime(const time_t *timer);

   struct tm *localtime(const time_t *timer);

   size_t strftime(char * restrict s, size_t maxsize,
                   const char * restrict format,
                   const struct tm * restrict timeptr);

实用工具

包括

  1. 数值转换函数: atof, atoi, atol, strtod, strtol, strtoul, atoll, strtof, strtold, strtoll, strtoull
  2. 伪随机序列生成函数: rand, srand
  3. 内存管理函数: malloc, calloc, realloc, free
  4. 与外部通信: abort, atexit, exit, _Exit, getenv, system
  5. 搜索与排序: bsearch, qsort
  6. 整数运算函数: abs, labs, llabs, div, ldiv, lldiv
  7. 多字节/宽字符转换函数
  8. 多字节/宽字符串转换函数

Linear Regression and Gradient Descent

线性回归与梯度下降

加载数据

加载数据(load data),需要完成如下几个工作

  1. 读取和准备数据
  2. 对数据归一化
  3. 划分训练数据和测试数据

数据应该是有 \(x_{1},x_{2},\cdots,x_{n}\) 和一组 \(y\)

记住要对数据归一化, 可以选择基本的移动归一化,即减去最小值再除以最大最小之差 \[ x' = \frac{x - \mathrm{min}(x)}{\mathrm{max}(x) - \mathrm{min}(x)} \]

或者减平均值除方差

\[ x' = \frac{x - \mathrm{mean}(x)}{\mathrm{std}(x)} \]

前向传播

前向传播(forward propagation) 就是从输入参数计算得到输出参数。

线性回归公式是 \[ y = \sum_{j=1}^{M} x_{j}w_{j} + b \]

所以前向传播很容易写

   def forward(x, w, b):
       result = np.dot(x, w) + b
       return result

损失函数

损失函数(loss function) 是衡量前向计算结果与数据目标结果之间偏差的函数。

简单的损失函数是均方误差 \[ \mathrm{Loss} = \frac{1}{N} \sum_{j=1}^{N} (y_{i} - z_{i})^{2} \] 其中 \(y_{i}\) 是目标结果, \(z_{i}\) 是计算结果。

   def loss(z, y):
       result = np.mean((z - y) ** 2)
       return result

梯度

梯度(gradient) 是损失函数随参数变化的改变。

对于这里的线性回归模型和均方误差损失函数,梯度定义如下 \[ \nabla \mathrm{Loss} = \left( \frac{\partial \mathrm{Loss}}{\partial w_{i}}, \dots \right) \]

对于我们要计算的损失函数 \[ L = \frac{1}{2N} \sum_{i} (y_{i} - z_{i})^{2} \] 预测值为 \[ z_{i} = \sum_{j} x_{i}^{j} w^{j} + b \] 其中 \(i\) 是样本的指标,\(j\) 是参数的指标。 那么损失函数 \(L\) 对参数 \(w_{j}\) 的微分为 \[ \frac{\partial L}{\partial w_{j}} = \frac{ 1 }{ N } \sum_{i} (z_{i}- y_{i}) \frac{\partial z_{i }}{\partial w_{j}} =\frac{ 1 }{ N } \sum_{i} (z_{i}- y_{i}) x_{i}^{j} \]

   def gradient(x, y, z, w, b):
       gradient_w = np.mean((z - y) * x, axis=1)
       gradient_b = np.mean((z - y))
       return gradient_w, gradient_b

梯度下降

梯度下降(gradient descent) 就是根据梯度来更新参数,最终到达收敛的过程。

在这部分要实现两个函数

  1. 更新(update),即从这一步梯度和学习率(learning rate)计算下一步的梯度
  2. 训练(train),即迭代执行前向计算-梯度-更新,并根据损失函数判断是否收敛

梯度下降的更新函数就是用上一步的参数减去学习率与梯度之积 \[ w_{i+1}^{j} = w_{i}^{j} - \eta \frac{\partial \mathrm{Loss}}{\partial w_{i}^{j}} \]

   def update(w, b, gradient_w, gradient_b, eta):
       w_new = w - eta * gradient_w
       b_new = b - eta * gradient_b
       return w_new, b_new
   def train(x, y, w, b, eta, N):
       L_list = []
       for i in range(N):
           z = forward(x, w, b)
           L = loss(z, y)
           gradient_w, gradient_b = gradient(x, y, z, w, b)
           w, b = update(w, b, gradient_w, gradient_b, eta)
           L_list.append(L)
       return w, b, L_list

随机梯度下降

随机梯度下降(Stochastic Gradient Descent, SGD) 是指每步迭代时从总训练集抽取一小部分来计算梯度,通过这种方式能够加快训练。 一些概念:

  • mini batch: 每次迭代时用到的那一小部分数据集
  • batch size: 一个 mini batch 中的样本数
  • epoch: 迭代时不断抽取 mini batch,当取过一遍整个数据集时就叫做一轮 epoch

具体的操作:

  1. 将总的数据集随机打乱
  2. 将打乱后的数据集划分成若干个 mini batch
  3. 用每个 mini batch 进行一次训练
  4. 用所有 mini batch 训练过一遍后,返回第 1 步,开启下一轮 epoch

dvc: Data Version Control

dvc 是一个增强 git 管理大二进制文件能力的工具。

基本使用

初始化 dvc

在一个 git 项目目录中运行

   dvc init
   git commit -m "Initialize DVC"

就自动创建好了 dvc 需要东西。

dvc 是寄生在git中的,其本身不提供版本管理能力,完全依靠 git 。

添加数据

如果有一个大的数据文件 data/data.h5 ,那么就首先将它添加给 dvc, 然后把dvc创建的记录 *.dvc 和自动生成的 .gitignore 一起加到 git 中,之后再用 dvc 上传数据即可

   dvc add data/data.h5
   git add data/data.h5.dvc data/.gitignore
   git commit -m "Add raw data"

dvc的远程库

dvc 支持多种远程库,甚至是本地的远程库

   mkdir -p /tmp/dvcstore
   dvc remote add -d myremote /tmp/dvcstore
   git commit .dvc/config -m "Configure local remote"

保存和同步数据

如果已经配置好了远程库,那么上传和下载数据操作就类似于 git

   dvc pull

   dvc push