aboutsummaryrefslogtreecommitdiff
path: root/vquad/benchmark.py
diff options
context:
space:
mode:
authorChristoph Groth <christoph.groth@cea.fr>2018-06-05 11:55:07 +0200
committerChristoph Groth <christoph.groth@cea.fr>2018-12-20 14:52:49 +0100
commit2c5446877794b7a900bb97f084f837253d78417a (patch)
tree57a0819d6f6ca8398fdd7fae832e1439349767d8 /vquad/benchmark.py
parent2a35d8d25092cd402c6db518474c3760ed10f285 (diff)
Lyness-Kaganove benchmark
Diffstat (limited to 'vquad/benchmark.py')
-rw-r--r--vquad/benchmark.py289
1 files changed, 289 insertions, 0 deletions
diff --git a/vquad/benchmark.py b/vquad/benchmark.py
new file mode 100644
index 0000000..1feb3d8
--- /dev/null
+++ b/vquad/benchmark.py
@@ -0,0 +1,289 @@
+# Copyright 2018 Christoph Groth (CEA).
+#
+# This file is part of Vquad. It is subject to the license terms in the file
+# LICENSE.rst found in the top-level directory of this distribution.
+
+import math
+import itertools
+import argparse
+import pickle
+from concurrent.futures import ProcessPoolExecutor
+
+import numpy as np
+import scipy.integrate
+
+import matplotlib, matplotlib.cm
+from matplotlib import pyplot
+
+from . import core
+
+
+def f63(x, α, λ):
+ return np.abs(x - λ)**α
+
+def F63_0_1(α, λ):
+ def F(x):
+ return (x - λ) * abs(x - λ)**α / (α + 1)
+ return F(1) - F(0)
+
+
+def f64(x, α, λ):
+ return (x > λ) * np.exp(α * x)
+
+def F64_0_1(α, λ):
+ assert 0 <= λ <= 1
+ if α == 0:
+ return 1 - λ
+ return (math.exp(α) - math.exp(α * λ)) / α
+
+
+def f65(x, α, λ):
+ return np.exp(-α * np.abs(x - λ))
+
+def F65_0_1(α, λ):
+ assert 0 <= λ <= 1
+ if α == 0:
+ return 1
+ return (math.expm1(α*λ - α) + math.expm1(-α*λ)) / -α
+
+
+# As shown in Pedro's thesis (Eq. (6.6) and Eq. (6.7)), the term 10^α is not
+# squared in the denominator. However, this is the correct version of the
+# integrand that is consistent with the benchmark results that are shown in the
+# thesis.
+def f67(x, α, *λs):
+ t = 10**α
+ return t * sum(1 / ((x - λ)**2 + t**2) for λ in λs)
+
+def F67_1_2(α, *λs):
+ t = 10**α
+ return sum(math.atan((2 - λ) / t) - math.atan((1 - λ) / t) for λ in λs)
+
+
+# Equation (6.8) is the only one where the benchmark results do not agree with
+# the ones listed in Pedro Gonnet's thesis. So Eq. (6.8) seems to contain a
+# typo. I have modified the function by adding a small constant so that the
+# definite integral is never zero. Otherwise any required relative error
+# cannot be obtained which leads to extremely long running times.
+def f68(x, α, λ):
+ β = 10**α / max(λ**2, (1 - λ)**2)
+ return 2 + 2 * β * (x - λ) * np.cos(β * (x - λ)**2)
+
+def F68_0_1(α, λ):
+ β = 10**α / max(λ**2, (1 - λ)**2)
+ return 2 + math.sin(β * ((1 - λ)**2)) - math.sin(β * λ**2)
+
+
+families = [
+ (r"|x - \lambda|^\alpha",
+ (f63, 0, 1), F63_0_1, (-0.5, 0), (0, 1)),
+
+ (r"(x - \lambda)\mathrm{e}^{\alpha x}",
+ (f64, 0, 1), F64_0_1, (0, 1), (0, 1)),
+
+ (r"\exp(-\alpha|x - \lambda|)",
+ (f65, 0, 1), F65_0_1, (0, 4), (0, 1)),
+
+ (r"10^\alpha/((x - \lambda)^2 + 10^{2\alpha})",
+ (f67, 1, 2), F67_1_2, (-6, -3), (1, 2)),
+
+ (r"\sum_i 10^\alpha/((x - \lambda_i)^2 + 10^{2\alpha})",
+ (f67, 1, 2), F67_1_2, (-5, -3)) + ((1, 2),) * 4,
+
+ (r"2 + 2\beta(x - \lambda) \cos(\beta (x - \lambda)^2),\quad"
+ r"\beta = 10^\alpha / max(\lambda^2, (1 - \lambda)^2)",
+ (f68, 0, 1), F68_0_1, (1.8, 2), (0, 1)),
+]
+
+
+def lk_job_vquad(seed, family, rtols):
+ """Run a single integration for a sequence of requested relative
+ tolerances. It's as if a len(rtols) integration were started,
+ but actually a single integrator is launched and the requested
+ tolerance is improved iteratively.
+ """
+ def ff(x):
+ nonlocal neval
+ neval += len(x)
+ return f(x, *args)
+
+ _, (f, a, b), F, *bounds = family
+ rng = np.random.RandomState(seed)
+ args = [rng.uniform(*b) for b in bounds]
+
+ relerrs = np.empty(len(rtols), float)
+ nevals = np.empty(len(rtols), int)
+
+ neval = 0
+ exact = F(*args)
+ vquad = core.Vquad(ff, a, b)
+ for j, rtol in enumerate(rtols):
+ igral, _ = vquad.improve_until(rtol)
+ relerrs[j] = abs((igral - exact) / exact)
+ nevals[j] = neval
+
+ return nevals, relerrs
+
+
+def lk_job_quadpack(seed, family, rtols):
+ """Run a single integration for a sequence of requested relative
+ tolerances. It's as if a len(rtols) integration were started,
+ but actually a single integrator is launched and the requested
+ tolerance is improved iteratively.
+ """
+ def ff(x):
+ nonlocal neval
+ neval += 1
+ return f(x, *args)
+
+ _, (f, a, b), F, *bounds = family
+ rng = np.random.RandomState(seed)
+ args = [rng.uniform(*b) for b in bounds]
+
+ relerrs = np.empty(len(rtols), float)
+ nevals = np.empty(len(rtols), int)
+
+ exact = F(*args)
+ for j, rtol in enumerate(rtols):
+ neval = 0
+ igral, _ = scipy.integrate.quad(
+ ff, a, b, epsabs=0, epsrel=rtol)
+ relerrs[j] = abs((igral - exact) / exact)
+ nevals[j] = neval
+
+ return nevals, relerrs
+
+
+def lk_runner(map, n, job, *args):
+ """Run job n times with different seed but otherwise same args.
+
+ return two arrays of shape (n, -1) where the second dimension is
+ determined by the args.
+ """
+ # map returns a sequence of (nevals, relerrs) pairs that is unzipped by the
+ # zip(*...) construct.
+ nevals, relerrs = zip(*map(job, range(n),
+ *(itertools.repeat(a) for a in args)))
+ return np.stack(nevals), np.stack(relerrs)
+
+
+def run(runner, job, n, rtols, thresholds, ):
+ old_settings = np.seterr(all='ignore')
+
+ executor = ProcessPoolExecutor()
+ for family in families:
+ nevals, relerrs = runner(executor.map, n, job, family, rtols)
+ nevals = np.mean(nevals, axis=0)
+ relerrs.sort(axis=0)
+ successratios = [np.searchsorted(*args) / n
+ for args in zip(relerrs.T, rtols)]
+ quantiles = {t: relerrs[int(t * n)] for t in thresholds}
+ yield nevals, quantiles, successratios
+
+ np.seterr(**old_settings)
+
+
+def display(datasets, dataset_names, familynames, cols, colnames):
+ matplotlib.rcParams['axes.prop_cycle'] = matplotlib.cycler(
+ 'color', matplotlib.cm.tab20.colors)
+
+ fig, axes = pyplot.subplots(2, 3, sharex='all', sharey='all')
+ fig.suptitle("Lyness-Kaganove diagrams")
+
+ print("Lyness-Kaganove benchmark as described in Section 6.5 "
+ "of P. Gonnet's thesis.\n"
+ "Legend: success count (mean number of evaluations)\n")
+ print("family \ rtol\t{}\t\t{}\t\t{}".format(*colnames))
+
+ for (i, familyname), ((m, n), ax), *results in zip(
+ enumerate(familynames), np.ndenumerate(axes), *datasets):
+ if i:
+ print()
+ line = [f"f_{i}\t"]
+ for j, (nevals, quantiles, successratios) in enumerate(results):
+ for col in cols:
+ line.append(f"{successratios[col]} ({nevals[col]:.0f})")
+ print("\t".join(line))
+ line = ["\t"]
+
+ for k, (t, q) in enumerate(quantiles.items()):
+ if j == m == n == 0:
+ label = f'{100*t:.0f}% quantile'
+ elif k == m == 0 and n == 1:
+ label = dataset_names[j]
+ else:
+ label = None
+ ax.loglog(q, nevals, 'o-', markevery=10,
+ label=label)
+
+ ax.set_title(f'$f_{i}(x) = {familyname}$')
+ if m == 0 and n in (0, 1):
+ ax.legend()
+ if m == 1:
+ ax.set_xlabel("Relative true error")
+ if n == 0:
+ ax.set_ylabel("Mean number of evaluations")
+
+
+def main():
+ VERSION = 'vquad benchmark dump version 0'
+
+ #### Parse command line.
+ examples = """Examples:
+python3 -m vquad -
+python3 -m vquad -a quadpack -o quadpack
+python3 -m vquad quadpack vquad-original -
+"""
+ parser = argparse.ArgumentParser(
+ 'python3 -m vquad',
+ description='Benchmark the integrator.',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=examples)
+ parser.add_argument('datasets', nargs='*')
+ parser.add_argument('-a', '--algorithm', default='vquad',
+ choices=['vquad', 'quadpack'])
+ parser.add_argument('-n', '--samples', type=int, default=200)
+ parser.add_argument('-o', '--output')
+ args = parser.parse_args()
+
+ # run_quadpack(args.samples, 10.0**np.linspace(0, -10, 101))
+
+ #### Setup benchmark.
+ if args.output or '-' in args.datasets:
+ if args.algorithm == 'vquad':
+ job = lk_job_vquad
+ elif args.algorithm == 'quadpack':
+ job = lk_job_quadpack
+ else:
+ raise RuntimeError('Unknown algorithm.')
+ computed, tobesaved = itertools.tee(run(
+ lk_runner, job,
+ args.samples, 10.0**np.linspace(0, -10, 101), [0.5, 0.95]))
+
+ datasets = []
+ dataset_names = []
+ #### Load any datasets.
+ for fname in args.datasets:
+ if fname == "-":
+ datasets.append(computed)
+ dataset_names.append('<{}>'.format(args.algorithm))
+ continue
+ with open(fname, 'rb') as f:
+ version, loaded = pickle.load(f)
+ if version != VERSION:
+ raise RuntimeError("Invalid version of file " + fname)
+ else:
+ datasets.append(loaded)
+ dataset_names.append(fname)
+
+ #### Launch computation and display.
+ if datasets:
+ display(datasets, dataset_names, (f[0] for f in families),
+ [30, 60, 90], ['1e-3', '1e-6', '1e-9'])
+
+ #### Save benchmark.
+ if args.output:
+ with open(args.output, 'wb') as f:
+ pickle.dump((VERSION, list(tobesaved)), f)
+
+ pyplot.show()