12
$\begingroup$

I would like to compute the discrete Laplacian of a real matrix (numeric values and full), using any method and targetting efficiency (I will call the Laplacian dozens of thousands of time).

I naively defined the following function:

laplacian[Z_] := Block[{Zcenter, Ztop, Zleft, Zbottom, Zright},
  Zcenter = Z[[2 ;; -2, 2 ;; -2]];
  Ztop = Z[[;; -3, 2 ;; -2]]; 
  Zleft = Z[[2 ;; -2, ;; -3]]; 
  Zbottom = Z[[3 ;;, 2 ;; -2]];
  Zright = Z[[2 ;; -2, 3 ;;]];
  Ztop + Zleft + Zbottom + Zright - 4*Zcenter
]

It reduces the dimension of the input (because the Laplacian for the elements of the border of the array is not computed) but I am fine with that.

I also tried writing the function in a compiled way:

compileLaplacian = Compile[{{Z, _Real, 2}},
  Module[{Zcenter = Z[[2 ;; -2, 2 ;; -2]], 
          Ztop = Z[[;; -3, 2 ;; -2]],
          Zleft = Z[[2 ;; -2, ;; -3]], 
          Zbottom = Z[[3 ;;, 2 ;; -2]],
          Zright = Z[[2 ;; -2, 3 ;;]]},
    Ztop + Zleft + Zbottom + Zright - 4*Zcenter
  ]
]

but it returns the error

Compile::cpintlt: 3;;All at position 2 of Z[[3;;All,2;;-2]] should be either a nonzero integer or a vector of nonzero integers; evaluation will use the uncompiled function.

Can I improve my discrete Laplacian function in terms of computation time? (targeted matrices are $100\times 100$ to $10000\times 10000$)


Edit The following graph summarizes the timings for the different proposed functions. RAM is not monitored. I'll investigate Szabolcs's suggestion using packed array to see if timing can be further reduced.

enter image description here

Full code for the image:

laplacian[Z_] := 
  Block[{Zcenter, Ztop, Zleft, Zbottom, Zright}, 
    Zcenter = Z[[2 ;; -2, 2 ;; -2]];
    Ztop = Z[[;; -3, 2 ;; -2]];
    Zleft = Z[[2 ;; -2, ;; -3]];
    Zbottom = Z[[3 ;;, 2 ;; -2]];
    Zright = Z[[2 ;; -2, 3 ;;]];
    Ztop + Zleft + Zbottom + Zright - 4*Zcenter]
lapJM[Z_] := 
  Differences[ArrayPad[Z, {{0, 0}, {-1, -1}}], 2] + 
    Differences[ArrayPad[Z, {{-1, -1}, {0, 0}}], {0, 2}]

<< CompiledFunctionTools`
Compiler`$CCompilerOptions = {"SystemCompileOptions" -> "-fPIC -Ofast -march=native"};

lapxzczd = 
  Hold@Compile[{{z, _Real, 2}}, 
    Module[{d1, d2}, {d1, d2} = Dimensions@z;
      Table[
        z[[i + 1, j]] + z[[i, j + 1]] + z[[i - 1, j]] + 
         z[[i, j - 1]] - 4 z[[i, j]], 
        {i, 2, d1 - 1}, {j, 2, d2 - 1}]
    ], 
    CompilationTarget -> "C", 
    RuntimeOptions -> "Speed"] /. Part -> Compile`GetElement // ReleaseHold;

d2 = SparseArray@
   N@Sum[NDSolve`FiniteDifferenceDerivative[i, {#, #} &[Range[1000]], 
       "DifferenceOrder" -> 2][
      "DifferentiationMatrix"], {i, {{2, 0}, {0, 2}}}];

lapJens[values_] := Partition[d2.Flatten[values], Length[values]]

