Earlier in the summer I had written the following for How to obtain adaptive sampling as in Plot function?. It is something like J. M.'s technique. But instead of a new version of Mathematica coming along, my brain turned on and I discovered a workaround using FunctionInterpolation
. I've been considering posting the following as an answer to that question, but perhaps it fits here better.
As I remarked in a comment to the above-mentioned question, how to adapt sampling depends on the goal. The goal is usually an approximation of some sort. It might be a curve, a function, an integral, and so forth. An approximation implies an error. The adaptation therefore should be made with a view to reducing the error estimation. There are different ways to look at error. The following subdivides each subinterval until the error estimate for each subinterval meets the goal specified by PrecisionGoal
and AccuracyGoal
.
The idea is fairly simple. An "active" subinterval has a special (undefined) head step
. At each recursive step, the head is replaced by split
, which decides whether the interval meets the error tolerance (... /; err[seg] < ag + pg Abs[fc]
) or should be split in two. (One could split into ten or some other number, I suppose, but splitting into two is easy.) Intervals with head step
keep getting split
until none are left, using ReplaceAll
like J. M.
Some remarks: (1) The use of Experimental`CreateNumericalFunction
was to handle WorkingPrecision
, as well as other potential alternatives I was exploring. It is not necessary here. It could be replaced by Function
, but since it was already in place, I left it. (2) I used Plot
to get an initial sampling. Plot
has been refined over the years, and I trust it. I added the initial point of the interval (Plot
samples very close to the initial point but not at the point), because the purpose was to approximate a function over the closed interval. (3) There is an option "ErrorNorm"
, with which an error measure can be passed to the function mesh
. The error norm will be passed a list of three points representing the interval with its midpoint.
ClearAll[mesh, mesh`imesh];
Options[mesh] = {PrecisionGoal -> 6, AccuracyGoal -> 6,
WorkingPrecision -> MachinePrecision, "InitialPoints" -> Automatic,
"ErrorNorm" -> Automatic};
mesh[f_, {var_, a_, b_}, opts : OptionsPattern[]] := mesh`imesh[f, {var, a, b}, opts];
Begin["mesh`"];
ClearAll[mesh`step, mesh`split, mesh`err, mesh`err0, mesh`imesh, mesh`initialpoints];
mesh`initialpoints = Flatten[{0.,
Cases[Plot[1, {x, 0, 1}, PlotPoints -> 11, MaxRecursion -> 0],
Line[p_] :> p[[All, 1]], Infinity]}];
Options[imesh] = Options[mesh];
(* default: overestimate Δf at midpoint from curve to chord *)
err0[{{_, fa_}, {_, fc_}, {_, fb_}}] := Abs[fc - (fa + fb)/2]/4.;
imesh[f_, {var_, a0_, b0_}, OptionsPattern[]] :=
Block[{step, split, err,
nf = Experimental`CreateNumericalFunction[{var}, f, {},
WorkingPrecision -> OptionValue@WorkingPrecision]},
err = OptionValue["ErrorNorm"] /. Automatic -> err0;
With[{pg = 10.^-OptionValue[PrecisionGoal],
ag = 10.^-OptionValue[AccuracyGoal]},
step[seg : {{a_, fa_}, {c_, fc_}, {b_, fb_}}] /; err[seg] < ag + pg Abs[fc] :=
Sequence[{a, fa}, {c, fc}];
step[{{a_, fa_}, {c_, fc_}, {b_, fb_}}] := Sequence[
split[{{a, fa}, {#, nf[{#}]} &[(a + c)/2], {c, fc}}],
split[{{c, fc}, {#, nf[{#}]} &[(b + c)/2], {b, fb}}]];
];
Append[#, {N[b0, OptionValue[WorkingPrecision]], nf[{b0}]}] &[
split[{{#1, nf[{#1}]}, {(#1 + #2)/2, nf[{(#1 + #2)/2}]}, {#2, nf[{#2}]}}] & @@@
Partition[
SetPrecision[OptionValue["InitialPoints"] /.
Automatic -> Rescale[initialpoints, {0, 1}, {a0, b0}],
OptionValue[WorkingPrecision]],
2, 1] //. split -> step]
];
End[];
Examples:
Linear interpolation.
The default method tries to make the error of a linear interpolation of the points meet the precision and accuracy goals (with a rather crude but fast error estimate).
mesh[x^2, {x, -1, 4}, PrecisionGoal -> 2, AccuracyGoal -> 2]
(*
{{-1., 1.}, {-1., 1.}, {-1., 0.999999}, {-0.759549, 0.576914},...,{3.74217,
14.0039}, {4., 16.}}
*)
Plot[x^2, {x, -1, 4},
Epilog -> {Red, Point[mesh[x^2, {x, -1, 4}, PrecisionGoal -> 2, AccuracyGoal -> 2]]}]

Subdivide according to the angle change.
In this case the angle is not a relative error, so the precision goal should be set to Infinity
; in such a case, AccuracyGoal
determines when subdivision halts. The VectorAngle
of the Differences
between the three consecutive points serves as the error function. An AccuracyGoal
of 1
corresponds to a bend of (at most) 0.1
radians or a little less than 6
degrees.
Plot[x^2, {x, -1, 4},
Epilog -> {Red,
Point[mesh[x^2, {x, -1, 4}, PrecisionGoal -> Infinity, AccuracyGoal -> 1,
"ErrorNorm" -> Function[x, Abs[VectorAngle @@ Differences[x]]]]]}]

Note: One difference (I think) between this and J. M.'s method, is that subdivision is done per subinterval: If $[a, b]$ and $[b,c]$ are divided at the midpoints into sequences {a,d,b}
and {b,e,c}
, the sequence {d,b,c}
will not be examined. This is similar in a way to the meshing of Plot3D
, ContourPlot
and DensityPlot
, but the real reason is that the algorithm was developed to minimize the error in approximating a function, not a curve. Whether a method is better often depends on what application one has in mind.
Mesh -> All
will show the points.MaxRecursion
controls the number of refinement steps.MaxRecursion -> 0
will uniformly samplePlotPoints
number of points. $\endgroup$Splines.m
are effectively the same as how the oldParametricPlot[]
worked. It's just a recursive improvement of an initial sample of points up untilMaxBend
orPlotDivisions
was satisfied. I actually refined that algorithm a bit, but it was about a decade ago, around version 5.1… $\endgroup$