玩具模型上的示例

该文档是针对示例文件 toy_model.rs 的补充说明。

1. 问题描述

该问题是一个简单的 4x4 矩阵求解问题:

一般来说,该问题是 复杂度的,因为该问题需要先对 分解为上三角、对角、下三角矩阵的乘积 (Cholesky 若对称半正定,SVD 或 QR 若非对称,等等),将问题化为若干 三角矩阵求解问题;但矩阵的分解本身是 的。

现在,我们注意到 矩阵本身具有特征:其矩阵数值上,对角矩阵元相比于非对角元大许多。譬如示例中用到的矩阵

#![allow(unused)]
fn main() {
[
    [1.0, 0.1, 0.3, 0.2],
    [0.1, 1.5, 0.2, 0.1],
    [0.3, 0.2, 1.8, 0.2],
    [0.2, 0.1, 0.2, 1.3],
]
}

在这种情况下,我们记矩阵 是矩阵 的对角元构成的矩阵;其对角值向量是

这种具有对角值大特性的矩阵,是否有办法避免 复杂度的计算?DIIS 提供一种 (不保证一定求解成功,但在计算化学中屡试不爽) 的策略。

2. 简单迭代策略

在具体应用中,DIIS 迭代需要以简单迭代算法为基础。对于当前问题,简单迭代算法设计如下。

首先,初猜向量可以通过下述表达式给出: 这里的除号是依元素除以的意思。因此,我们就以 作为初猜。

其次,对于迭代过程中的向量 ,为了得到下一次的向量 ,我们注意并联立下述两式: 得到 我们就将该近似式定义为 。这样就得到了简单迭代策略。

3. 简单迭代程序实现

程序实现如下。

#![allow(unused)]
fn main() {
let f = |x: &Tsr<f64>| &a % x - &b;

// the initial guess
let mut x = &b / &d;
let mut x0;

// the number of iterations and the tolerance
let mut niter = 0;
let maxiter = 20;
let tol = 1e-7;

while niter < maxiter && f(&x).l2_norm_all() > tol {
    niter += 1;
    x0 = x;
    let b0 = &a % &x0;
    x = (&b - b0) / &d + &x0;
}
}

最终实际迭代次数是 19 次。

4. DIIS 实现

DIIS 相对于简单迭代情景,所需要增加的代码很少:

  • 初始化 DIIS 实例需要 1--2 行;
  • 将初猜提供给 DIIS 需要 1 行;
  • 迭代过程中 DIIS 外推更新需要 1 行。
#![allow(unused)]
fn main() {
let f = |x: &Tsr<f64>| &a % x - &b;

// the initial guess
let mut x = &b / &d;
let mut x0;

// DIIS incore driver
let diis_flags = DIISIncoreFlagsBuilder::default().build().unwrap(); // <==
let mut diis = DIISIncore::<f64>::new(diis_flags, &device);          // <==
// initial guess to DIIS
diis.update(x.to_owned(), None, None);                               // <==

// the number of iterations and the tolerance
let mut niter = 0;
let maxiter = 20;
let tol = 1e-7;

while niter < maxiter && f(&x).l2_norm_all() > tol {
    niter += 1;
    x0 = x;
    let b0 = &a % &x0;
    x = (&b - b0) / &d + &x0;
    x = diis.update(x, None, None);                                  // <==
}
}

经过 DIIS 加速,该问题可以在 5 次迭代中完成。

5. 同时允许 semi-incore (disk I/O) 或 fully incore 实现的 DIIS

在实际的化学问题中,我们可能需要同时处理 semi-incore 与 fully incore 两种 DIIS:在大量计算的较小体系的情景下,我们希望用 fully incore DIIS 以增加效率;在计算较大体系时,则使用 semi-incore DIIS 以保证内存空间充足。

在这种情形下,DIISAPI 作为 dyn-compatible trait 提供了一种方式,以使得 semi-incore 与 fully incore 能同时处理:

#![allow(unused)]
fn main() {
// DIIS incore driver
let semi_incore = x.size() < 3; // assuming x > 3 requires semi-incore (disk I/O)
let mut diis: Box<dyn DIISAPI<Tsr<f64>>> = match semi_incore {
    false => Box::new({
        let diis_flags = DIISIncoreFlagsBuilder::default().build().unwrap();
        DIISIncore::<f64>::new(diis_flags, &device)
    }),
    true => Box::new({
        let diis_flags = DIISSemiIncoreFlagsBuilder::default().build().unwrap();
        DIISSemiIncore::<f64>::new(diis_flags, &device)
    }),
};
// initial guess to DIIS
diis.update(x.to_owned(), None, None);
}