From 2c5446877794b7a900bb97f084f837253d78417a Mon Sep 17 00:00:00 2001 From: Christoph Groth Date: Tue, 5 Jun 2018 11:55:07 +0200 Subject: Lyness-Kaganove benchmark --- vquad/benchmark.py | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 vquad/benchmark.py (limited to 'vquad/benchmark.py') 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() -- cgit v1.2.3-74-g4815