2

Suppose we have an InterpolatingFunction from NDSolve

L = 5;

sol = First@NDSolve[{D[u[x, t], t] == D[u[x, t], {x, 2}], u[x, 0] == 0, 
      u[-5, t] == Sin[t], u[5, t] == 0}, u, {x, -L, L}, {t, 0, 10}]

Defining the functions:

int1st[x_?NumericQ, t_?NumericQ] := NIntegrate[D[Evaluate[u[s, t] /. sol], s]*Cot[(π (x - s))/(2*L)], {s, -L, x, L}, Method -> "PrincipalValue"];

intNest[x_?NumericQ, t_?NumericQ] := NIntegrate[D[Evaluate[u[xp, t] /. sol], xp]*int1st[xp, t]*Cot[(π (x - xp))/(2*L)], {xp, -L, x, L}, Method -> "PrincipalValue"];

which are, respectively, similar to the circular Hilbert transform of a periodic function and its nest (please see the section 'Hilbert transform on the circle' on that webpage). Note in intNest, int1st is called with xp as its argument, which acts as the variable of integration of intNest.

For example, plotting int1st at tn=5; I obtain the curve within 25s.

tn=5;
Plot[int1st[x,tn], {x, -L, L}, PlotRange -> {{-L, L}, All}, ImageSize -> 400, PlotPoints -> 60, AspectRatio -> 0.5, Frame -> True, Axes -> False, PlotStyle -> {Black, Thick}]

enter image description here

My problem is on intNest: when plotting it, there is no warning but my computer runs for hours without output. Is my definition of the intNest correct. If correct, how to make it more efficient?

Plot[intNest[x, tn], {x, -L, L}, PlotRange -> {{-L, L}, All}, ImageSize -> 400, PlotPoints -> 60, AspectRatio -> 0.5, Frame -> True, Axes -> False, PlotStyle -> {Black, Thick}]

After reading this post, I modified the function definitions:

ClearAll[int1st, intNest];

Block[{x, t}, int1st[x_?NumericQ, t_?NumericQ] = NIntegrate[D[u[s, t] /. sol, s]*Cot[(π (x - s))/(2*L)], {s, -L, x, L}, Method -> "PrincipalValue"]];

Block[{x, t}, intNest[x_?NumericQ, t_?NumericQ] = NIntegrate[D[u[xp, t] /. sol, xp]*int1st[xp, t]*Cot[(π (x - xp))/(2*L)], {xp, -L, x, L}, Method -> "PrincipalValue"]];

Findings: int1st can be plotted well in about 20s (same result as above), but it is still very slow to plot intNest.

Michael E2
  • 235,386
  • 17
  • 334
  • 747
lxy
  • 165
  • 5
  • 19

1 Answers1

4

Update: Used the built-in "InterpolationPointSubdivision" method.

Update 2: Memoization helps because the subdivision is at the same points (given by xcoords in the code below) every time. So in intNest2[], the call int1st2[xp, t] will initially be made at the same xp for each subinterval, unless the subinterval contains the singular point x. I say, "initially," because the recursive subdivision of each subinterval in the intNest2[] call may be different depending on the value of x and its effect on the complete integrand.

Some explanation in the comments. If I have more time, I might be able to add more:

L = 5;