src = "
  #include \"WolframLibrary.h\"

  DLLEXPORT int laplacian(WolframLibraryData libData, mint Argc, \
MArgument *Args, MArgument Res) {
      MTensor tensor_A, tensor_B;
      mreal *a, *b;
      mint const *A_dims;
      mint n;
      int err;
      mint dims[2];
      mint i, j;
      tensor_A = MArgument_getMTensor(Args[0]);
      a = libData->MTensor_getRealData(tensor_A);
      A_dims = libData->MTensor_getDimensions(tensor_A);
      n = A_dims[0];
      dims[0] = dims[1] = n - 2;
      err = libData->MTensor_new(MType_Real, 2, dims, &tensor_B);
      b = libData->MTensor_getRealData(tensor_B);
      for (i = 1; i <= n - 2; i++) {
          for (j = 1; j <= n - 2; j++) {
              b[(n-2)*(i-1)+j-1] = a[n*(i-1)+j] + a[n*i+j-1] + \
a[n*(i+1)+j] + a[n*i+j+1]- 4*a[n*i+j];
          }
      }
      MArgument_setMTensor(Res, tensor_B);
      return LIBRARY_NO_ERROR;
  }
  ";
Needs["CCompilerDriver`"]
lib = CreateLibrary[src, "laplacian"];
lapShutao = LibraryFunctionLoad[lib, "laplacian", {{Real, 2}}, {Real, 2}];

