11
$\begingroup$

Background: I was trying to convert a Matlab code (fluid simulation, SPH method) into a Mathematica one, but the speed difference is huge.

Matlab code:

function s = initializeDensity2(s)
nTotal   = s.params.nTotal;  %# particles
h    = s.params.h;
h2Sq = (2*h)^2;
for ind1 = 1:nTotal  %loop over all receiving particles; one at a time
%particle i is the receiving particle; the host particle
%particle j is the sending particle
xi = s.particles.pos(ind1,1);
yi = s.particles.pos(ind1,2);
xj = s.particles.pos(:,1); %all others
yj = s.particles.pos(:,2); %all others
mj = s.particles.mass; %all others
rSq = (xi-xj).^2+(yi-yj).^2;
%Boolean mask returns values where r^2 < (2h)^2
mask1 = rSq<h2Sq;
rSq   = rSq(mask1);
mTemp = mj(mask1);
densityTemp = mTemp.*liuQuartic(sqrt(rSq),h);
s.particles.density(ind1) = sum(densityTemp);
end

And the corresponding Mathematica code:

Needs["HierarchicalClustering`"]
computeDistance[pos_] := 
DistanceMatrix[pos, DistanceFunction -> EuclideanDistance];
initializeDensity[distance_] := 
uniMass*Total/@(liuQuartic[#,h]&/@Pick[distance,Boole[Map[#<2h&,distance,{2}]],1])
initializeDensity[computeDistance[totalPos]]

The data are coordinates of 1119 points, in the form of {{x1,y1},{x2,y2}...}, stored in s.particles.pos and totalPos respectively. And liuQuartic is just a polynomial function. The complete Matlab code is way more than this, but it can run about 160 complete time steps in 60 seconds, whereas the Mathematica code listed above alone takes about 3 seconds to run. I don't know why there is such huge speed difference. Any thoughts is appreciated. Thanks.

Edit:

The liuQuartic is defined as

liuQuartic[r_,h_]:=15/(7Pi*h^2) (2/3-(9r^2)/(8h^2)+(19r^3)/(24h^3)-(5r^4)/(32h^4))

and example data can be obtained by

h=2*10^-3;conWidth=0.4;conHeight=0.16;totalStep=6000;uniDensity=1000;uniMass=1000*Pi*h^2;refDensity=1400;gamma=7;vf=0.07;eta=0.01;cs=vf/eta;B=refDensity*cs^2/gamma;gravity=-9.8;mu=0.02;beta=0.15;dt=0.00005;epsilon=0.5;

iniFreePts=Block[{},Table[{-conWidth/3+i,1.95h+j},{i,10h,conWidth/3-2h,1.5h},{j,0,0.05,1.5h}]//Flatten[#,1]&];
leftWallIniPts=Block[{x,y},y=Table[i,{i,conHeight/2-0.5h,0.2h,-0.5h}];x=ConstantArray[-conWidth/3,Length[y]];Thread[List[x,y]]];
botWallIniPts=Block[{x,y},x=Table[i,{i,-conWidth/3,-0.4h,h}];y=ConstantArray[0,Length[x]];Thread[List[x,y]]];
incWallIniPts=Block[{x,y},Table[{i,0.2125i},{i,0,(2conWidth)/3,h}]];
rightWallIniPts=Block[{x,y},y=Table[i,{i,Last[incWallIniPts][[2]]+h,conHeight/2,h}];x=ConstantArray[Last[incWallIniPts][[1]],Length[y]];Thread[List[x,y]]];
topWallIniPts=Block[{x,y},x=Table[i,{i,-conWidth/3+0.7h,(2conWidth)/3-0.7h,h}];y=ConstantArray[conHeight/2,Length[x]];Thread[List[x,y]]];
freePos = iniFreePts;
wallPos = leftWallIniPts~Join~botWallIniPts~Join~incWallIniPts~Join~rightWallIniPts~Join~topWallIniPts;
totalPos = freePos~Join~wallPos;

where conWidth=0.4, conHeight=0.16 and h=0.002

$\endgroup$
  • $\begingroup$ @xzczd I have edited the liuQuartic function so that you can directly copy and paste it into Mathematica $\endgroup$ – zjx1805 Feb 15 '15 at 16:33
19
$\begingroup$

Modify the calculation order a little to avoid ragged array and then make use of Listable and Compile:

computeDistance[pos_] := DistanceMatrix[pos, DistanceFunction -> EuclideanDistance]
liuQuartic = {r, h} \[Function] 
   15/(7 Pi*h^2) (2/3 - (9 r^2)/(8 h^2) + (19 r^3)/(24 h^3) - (5 r^4)/(32 h^4));
initializeDensity = 
  With[{l = liuQuartic, m = uniMass}, 
   Compile[{{d, _Real, 2}, {h, _Real}}, m Total@Transpose[l[d, h] UnitStep[2 h - d]]]];
new = initializeDensity[computeDistance[N@totalPos], h]; // AbsoluteTiming

Tested with your new added sample data, my code ran for 0.390000 s while the original code ran for 4.851600 s and ybeltukov's code ran for 0.813200 s on my machine.

If you have a C compiler installed, the following code

computeDistance[pos_] := DistanceMatrix[pos, DistanceFunction -> EuclideanDistance]
liuQuartic = {r, h} \[Function] 
   15/(7 Pi*h^2) (2/3 - (9 r^2)/(8 h^2) + (19 r^3)/(24 h^3) - (5 r^4)/(32 h^4));
initializeDensity = 
  With[{l = liuQuartic, m = uniMass, g = Compile`GetElement}, 
   Compile[{{d, _Real, 2}, {h, _Real}}, 
    Module[{b1, b2}, {b1, b2} = Dimensions@d; 
     m Table[Sum[If[2 h > g[d, i, j], l[g[d, i, j], h], 0.], {j, b2}], {i, b1}]], 
      CompilationTarget -> "C", RuntimeOptions -> "Speed"]];

will give you a 2X speedup once again. Notice the C compiler is necessary, see this post for some more details.

$\endgroup$
  • $\begingroup$ Thank you for your advise. I've never thought that UnitStep could be used as a Boolean mask to a matrix. $\endgroup$ – zjx1805 Feb 15 '15 at 17:34
  • $\begingroup$ Wow, that's wired. The original code ran for 2.47 s and ybeltukov's code ran for just 0.2 s on my computer, with data packed in both cases. $\endgroup$ – zjx1805 Feb 15 '15 at 17:45
  • $\begingroup$ @zjx1805 Then what's the timing for my code? $\endgroup$ – xzczd Feb 15 '15 at 17:49
  • $\begingroup$ It's 0.12 seconds. For now the speedup is pretty satisfactory to me but there is more for me to improve since Matlab can run the complete code at about 0.35 sec/step (even including plotting of data points and exporting of the images) and the amount of computation required for one step for the complete code is at least 3 times larger than the code listed here. I was expecting Mathematica could outperform Matlab. $\endgroup$ – zjx1805 Feb 15 '15 at 17:54
  • $\begingroup$ @zjx1805 I believe you'll manage to improve the complete code if you fully understand the answers you got. BTW, See my edit for another 2X speedup. $\endgroup$ – xzczd Feb 16 '15 at 4:06
19
$\begingroup$

You miss that many Mathematica functions are Listable. It allows you to write a fast and clear code

init2[distance_] := uniMass Total[liuQuartic[distance, h] UnitStep[2 h - distance], {2}]

h = 0.1;
uniMass = 1.0;
liuQuartic[d_, h_] := d^2 - h^2;
totalPos = RandomReal[1, {1119, 2}];
res1 = initializeDensity@computeDistance[totalPos]; // AbsoluteTiming
res2 = init2@computeDistance[totalPos]; // AbsoluteTiming
res1 == res2
(* {3.088372, Null} *)
(* {0.130059, Null} *)
(* True *)

It seems that this code is faster than MATLAB.

$\endgroup$
  • $\begingroup$ Weirdly the init2 function takes 9.49 seconds to run on my computer and I don't know why. $\endgroup$ – zjx1805 Feb 15 '15 at 16:22
  • $\begingroup$ I ran your code, but the new version takes 9.49 seconds to run and it's even slower than the original one. $\endgroup$ – zjx1805 Feb 15 '15 at 16:30
  • $\begingroup$ @zjx1805 Did you try it with fresh kernel? Which version do you use? $\endgroup$ – ybeltukov Feb 15 '15 at 16:39
  • $\begingroup$ Yes, I actually tried with fresh kernel. I found that when I replaced your version of totalPos with my own version (I listed the way to generate it in the latest edit), the speed significantly reduces, from 0.2 seconds to 4.5 seconds. $\endgroup$ – zjx1805 Feb 15 '15 at 17:01
  • 5
    $\begingroup$ @zjx1805 Probably your data is not packed. Try to apply Developer`ToPackedArray@N[...] to your data. $\endgroup$ – ybeltukov Feb 15 '15 at 17:09
6
$\begingroup$

Here a slightly improved version of xzczd's code (I call the function cinitializeDensity in the following) that does not require computing the DistanceMatrix beforehand. Moreover, I tried to suppress some type casts within the CompiledFunction and to exploit parallelization.

Block[{r, h},
  cinitializeDensity2 = 
   With[{code = N[liuQuartic[Sqrt[r], h]], m = N[uniMass], g = Compile`GetElement}, 
    Compile[{{x, _Real, 1}, {y, _Real, 2}, {h, _Real}},
     Block[{r, sum = 0., x1, x2},
      x1 = g[x, 1];
      x2 = g[x, 2];
      Do[
       r = (x1 - g[y, j, 1])^2 + (x2 - g[y, j, 2])^2;
       sum += If[r < 4. h^2, code, 0.],
       {j, 1, Length[y]}];
      m sum
      ],
     CompilationTarget -> "C",
     Parallelization -> True,
     RuntimeOptions -> "Speed",
     RuntimeAttributes -> {Listable}
     ]]
  ];

Along with packing, this leads to further speedup:

ptotalPos = Developer`ToPackedArray[N[totalPos]];
a = initializeDensity[computeDistance[ptotalPos]]; // AbsoluteTiming // First
b = cinitializeDensity[computeDistance[ptotalPos], h]; // AbsoluteTiming // First
c = cinitializeDensity2[ptotalPos, ptotalPos, h]; // AbsoluteTiming // First
a == b == c

1.28708

0.006039

0.000844

True

For even longer list, it might be worthwhile to delegate the distance checks to Nearest:

Block[{r, h},
  cinitializeDensity3 = 
   With[{code = N[liuQuartic[Sqrt[r], h]], m = N[uniMass], 
     g = Compile`GetElement}, 
    Compile[{{x, _Real, 1}, {y, _Real, 2}, {h, _Real}},
     Block[{r, sum = 0., x1, x2},
      x1 = g[x, 1];
      x2 = g[x, 2];
      Do[
       r = (x1 - g[y, j, 1])^2 + (x2 - g[y, j, 2])^2;
       sum += code,
       {j, 1, Length[y]}];
      m sum
      ],
     CompilationTarget -> "C",
     Parallelization -> True,
     RuntimeOptions -> "Speed",
     RuntimeAttributes -> {Listable}
     ]]
  ];

d = cinitializeDensity3[ptotalPos, Nearest[ptotalPos, ptotalPos, {∞, 2 h}], h]; // AbsoluteTiming // First
a == d

0.00126

True

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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