From 646989dee24e420c032978ba9f940fc84a9715e8 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Thu, 1 Mar 2012 21:29:08 -0500 Subject: [PATCH 1/7] Added fplot to Axes subclass. Plotting functionality for Python callables, ala Matlab's 'fplot' function. An adaptive step-size is used. Smaller step sizes are used when the gradient of the function is larger. Only one callable is supported so far, it would be desirable to support an array/list of callables and plot them all simultaneously. There are some unresolved problems. One is the inability to deal with the scenario when f(x) is undefined for some x. --- lib/matplotlib/axes.py | 4 ++ lib/matplotlib/fplot.py | 110 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 lib/matplotlib/fplot.py diff --git a/lib/matplotlib/axes.py b/lib/matplotlib/axes.py index a5418abbff90..9852beb1279c 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -19,6 +19,7 @@ import matplotlib.dates as mdates from matplotlib import docstring import matplotlib.font_manager as font_manager +import matplotlib.fplot as mfplot import matplotlib.image as mimage import matplotlib.legend as mlegend import matplotlib.lines as mlines @@ -6382,6 +6383,9 @@ def quiver(self, *args, **kw): return q quiver.__doc__ = mquiver.Quiver.quiver_doc + def fplot(self, f, limits, *args, **kwargs): + return mfplot.fplot(self, f, limits, *args, **kwargs) + def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, cmap=None, arrowsize=1, arrowstyle='-|>', minlength=0.1): if not self._hold: self.cla() diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py new file mode 100644 index 000000000000..c53b8a41e3c1 --- /dev/null +++ b/lib/matplotlib/fplot.py @@ -0,0 +1,110 @@ +""" +1D Callable function plotting. + +""" +import numpy as np +import matplotlib + + +__all__ = ['fplot'] + + +def fplot(axes, f, limits, *args, **kwargs): + """Plots a callable function f. + + Parameters + ---------- + *f* : Python callable, the function that is to be plotted. + *limits* : 2-element array or list of limits: [xmin, xmax]. The function f + is to to be plotted between xmin and xmax. + + Returns + ------- + *lines* : `matplotlib.collections.LineCollection` + Line collection with that describes the function *f* between xmin + and xmax. all streamlines as a series of line segments. + """ + + # TODO: Check f is callable. If not callable, support array of callables. + # TODO: Support y limits? + + # Some small number, usually close to machine epsilon + eps = 1e-10 + + tol = kwargs.pop('tol', None) + n = kwargs.pop('tol', None) + + if tol is None: + # 0.2% absolute error + tol = 2e-3 + + if n is None: + n = 50 + x = np.linspace(limits[0], limits[1], n) + + # Bisect abscissa until the gradient error changes by less than tol + within_tol = False + + while not within_tol: + within_tol = True + new_pts = [] + for i in xrange(len(x)-1): + # Make sure the step size is not pointlessly small. + # This is a numerical check to prevent silly roundoff errors. + # + # The temporary variable is to ensure the step size is + # represented properly in binary. + min_step = np.sqrt(eps) * x[i] + tmp = x[i] + min_step + + # The multiplation by two is just to be conservative. + min_step = 2*(tmp - x[i]) + + # Subdivide + x_new = (x[i+1] + x[i]) / 2.0 + + # If the absicissa points are too close, don't bisect + # since calculation of the gradient will produce mostly + # nonsense values due to roundoff error. + # + # If the function values are too close, the payoff is + # negligible, so skip them. + if np.abs(x_new - x[i]) < min_step or np.abs(f(x_new) - f(x[i])) < min_step: + continue + + # Compute gradient + # FIXME: What if f(x[i]) is nan? + grad = (f(x[i+1]) - f(x[i])) / (x[i+1] - x[i]) + + # Compute gradients to the left and right of x_new + grad_right = (f(x[i+1]) - f(x_new)) / (x[i+1] - x_new) + grad_left = (f(x_new) - f(x[i])) / (x_new - x[i]) + + # If the new gradients are not within the tolerance, store + # the subdivision point for merging later + if np.abs(grad_right - grad) > tol or np.abs(grad_left - grad) > tol: + within_tol = False + new_pts.append(x_new) + + if not within_tol: + # Not sure this is the best way to do this... + # Merge the subdivision points into the array of abscissae + x = merge_pts(x, new_pts) + + return axes.plot(x, f(x)) + +def merge_pts(a, b): + x = [] + ia = 0 + ib = 0 + while ib < len(b): + if b[ib] < a[ia]: + x.append(b[ib]) + ib += 1 + else: + x.append(a[ia]) + ia += 1 + if ia < len(a): + return np.append(x, a[ia::]) + else: + return np.array(x) From a04f5811ebd9f20d7d64e74c02c3ae051a385ca8 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Sat, 17 Mar 2012 17:37:31 -0400 Subject: [PATCH 2/7] Added docstring to Axes.fplot --- lib/matplotlib/axes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/axes.py b/lib/matplotlib/axes.py index 9852beb1279c..4c6c263ccd2f 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -6385,6 +6385,7 @@ def quiver(self, *args, **kw): def fplot(self, f, limits, *args, **kwargs): return mfplot.fplot(self, f, limits, *args, **kwargs) + fplot.__doc__ = mfplot.fplot.__doc__ def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, cmap=None, arrowsize=1, arrowstyle='-|>', minlength=0.1): From d0b01d55596679cef4f5bbfbe1e718dc35a9cce7 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Thu, 22 Mar 2012 19:40:07 -0400 Subject: [PATCH 3/7] Scaled minimum step size with domain length. Reduces the amount of unnecessary computation by ignoring partition sizes that the user won't see with longer domains. --- lib/matplotlib/fplot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py index c53b8a41e3c1..9ba433b04af0 100644 --- a/lib/matplotlib/fplot.py +++ b/lib/matplotlib/fplot.py @@ -31,6 +31,10 @@ def fplot(axes, f, limits, *args, **kwargs): # Some small number, usually close to machine epsilon eps = 1e-10 + # The scaling factor used to scale the step size + # as a function of the domain length + scale = max(1.0, abs(limits[1] - limits[0])) + tol = kwargs.pop('tol', None) n = kwargs.pop('tol', None) @@ -54,7 +58,8 @@ def fplot(axes, f, limits, *args, **kwargs): # # The temporary variable is to ensure the step size is # represented properly in binary. - min_step = np.sqrt(eps) * x[i] + min_step = np.sqrt(eps) * x[i] * scale + tmp = x[i] + min_step # The multiplation by two is just to be conservative. From c6c8b3130e3bed7e6280f53001a42c9c185034d5 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Wed, 28 Mar 2012 18:34:25 -0400 Subject: [PATCH 4/7] Small changes to the algorithm to reduce run time. The old method computed 'left' and 'right' gradients. These are the same for a linear approximation, which is what Line2D is anyway. I have also stored some function values that are used later in the algorithm. This is good if f is expensive to compute. The above changes to reduce computation time by 20%. --- lib/matplotlib/fplot.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py index 9ba433b04af0..8cfe3dea7cb8 100644 --- a/lib/matplotlib/fplot.py +++ b/lib/matplotlib/fplot.py @@ -74,20 +74,22 @@ def fplot(axes, f, limits, *args, **kwargs): # # If the function values are too close, the payoff is # negligible, so skip them. - if np.abs(x_new - x[i]) < min_step or np.abs(f(x_new) - f(x[i])) < min_step: + f_new = f(x_new) # Used later, so store it + f_i = f(x[i]) # Used later, so store it + if abs(x_new - x[i]) < min_step or abs(f_new - f_i) < min_step: continue - # Compute gradient - # FIXME: What if f(x[i]) is nan? - grad = (f(x[i+1]) - f(x[i])) / (x[i+1] - x[i]) + # Compare gradients of actual f and linear approximation + # FIXME: What if f(x[i]) or f(x[i+1]) is nan? + dx = abs(x[i+1] - x[i]) + f_interp = (f(x[i+1]) + f_i) - # Compute gradients to the left and right of x_new - grad_right = (f(x[i+1]) - f(x_new)) / (x[i+1] - x_new) - grad_left = (f(x_new) - f(x[i])) / (x_new - x[i]) + # This line is the absolute error of the gradient + grad_error = np.abs(f_interp - 2.0 * f_new) / dx - # If the new gradients are not within the tolerance, store + # If the new gradient is not within the tolerance, store # the subdivision point for merging later - if np.abs(grad_right - grad) > tol or np.abs(grad_left - grad) > tol: + if grad_error > tol: within_tol = False new_pts.append(x_new) From f04c2ab6c4430fb9f375c8080638b60b2897cbfd Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Wed, 28 Mar 2012 19:23:45 -0400 Subject: [PATCH 5/7] Updated kwargs popping. --- lib/matplotlib/fplot.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py index 8cfe3dea7cb8..247bb7cb0469 100644 --- a/lib/matplotlib/fplot.py +++ b/lib/matplotlib/fplot.py @@ -35,15 +35,10 @@ def fplot(axes, f, limits, *args, **kwargs): # as a function of the domain length scale = max(1.0, abs(limits[1] - limits[0])) - tol = kwargs.pop('tol', None) - n = kwargs.pop('tol', None) + # 0.2% absolute error + tol = kwargs.pop('tol', 2e-3) + n = kwargs.pop('tol', 50) - if tol is None: - # 0.2% absolute error - tol = 2e-3 - - if n is None: - n = 50 x = np.linspace(limits[0], limits[1], n) # Bisect abscissa until the gradient error changes by less than tol From 04bb1a0d233e1a6fd83393d2abd5b3e3a03ca61f Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Thu, 29 Mar 2012 14:15:57 -0400 Subject: [PATCH 6/7] Speed up computation when f is expensive. Now we call axes.plot with stored values of f instead of calling f on x. This will improve performance when f is an expensive function. --- lib/matplotlib/fplot.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py index 247bb7cb0469..6096610ad7bf 100644 --- a/lib/matplotlib/fplot.py +++ b/lib/matplotlib/fplot.py @@ -40,6 +40,7 @@ def fplot(axes, f, limits, *args, **kwargs): n = kwargs.pop('tol', 50) x = np.linspace(limits[0], limits[1], n) + f_vals = [f(xi) for xi in x] # Bisect abscissa until the gradient error changes by less than tol within_tol = False @@ -47,6 +48,7 @@ def fplot(axes, f, limits, *args, **kwargs): while not within_tol: within_tol = True new_pts = [] + new_f = [] for i in xrange(len(x)-1): # Make sure the step size is not pointlessly small. # This is a numerical check to prevent silly roundoff errors. @@ -70,14 +72,13 @@ def fplot(axes, f, limits, *args, **kwargs): # If the function values are too close, the payoff is # negligible, so skip them. f_new = f(x_new) # Used later, so store it - f_i = f(x[i]) # Used later, so store it - if abs(x_new - x[i]) < min_step or abs(f_new - f_i) < min_step: + if abs(x_new - x[i]) < min_step or abs(f_new - f_vals[i]) < min_step: continue # Compare gradients of actual f and linear approximation # FIXME: What if f(x[i]) or f(x[i+1]) is nan? dx = abs(x[i+1] - x[i]) - f_interp = (f(x[i+1]) + f_i) + f_interp = (f_vals[i+1] + f_vals[i]) # This line is the absolute error of the gradient grad_error = np.abs(f_interp - 2.0 * f_new) / dx @@ -87,26 +88,30 @@ def fplot(axes, f, limits, *args, **kwargs): if grad_error > tol: within_tol = False new_pts.append(x_new) + new_f.append(f_new) if not within_tol: # Not sure this is the best way to do this... # Merge the subdivision points into the array of abscissae - x = merge_pts(x, new_pts) + x, f_vals = merge_pts(x, new_pts, f_vals, new_f) - return axes.plot(x, f(x)) + return axes.plot(x, f_vals) -def merge_pts(a, b): +def merge_pts(xs, xs_sub, fs, fs_sub): x = [] + f = [] ia = 0 ib = 0 - while ib < len(b): - if b[ib] < a[ia]: - x.append(b[ib]) + while ib < len(xs_sub): + if xs_sub[ib] < xs[ia]: + x.append(xs_sub[ib]) + f.append(fs_sub[ib]) ib += 1 else: - x.append(a[ia]) + x.append(xs[ia]) + f.append(fs[ia]) ia += 1 - if ia < len(a): - return np.append(x, a[ia::]) + if ia < len(xs): + return np.append(x, xs[ia::]), np.append(f, fs[ia::]) else: - return np.array(x) + return np.array(x), np.array(f) From 90af9c94005fd67bd063ff9544fcdcbf7b8ae4b3 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Thu, 29 Mar 2012 14:27:04 -0400 Subject: [PATCH 7/7] Account for singularities in f. When we find singularities, replace the function values with NaN so they appear as separate Line2Ds on the plot --- lib/matplotlib/fplot.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py index 6096610ad7bf..10fda504fd9f 100644 --- a/lib/matplotlib/fplot.py +++ b/lib/matplotlib/fplot.py @@ -31,6 +31,9 @@ def fplot(axes, f, limits, *args, **kwargs): # Some small number, usually close to machine epsilon eps = 1e-10 + # If the gradient is bigger than this, we say it has a singularity + sing_tol = 1e6 + # The scaling factor used to scale the step size # as a function of the domain length scale = max(1.0, abs(limits[1] - limits[0])) @@ -50,6 +53,8 @@ def fplot(axes, f, limits, *args, **kwargs): new_pts = [] new_f = [] for i in xrange(len(x)-1): + if np.isnan(f_vals[i]): + continue # Make sure the step size is not pointlessly small. # This is a numerical check to prevent silly roundoff errors. # @@ -85,7 +90,10 @@ def fplot(axes, f, limits, *args, **kwargs): # If the new gradient is not within the tolerance, store # the subdivision point for merging later - if grad_error > tol: + if grad_error > sing_tol: + f_vals[i] = np.nan + f_vals[i+1] = np.nan + elif grad_error > tol: within_tol = False new_pts.append(x_new) new_f.append(f_new)