# Efficient discrete Laplacian of a matrix

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.

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 -> CompileGetElement // ReleaseHold; d2 = SparseArray@ N@Sum[NDSolveFiniteDifferenceDerivative[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[NDSolveFiniteDifferenceDerivative[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"}]  • What is dx supposed to be? Aug 13 '16 at 20:20 • @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. Aug 13 '16 at 20:22 • OK, try Differences[ArrayPad[Z, {{0, 0}, {-1, -1}}], 2] + Differences[ArrayPad[Z, {{-1, -1}, {0, 0}}], {0, 2}]. Aug 13 '16 at 20:57 • @JM see my edit, it works but it seems slower. Aug 13 '16 at 23:17 • The edit plot might be more informative in log-log axes Aug 17 '16 at 11:03 ## 4 Answers 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  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];  • What's your timing for laplacian? Have you already set something like "SystemCompileOptio‌​ns"->"-Ofast"? Aug 15 '16 at 12:59 • I mean the laplacian written by OP… Aug 15 '16 at 13:37 • 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. Aug 15 '16 at 15:29 • @anderstood Same in my PC. Thanks for pointing out. (I thought something is wrong again with my compiler before reading your comment! ) Aug 16 '16 at 2:39 • 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. Sep 25 '16 at 15:51 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 *)  • 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$). Aug 14 '16 at 3:15 • You didn't mention what kind of matrices you have, @anderstood. Are they matrices of exact or inexact numbers? Are they sparse, or packed? Aug 14 '16 at 8:33 • The Laplacian kernel is separable into two one-dimensional kernels. In principle, less computation would be needed. Aug 14 '16 at 9:02 • Could you test the performance of my function in your laptop? I cannot do that test because my laptop lacks enough memory. – xyz Aug 15 '16 at 13:31 • @J.M. Edited. Matrices are full of numeric real values (not sparse nor packed). Aug 15 '16 at 14:29 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 -> CompileGetElement // ReleaseHold; mat = RandomReal[10, {1000, 1000}]; Array[laplacian[mat] &, 10]; // AbsoluteTiming (* {0.447978, Null} *) Array[cLa[mat] &, 10]; // AbsoluteTiming (* {0.084375, Null} *)  • 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. Aug 14 '16 at 3:18 • @anderstood I'm on MMA 9.0.1, win10 64bit, with TDM-GCC 4.9.2, "SystemCompileOptions"->"-Ofast" Aug 14 '16 at 3:22 • Thank you, now I have the same timing, using << CompiledFunctionToolsCompiler$CCompilerOptions = {"SystemCompileOptions" -> "-fPIC -Ofast -march=native"}; Aug 14 '16 at 3:38

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[NDSolveFiniteDifferenceDerivative[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.