compare[n_] := Block[{mat = RandomReal[10, {n, n}]},
  d2 = SparseArray@
    N@Sum[NDSolve`FiniteDifferenceDerivative[i, {#, #} &[Range[n]], 
        "DifferenceOrder" -> 2][
       "DifferentiationMatrix"], {i, {{2, 0}, {0, 2}}}];
  {AbsoluteTiming[Array[laplacian[mat] &, 10];], 
    If[n > 1000, {12345, 0}, 
     AbsoluteTiming[Array[lapJM[mat] &, 10];]], 
    AbsoluteTiming[Array[lapxzczd[mat] &, 10];], 
    AbsoluteTiming[Array[lapJens[mat] &, 10];], 
    AbsoluteTiming[Array[lapShutao[mat] &, 10];]}[[All, 1]]]

tab = Table[{Floor[1.3^i], #} & /@ compare[Floor[1.3^i]], {i, 6, 31}];

ListLinePlot[Transpose@tab, 
  PlotLegends -> {"original", "JM", "xzczd", "Jens", "Shutao"}, 
  AxesLabel -> {"Size", "Time"}]
$\endgroup$
7
  • $\begingroup$ What is dx supposed to be? $\endgroup$ Commented Aug 13, 2016 at 20:20
  • $\begingroup$ @J.M. Oops, I forgot to mention this. dx is the ratio between the "real dimension" (of the continuous problem) and the number of points used in the discretized problem. Using dx=1 is fine here. $\endgroup$
    – anderstood
    Commented Aug 13, 2016 at 20:22
  • 1
    $\begingroup$ OK, try Differences[ArrayPad[Z, {{0, 0}, {-1, -1}}], 2] + Differences[ArrayPad[Z, {{-1, -1}, {0, 0}}], {0, 2}]. $\endgroup$ Commented Aug 13, 2016 at 20:57
  • $\begingroup$ @JM see my edit, it works but it seems slower. $\endgroup$
    – anderstood
    Commented Aug 13, 2016 at 23:17
  • $\begingroup$ The edit plot might be more informative in log-log axes $\endgroup$ Commented Aug 17, 2016 at 11:03

4 Answers 4

6
$\begingroup$

Accodrding to your laplacian[] function, I can draw the following conclusion:

For a matrix $A_{n\times n}$

$$ \left( \begin{array}{cccc} a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\ a_{2,1} & a_{1,1} & \cdots & a_{2,n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n,1} & a_{n,2} & \cdots & a_{n,n} \\ \end{array} \right)_{n \times n} $$

$$\mathcal{L}(a_{i,j}) \Longleftrightarrow b_{i-1,j-1}=a_{i-1,j}+a_{i+1,j}+a_{i,j-1}+a_{i,j+1}-4 \cdot a_{i,j}$$

where, the $b_{i,j}$ is the element of matrix $B_{(n-2)\times(n-2)}$, and $i=2,\cdots ,n-1, \quad j=2,\cdots,n-1$

Here, I will give a C solution with the help of LibraryLink wrapper.

src = "
#include \"WolframLibrary.h\"

DLLEXPORT int laplacian(WolframLibraryData libData, mint Argc, MArgument *Args, MArgument Res) {
    MTensor tensor_A, tensor_B;
    mreal *a, *b;
    mint const *A_dims;
    mint n;
    int err;
    mint dims[2];
    mint i, j, idx;
    tensor_A = MArgument_getMTensor(Args[0]);
    a = libData->MTensor_getRealData(tensor_A);
    A_dims = libData->MTensor_getDimensions(tensor_A);
    n = A_dims[0];
    dims[0] = dims[1] = n - 2;
    err = libData->MTensor_new(MType_Real, 2, dims, &tensor_B);
    b = libData->MTensor_getRealData(tensor_B);
    for (i = 1; i <= n - 2; i++) {
        for (j = 1; j <= n - 2; j++) {
            idx = n*i + j;
            b[idx+1-2*i-n] = a[idx-n] + a[idx-1] + a[idx+n] + a[idx+1] - 4*a[idx];
        }
    }
    MArgument_setMTensor(Res, tensor_B);
    return LIBRARY_NO_ERROR;
}
";

Needs["CCompilerDriver`"]
lib = CreateLibrary[src, "laplacian"];

lapShutao = LibraryFunctionLoad[lib, "laplacian", {{Real, 2}}, {Real, 2}]

OK, let's test it

mat = RandomReal[10, {1000, 1000}];
lapShutao[mat]; // AbsoluteTiming

enter image description here

Remark:

In my laptop that with 4GB RAM, I discovered that when $n = 15000$, the lapShutao[] and cLa[] will lead to system halted.

Update

For the following code:

b[(n-2)*(i-1)+j-1] = a[n*(i-1)+j] + a[n*i+j-1] + a[n*(i+1)+j] + a[n*i+j+1] - 4*a[n*i+j];

let idx = n*i+j, then the above code could be refactored as below:

b[idx+1-2*i-n] = a[idx-n] + a[idx-1] + a[idx+n] + a[idx+1] - 4*a[idx];
$\endgroup$
6
  • $\begingroup$ What's your timing for laplacian? Have you already set something like "SystemCompileOptio‌​ns"->"-Ofast"? $\endgroup$
    – xzczd
    Commented Aug 15, 2016 at 12:59
  • $\begingroup$ I mean the laplacian written by OP… $\endgroup$
    – xzczd
    Commented Aug 15, 2016 at 13:37
  • 1
    $\begingroup$ I had to delete the space on the left of WolframLibrary.h and in LIBRARY_NO _ERROR; in the end. I am preparing a benchmark to compare all proposed functions. $\endgroup$
    – anderstood
    Commented Aug 15, 2016 at 15:29
  • $\begingroup$ @anderstood Same in my PC. Thanks for pointing out. (I thought something is wrong again with my compiler before reading your comment! ) $\endgroup$
    – xzczd
    Commented Aug 16, 2016 at 2:39
  • 1
    $\begingroup$ Accepted answer because it's the most efficient according to my experiments; however, other users might also be interested in the simpler solution of xzcd. $\endgroup$
    – anderstood
    Commented Sep 25, 2016 at 15:51
13
$\begingroup$

ListCorrelate is a very general function to handle operations of this type. The Laplacian is a correlation with a specific kernel.

ker = {{0, 1, 0}, {1, -4, 1}, {0, 1, 0}};
ListCorrelate[ker, Z]

which gives the same answer as your laplacian function. As for speed, this appears to be faster than laplacian for small sizes, slower for medium sizes, and faster for larger sizes. Consider the comparison:

n = 2^14;
mat = RandomReal[10, {n, n}];
AbsoluteTiming[laplacian[mat]][[1]]
ker = {{0, 1, 0}, {1, -4, 1}, {0, 1, 0}};
AbsoluteTiming[ListCorrelate[ker, mat]][[1]]
(*
  81.0357
  35.7189 
*)
$\endgroup$
7
  • $\begingroup$ It's very short and is fast when the size of the matrix is small (~100) but because very slow when it increases (10 times slower when Z is $1000\times 1000$). $\endgroup$
    – anderstood
    Commented Aug 14, 2016 at 3:15
  • $\begingroup$ You didn't mention what kind of matrices you have, @anderstood. Are they matrices of exact or inexact numbers? Are they sparse, or packed? $\endgroup$ Commented Aug 14, 2016 at 8:33
  • $\begingroup$ The Laplacian kernel is separable into two one-dimensional kernels. In principle, less computation would be needed. $\endgroup$
    – mikado
    Commented Aug 14, 2016 at 9:02
  • $\begingroup$ Could you test the performance of my function in your laptop? I cannot do that test because my laptop lacks enough memory. $\endgroup$
    – xyz
    Commented Aug 15, 2016 at 13:31
  • $\begingroup$ @J.M. Edited. Matrices are full of numeric real values (not sparse nor packed). $\endgroup$
    – anderstood
    Commented Aug 15, 2016 at 14:29
10
$\begingroup$

Your compiling fails because 3;; can't be compiled, see here for more information. The easiest fix is to modify it to 3 ;; -1, but this leads to no improvement for speed. To write a faster laplacian, we can make use of the experience obtained in this post:

cLa = Hold@Compile[{{z, _Real, 2}}, 
     Module[{d1, d2}, {d1, d2} = Dimensions@z; 
      Table[z[[i + 1, j]] + z[[i, j + 1]] + z[[i - 1, j]] + z[[i, j - 1]] - 
        4 z[[i, j]], {i, 2, d1 - 1}, {j, 2, d2 - 1}]], CompilationTarget -> C, 
     RuntimeOptions -> "Speed"] /. Part -> Compile`GetElement // ReleaseHold;

mat = RandomReal[10, {1000, 1000}];
Array[laplacian[mat] &, 10]; // AbsoluteTiming
(* {0.447978, Null} *)
Array[cLa[mat] &, 10]; // AbsoluteTiming
(* {0.084375, Null} *)
$\endgroup$
3
  • $\begingroup$ That's strange: on my PC (Ubuntu 64 bits), MMA 11.0 gives me the same time for the first one (around 0.4), but between 0.20 and 0.25 for the second one. It's still a substantial gain. I'll check this further a bit later. $\endgroup$
    – anderstood
    Commented Aug 14, 2016 at 3:18
  • 1
    $\begingroup$ @anderstood I'm on MMA 9.0.1, win10 64bit, with TDM-GCC 4.9.2, "SystemCompileOptions"->"-Ofast" $\endgroup$
    – xzczd
    Commented Aug 14, 2016 at 3:22
  • $\begingroup$ Thank you, now I have the same timing, using << CompiledFunctionToolsCompiler$CCompilerOptions = {"SystemCompileOptions" -> "-fPIC -Ofast -march=native"}; $\endgroup$
    – anderstood
    Commented Aug 14, 2016 at 3:38
6
$\begingroup$

You can get very good speed and additional flexibility by using the built-in functionality described in "The Numerical Method of Lines". For example, here I assume that the dimensions of the matrix you call Z are known a priori, so that they can be hard-coded. That allows me to define a matrix d2 that performs the Laplacian on the flattened version of Z:

d2 = 
  SparseArray@
   N@Sum[NDSolve`FiniteDifferenceDerivative[i, {#, #} &[Range[1000]], 
       "DifferenceOrder" -> 2]["DifferentiationMatrix"], {i, {{2, 0}, {0, 2}}}];

lap2[values_] := Partition[d2.Flatten[values], Length[values]]

Z = RandomReal[10, {1000, 1000}];
Array[lap2[Z] &, 10]; // AbsoluteTiming

(* ==> {0.079342, Null} *)

In the function FiniteDifferenceDerivative, I could now also specify higher differentiation orders than 2. The generalization to three dimensions would also be straightforward, by changing the sum to run over {{2,0,0},{0,2,0},{0,0,2}} instead of {{2, 0}, {0, 2}}.

Another option is to add ,PeriodicInterpolation->True after the "DifferenceOrder" option. So this lets you do lots of variations on the Laplacian very efficiently.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.