The function $x \mapsto P_x([a,b])$ is continuous and piecewise linear, so it can be easily and exactly integrated with the trapezoidal rule. Then the resulting quadrature rule can be compiled for better performance. The only problem is to place the quadrature points onto the discontinuities of the first derivative.
The following CompiledFunction
getRowofQ
does precisely that and computes a single row of the matrix $Q$ at once. Computation of the whole matrix $Q$ can than performed in parallel by exploiting that getRowofQ
has attribute Listable
.
Block[{x, k, aj, bj, σ},
P = Function @@ {
{x, σ, aj, bj},
Simplify[
PiecewiseExpand[
1/2 Ramp[ Min[(bj^3 - x)/σ, 1] - Max[(aj^3 - x)/σ, -1]]
],
σ > 0
]
};
getRowofQ =
With[{Pcode = N@P[Compile`GetElement[x, k], σ, aj, bj]},
Compile[{{ai, _Real}, {bi, _Real}, {a, _Real, 1}, {b, _Real, 1}, {σ, _Real}},
Block[{aj, bj, x, ω, λ, factor},
x = Table[0., {6}];
x[[1]] = ai;
x[[6]] = bi;
ω = Table[0., {6}];
factor = 1./(bi - ai);
Table[
aj = Compile`GetElement[a, j];
bj = Compile`GetElement[b, j];
(*Finding the quadrature points.*)
x[[2 ;; 5]] = Sort[{
Max[Min[aj^3 - σ, bi], ai],
Max[Min[aj^3 + σ, bi], ai],
Max[Min[bj^3 - σ, bi], ai],
Max[Min[bj^3 + σ, bi], ai]
}];
(*Computing the quadrature weights.*)
ω[[1]] = 0.;
Do[
λ = 0.5 (Compile`GetElement[x, k + 1] - Compile`GetElement[x, k]);
ω[[k]] += λ;
ω[[k + 1]] = λ;, {k, 1, 5}];
(*Computing the integral.*)
factor Sum[
If[Compile`GetElement[ω, k] > 0.,
Compile`GetElement[ω, k] Pcode,
0.
], {k, 1, 6}]
, {j, 1, Length[a]}]
],
CompilationTarget -> "C",
RuntimeAttributes -> {Listable},
Parallelization -> True,
RuntimeOptions -> "Speed"
]
];
]
So, let's try it on a test problem:
n = 100;
σ = RandomReal[{2/3/Sqrt[3], 5}];
xminus = Root[σ - #1 + #1^3 &, 1];
xplus = Root[-σ - #1 + #1^3 &, 1];
xlist = Subdivide[xminus, xplus, n];
m = Differences[xlist];
a = Most[xlist];
b = Rest[xlist];
Q = getRowofQ[a, b, a, b, σ]; // AbsoluteTiming // First
0.001608
Checking the row sums:
MinMax[Total[Q, {2}]] == {1., 1.}
True
Comparison with NIntegrate
:
int[ai_, bi_, aj_, bj_, σ_] := NIntegrate[P[x, σ, aj, bj], {x, ai, bi}]/(bi - ai);
Qref = ParallelTable[
int[a[[i]], b[[i]], a[[j]], b[[j]], σ],
{i, 1, Length[a]}, {j, 1, Length[a]}
]; // AbsoluteTiming // First
Max[Abs[Q - Qref]]
20.9859
1.22125*10^-14
This is about $12000$ times faster with negligible error.
For $n = 10000$, the matrix Q
is computed by getRowofQ
in less than 12 seconds. It is not as fast as the function in the previous version of my post (it can certainly be optimized by unfolding the sort code), but it is hopefully more correct.
As always, please check whether this code really produces the desired result in your use cases.
Matrix-vector multiplication for $Q^\top$
An $O(n^2)$-implementation for the matrix-vector multiplication $u.Q$ is given below. Just us it like this getQTDotU[a, b, a, b, σ, u]
.
getQTDotU =
With[{Pcode = N@P[Compile`GetElement[x, k], σ, aj, bj]},
Compile[{{aj, _Real}, {bj, _Real}, {a, _Real, 1}, {b, _Real, 1}, {σ, _Real}, {u, _Real, 1}},
Block[{ai, bi, x, ω, λ, factor, sum},
x = Table[0., {6}];
ω = Table[0., {6}];
sum = 0.;
Do[
ai = Compile`GetElement[a, i];
bi = Compile`GetElement[b, i];
factor = 1./(bi - ai);
x[[1]] = ai;
x[[6]] = bi;
(*Finding the quadrature points.*)
x[[2 ;; 5]] = Sort[{
Max[Min[aj^3 - σ, bi], ai],
Max[Min[aj^3 + σ, bi], ai],
Max[Min[bj^3 - σ, bi], ai],
Max[Min[bj^3 + σ, bi], ai]
}];
(*Computing the quadrature weights.*)
ω[[1]] = 0.;
Do[λ = 0.5 (Compile`GetElement[x, k + 1] - Compile`GetElement[x, k]);
ω[[k]] += λ;
ω[[k + 1]] = λ;, {k, 1, 5}];
(*Computing the integral.*)
sum += Times[
Compile`GetElement[u, i],
factor,
Sum[
If[Compile`GetElement[ω, k] > 0., Compile`GetElement[ω, k] Pcode, 0.],
{k, 1, 6}]
],
{i, 1, Length[a]}];
sum
],
CompilationTarget -> "C",
RuntimeAttributes -> {Listable},
Parallelization -> True,
RuntimeOptions -> "Speed"]
];
Power Iteration
Setting up the power iteration is straight-forward:
n = 1000;
σ = RandomReal[{2/3/Sqrt[3], 5}];
xminus = Root[σ - #1 + #1^3 &, 1];
xplus = Root[-σ - #1 + #1^3 &, 1];
xlist = Subdivide[xminus, xplus, n];
m = Differences[xlist];
a = Most[xlist];
b = Rest[xlist];
Q = getRowofQ[a, b, a, b, σ];
Of course, I computed Q
only for comparison.
u = ConstantArray[1., n];
v = FixedPointList[Normalize[getQTDotU[a, b, a, b, σ, #]] &, u,
SameTest -> (Norm[#1 - #2]/Sqrt[n] < 1*^-15 &)];
Length[v]
Norm[v[[-1]] - v[[-1]].Q]
15
1.67708*10^-15
As we can see, 15 iterations where enough to produce a dominant eigenvector (with eigenvalue 1).
Of course, this takes roughly 15 times as long as assembling the matrix Q
. But as it turns out, the matrix-vector multiplication u.Q
can be implemented much faster (see below).
Further optimization potential
It is always worthwhile to plot the matrices that one works with. ArrayPlot[Q]
reveals that the structure of Q
is pretty boring: The columns of Q
are basically step functions with at most 2 steps and essentially two values: 0
and another nonzero value. (There are also 2 or three entries per column around the jump position that take other values.) It should be pretty straight-forward to determine where exaxtly the jump occurs in each column, what the dominant nonzero function value is (and where and what the other 2--3 entries are).
Let's forget for a moment about these 2--3 other values (the should not influence the total outcome of matrix-vector products significantly).
Then the i
-th column of Q
can be fully described by (i) two integers α[[i]]
and β[[i]]
that mark the jump positions and (ii) a single nonzero value c[[i]]
.
Then the i
-th entry of the matrix-vector product u.Q
can be be computed by just summing the entries of u
that correspond to the nonzero positions in the i
-th column of Q
and by multiplying the sum with the corresponding nonzero value in the i
-th column of Q
, i.e., by
Total[u[[α[[i]];;β[[i]]] c[[i]]
In fact, it is even more efficient to set
U = Prepend[Accumulate[u],0.]
and to compute the i
-th entry of u.Q
by
(U[[α[[i]]]]-U[[β[[i]]-1]]) c[[i]]
So the matrix-vector product u.Q
can be computed in $O(n)$ time which is considerably less than computing Q
once in $O(n^2)$ time and computing u.Q
naively also in $O(n^2)$ time.
Unfortunately, I do not have the time to set up the details. But if your work heavily relies on this, you should investigate this further.
DistributeDefinitions
andParallelCombine
could be worth looking into. Also instead of nestingIf
statements I would useWhich
orDispatch
. I am thinking that for large integer values I would look into usingMod[ m, n ]
and use this difference instead of the original number. I am also thinking intoCompile
and in theMethod->"CoarsestGrained"
option forParallelCombine
. I am still looking into it. To save intermediate resultsSow
andReap
instead ofg[x]:= g =expr_
. $\endgroup$