(* OP's *)
ClearAll[int1st, intNest];
int1st[x_?NumericQ, t_?NumericQ] := 
  NIntegrate[
   Derivative[1, 0][u /. sol][s, t]*
    Cot[(π (x - s))/(2*L)], {s, -L, x, L}, 
   Method -> "PrincipalValue"];

intNest[x_?NumericQ, t_?NumericQ] := 
  NIntegrate[
   D[Evaluate[u[xp, t] /. sol], xp]*int1st[xp, t]*
    Cot[(π (x - xp))/(2*L)], {xp, -L, x, L}, 
   Method -> "PrincipalValue"];

(* with singularity removed from NIntegrate and 
 * manual interpolating nodes subdivision *)
ClearAll[int1st2, intNest2];

(* interpolating nodes *)
{xcoords, tcoords} = u["Coordinates"] /. sol;

mem : int1st2[x_?NumericQ, t_?NumericQ] :=
  mem = Block[{s}, (* memoization seems to help with Plot[] *)
    With[{
      du = Derivative[1, 0][u /. sol],
      dux = Derivative[1, 0][u /. sol][x, t]},
     NIntegrate[
       Piecewise[{{du[s, t]*Cot[(π (x - s))/(2*L)] - 
           dux (2*L)/(π (x - s)), x != s}}], (* subtract singular part *)
       {s, -L, x, L},
       Method -> {"InterpolationPointsSubdivision", (* divide interval at nodes *)
         "SymbolicProcessing" -> 0},
       PrecisionGoal -> 4 (* PDE solution is low-precision *)
       ] + dux ( (2*L) Log[(L + x)/(L - x)])/π (* add PV integral of singular part *)
     ]];

intNest2[x_?NumericQ, t_?NumericQ] := 
  Block[{xp}, 
   With[{
     du = Derivative[1, 0][u /. sol], 
     dux = Derivative[1, 0][u /. sol][x, t]*int1st2[x, t]}, 
    NIntegrate[
      Piecewise[{{du[xp, t]*int1st2[xp, t]*
           Cot[(π (x - xp))/(2*L)] - dux (2*L)/(π (x - xp)), 
         x != xp}}], {xp, -L, x, L}, 
      Method -> {"InterpolationPointsSubdivision", 
        "SymbolicProcessing" -> 0}, PrecisionGoal -> 4] + 
     dux*((2*L) Log[(L + x)/(L - x)])/π]];

Single-call timings:

int1st2[4, 5] // Quiet // AbsoluteTiming
int1st[4, 5] // AbsoluteTiming
(*
  {0.017778, -1.6291}
  {0.056669, -1.6291}
*)

intNest2[4, 5] // AbsoluteTiming
intNest[4, 5] // AbsoluteTiming
(*
  {5.96797, 1.78776}
  {52.9943, 1.78775}
*)
Plot[int1st2[x, 5], {x, -L, L}, PlotRange -> {{-L, L}, All}, 
  ImageSize -> 400, PlotPoints -> 60, AspectRatio -> 0.5, 
  Frame -> True, Axes -> False, 
  PlotStyle -> {Black, Thick}] // AbsoluteTiming

enter image description here

There are 441 points here, so multiply by the single call timings to get an estimate of the time to plot:

Cases[%, Line[p_] :> Length@p, Infinity]
(*  {441}  *)

The nested integral is still going to be slow. You can use options like PlotPoints and MaxRecursion to reduce the number of points plotted (that is, reduce the number of function calls).

AsukaMinato
  • 9,758
  • 1
  • 14
  • 40
Michael E2
  • 235,386
  • 17
  • 334
  • 747
  • Nice! Could you memoize int1st2 to take advantage of the recycled points? – Chris K Feb 15 '20 at 00:50
  • @ChrisK Memoization doesn't help with intNest[4, 5]. Rather doubt it would help with Plot, either. There aren't many integration rules that recycle points. But if the same call to intNest needs to be done multiple times, then obviously memoization would speed things up here. – Michael E2 Feb 15 '20 at 03:21
  • I should have used “InterpolationPointsSubdicision”. Will update when I can log into the site (tomorrow). The rest doesn’t matter that much. Some of it is typos. (As I implied, I didn’t have much time. ) – Michael E2 Feb 15 '20 at 04:10
  • @MichaelE2 I haven't looking into why, but memoizing with int1st2[x_?NumericQ, t_?NumericQ] := int1st2[x, t] = allows Plot[intNest2[x, 5], ..., PlotPoints -> 5] to run in four minutes, whereas without memoizing it doesn't complete in at least fifteen minutes. – Chris K Feb 15 '20 at 14:55
  • @Chris K, did you mean f[x_?NumericQ, t_?NumericQ] := f[x, t] =... can memorize all the data of f[x,t] even though it is a function of x and t? – lxy Feb 15 '20 at 15:18
  • @jsxs Whenever f[x, t] is called for a particular x and t, the value is stored for future use, called memoization (yeah, weird name). See my most popular question ever for more info. – Chris K Feb 15 '20 at 15:24
  • @ChrisK OK, I guess I did something wrong in testing. Even the single calls are faster this morning. – Michael E2 Feb 15 '20 at 16:01
  • @ChrisK I think I understand why memoization helps, and why it doesn't help much on the initial call to intNest2[]. The key is explained in "Update 2." – Michael E2 Feb 15 '20 at 16:40
  • @MichaelE2 Yeah that's what I suspected. Thanks for the update! – Chris K Feb 15 '20 at 17:44
  • @MichaelE2 when I try the update code to plot intNest2, I got the warnings:>> Nonatomic expression expected at position 1 in Total[...] and obtained an empty plot. – lxy Feb 16 '20 at 10:39
  • @jsxs I get no such error, but the Total[] was an unnecessary relic, so I removed it. Perhaps you have a latent definition, which is a common reason one user gets an error and another does not. Restarting the kernel usually fixes such a problem. I doubt it's a version issue, but I'm using 12.0.0. – Michael E2 Feb 16 '20 at 12:46
  • @MichaelE2, now it works. Thanks. Now I understand +dux ((2*L) Log[(L + x)/(L - x)])/\[Pi] adds the PV integral of the subtracted part. Just one last question: could u explain why, e.g. by -dux (2*L)/(\[Pi] (x - s)) we subtract the singularity? – lxy Feb 16 '20 at 15:39
  • @jsxs The function $f(t)\cot at$ has a Laurent series expansion ${f(0) \over a t} + {f'(0)\over a}+\cdots$, so $-{f(0)\over at} + f(t) \cot at$ has no singularity at $t=0$. So $f$ corresponds to du, $f(0)$ to dux, $a$ to -2 Pi/L, and $t$ to s - x (s is the independent variable). I guess the Piecewise function should have {f'(0)\over a} as the default instead of zero. It's just a single point, but in a discrete algorithm, it might be an unlikely bug waiting to happen; with s == x (that is, $t=0$) specified as a singularity, it should be ok, though. – Michael E2 Feb 17 '20 at 13:47
  • @MichaelE2 thank you. but the add PV integral of singular part seems to be wrong. $\int_{-L}^L f(0)/(at)ds=(-2L/\pi)f(0) \int_{-L}^L d(x-s)/(x-s)=f(0)(2L/\pi) \ln[(x+L)/(x-L)]$ instead of $f(0)(2L/\pi) \ln[(L + x)/(L - x)]$. Please modifiy when you confirm and have time. – lxy Feb 24 '20 at 05:41
  • @MichaelE2 The the interpolating nodes, obtained with {xcoords, tcoords} = u["Coordinates"] /. sol;, are not used in the subsequent computation. What is it use? – lxy Mar 10 '20 at 06:40