03_resolvent_and_expv.cpp¶
Shifted solves (sI - A)x = b at many shifts, and the action of the matrix exponential.
hessenberg_resolvent R(A) reduces A to Hessenberg form once. After that, shift(R, s) factors sI - A in O(n^2), and solve_batch solves at many shifts in parallel. expv(t, A, v) computes \(e^{tA}v\) from a Krylov subspace without ever forming \(e^{tA}\). When A is a Markov generator, \(e^{tA}\) conserves total probability, which the output shows.
#include <cmath>
#include <complex>
#include <cstdio>
#include <numerics.hpp>
#include <string_view>
#include <vector>
using namespace num;
int main(int argc, char **argv) {
const bool plot = argc > 1 && std::string_view(argv[1]) == "--plot";
// A birth-death chain on 30 states: A is a generator (columns sum to zero).
constexpr idx n = 30;
mat<real> A(n, n, 0.0);
for (idx j = 0; j < n; ++j) {
if (j + 1 < n) {
A(j + 1, j) += 1.0; // birth
A(j, j) -= 1.0;
}
if (j > 0) {
A(j - 1, j) += 0.6; // death
A(j, j) -= 0.6;
}
}
vec<real> p0(n, 0.0);
p0[0] = 1.0;
// One shift, checked against its residual.
const hessenberg_resolvent R(A);
const cplx s(1.0, 2.0);
const vec<cplx> z = solve(shift(R, s), p0);
real residual = 0.0;
for (idx i = 0; i < n; ++i) {
cplx row = s * z[i];
for (idx j = 0; j < n; ++j) {
row -= A(i, j) * z[j];
}
residual = std::max(residual, std::abs(row - p0[i]));
}
std::printf("(sI - A)z = p0 at s = 1+2i: z[0] = %.5f%+.5fi residual %.1e\n", z[0].real(),
z[0].imag(), residual);
// Many shifts from the one reduction.
array<cplx> shifts;
for (int k = 0; k < 64; ++k) {
shifts.push_back(cplx(1.0, 0.25 * k));
}
const auto batch = solve_batch(R, shifts, p0);
std::printf("%zu shifts solved from one Hessenberg reduction; |z[0]| at s = 1+15.75i: %.5f\n",
batch.size(), std::abs(batch.back()[0]));
// p(t) = e^{tA} p0 conserves probability.
const operators::dense_op op(A);
std::vector<double> times, mean;
std::printf("\n t sum p(t) mean state\n");
for (real t : {0.5, 1.0, 2.0, 5.0, 10.0}) {
const vec<real> p = expv(t, op, p0, 30, 1e-10);
real total = 0.0, average = 0.0;
for (idx i = 0; i < n; ++i) {
total += p[i];
average += static_cast<real>(i) * p[i];
}
std::printf("%5.1f %10.8f %9.4f\n", t, total, average);
times.push_back(t);
mean.push_back(average);
}
if (plot) {
plt::plot(times, mean, "mean state", "linespoints");
plt::title("03 Expected state of a birth-death chain");
plt::show();
}
}
Possible output:
(sI - A)z = p0 at s = 1+2i: z[0] = 0.23377-0.26651i residual 5.7e-17
64 shifts solved from one Hessenberg reduction; |z[0]| at s = 1+15.75i: 0.06284
t sum p(t) mean state
0.5 1.00000000 0.4410
1.0 1.00000000 0.8058
2.0 1.00000000 1.4264
5.0 1.00000000 2.9681
10.0 1.00000000 5.2139