Quickstart#

This quickstart introduces RMT, Mellin products, one-loop integrals and the dispersive sunset functions. CI executes the examples and checks their output.

To follow along, install the package, or run the same calls without installing anything on the Try it in your browser page.

The functions introduced here take dps=, the number of decimal digits requested, and return mpmath numbers (mpf for real, mpc for complex results).

For graph-based calculations beyond the named topologies, see Massive Euclidean multiloop graphs. That experimental backend returns float64 estimates with statistical errors and uses epsrel/epsabs instead of dps. For a graph-generated multivariate Gamma series with arbitrary precision, see Multivariate Ramanujan series for the D=2 sunset, currently restricted to two justified convergence charts of the massive, unit-power D=2 sunset.

For a method-selection table and three reference-checked graph calculations, see Choose a method and check your first result.

1. Ramanujan’s Master Theorem#

If \(f(x) = \sum_{n\ge 0} \varphi(n)\,(-x)^n/n!\), then, under Hardy’s growth condition on \(\varphi\),

\[ \int_0^\infty x^{s-1} f(x)\,dx = \Gamma(s)\,\varphi(-s). \]

For \(f(x) = (1+x)^{-3}\) the Taylor coefficients give \(\varphi(n) = \Gamma(3+n)/\Gamma(3)\), so the theorem predicts the Beta function \(B(s, 3-s)\):

>>> import mpmath as mp
>>> from ramanujan_qft import ramanujan_master_theorem, mellin_numeric
>>> phi = lambda n: mp.gamma(3 + n) / 2
>>> ramanujan_master_theorem(phi, 1.25)
mpf('0.41652027545234682')
>>> mp.beta(1.25, 1.75)
mpf('0.41652027545234682')
>>> mellin_numeric(lambda x: (1 + x)**-3, 1.25)   # direct quadrature, as a check
mpf('0.41652027545234682')

ramanujan_master_theorem evaluates \(\Gamma(s)\,\varphi(-s)\) exactly as given. It cannot tell whether the function you pass is the continuation of \(\varphi\) that the theorem requires, so check new uses with mellin_numeric. See Ramanujan’s Master Theorem.

2. Product integrals by Mellin residues#

product_integral(f, g, s=1) evaluates \(\int_0^\infty x^{s-1} f(x)\,g(x)\,dx\), where f and g come from a small atlas of functions with known Mellin transforms: exponential(k) \(= e^{-kx}\), gaussian(a) \(= e^{-ax^2}\), bessel_j(nu, b) \(= J_\nu(bx)\), cosine(b), sine(b), inverse_power(m, v) \(= (x+m)^{-v}\) and gaussian_bessel(a, b, nu) \(= e^{-ax^2} J_\nu(bx)\).

>>> from ramanujan_qft import product_integral, exponential, bessel_j
>>> product_integral(exponential(1), bessel_j(0, 3))
mpf('0.31622776601683794')
>>> 1 / mp.sqrt(10)                                 # closed form
mpf('0.31622776601683794')

The cost hardly depends on the oscillation frequency. Here is \(\int_0^\infty e^{-x} J_0(10^5 x)\,dx = 1/\sqrt{1+10^{10}}\) to 30 digits:

>>> v = product_integral(exponential(1), bessel_j(0, 1e5), dps=30)
>>> mp.nstr(v, 30)
'0.0000099999999995000000000375'
>>> with mp.workdps(30):
...     mp.nstr(1 / mp.sqrt(1 + mp.mpf(10)**10), 30)
'0.0000099999999995000000000375'

Pass return_info=True to see which factor was expanded, the error estimate, the number of terms and the working precision:

>>> product_integral(exponential(1), bessel_j(0, 3), dps=30, return_info=True)[1]
{'method': 'expand exp(-1 x)', 'error_estimate': mpf('1.3611704260536571e-31'), 'terms': 73, 'working_dps': 45}

Refusals. The residue series converge only when the scales of the two factors are far enough apart. \(\int_0^\infty e^{-x} J_0(x)\,dx = 1/\sqrt{2}\) exists, but both of its residue series sit on the convergence boundary (\(\rho = 1\)), so product_integral raises an error instead of returning a number it cannot verify:

>>> product_integral(exponential(1), bessel_j(0, 1))
Traceback (most recent call last):
  ...
ramanujan_qft.mellin.SeriesConvergenceError: neither ordering of exp(-1 x) * J_0(1 x) is a convergent residue expansion (the scales are at the convergence boundary rho = 1, or the factors cannot be expanded). Use quadrature or a closed form.

An integral that does not exist for the given s, such as \(\int_0^\infty \cos x \cos 2x\,dx\), raises ValueError with the range of Re s where it does exist. See Product integrals by Mellin residue summation.

3. One-loop integrals#

B0 and C0 follow the LoopTools conventions: arguments are squared masses, the metric is Minkowski (\(p^2 > 0\) is time-like), propagators carry \(+i0\), and B0 returns the finite part with \(\Delta = 0\) and \(\mu^2 = 1\) by default (parameters delta and mu2). C0(s, m1sq, m2sq, m3sq) is \(C_0(0, 0, s; m_1^2, m_2^2, m_3^2)\).

>>> from ramanujan_qft import B0, C0
>>> B0(10, 1, 2)
mpc(real='0.62259207309054199', imag='2.0116008064341786')
>>> C0(10, 1, 2, 3)
mpc(real='-0.42193253167473543', imag='-0.33673226803368017')

LoopTools 2.16 gives 0.622592073090541875 + 2.01160080643417860 i and -0.421932531674735434 - 0.336732268033680282 i for these two calls (tests/data/looptools_reference.csv): the results agree to double-precision round-off. Unlike LoopTools, the same functions go to any precision:

>>> mp.nstr(C0(10, 1, 2, 3, dps=50), 50)
'(-0.42193253167473545355940149447302777724030318471005 - 0.33673226803368019435750959172486639656816178457921j)'

Also available: B0_mob and C0_mob (the equal-mass Method-of-Brackets forms) and bubble_3d (the finite \(D=3\) bubble). See One-loop integrals and Conventions.

4. The two-loop sunset#

sunset_im(s, m1sq, m2sq, m3sq) is the imaginary part of the massive sunset. It vanishes below the three-particle threshold \(s = (m_1+m_2+m_3)^2\):

>>> from ramanujan_qft import sunset_im, sunset_threshold
>>> sunset_threshold(1, 2, 3)
mpf('17.191508225450303')
>>> sunset_im(100, 1, 2, 3)
mpf('-88.184995551555915')
>>> sunset_im(5, 1, 2, 3)
mpf('0.0')

sunset_im takes a fraction of a second. The other two sunset functions, sunset_subtracted (\(S(s)-S(0)-s\,S'(0)\)) and sunset_d2 (\(S''(s)\)), run nested quadratures at two working precisions and take 10–30 s per call, so they are not run on this page. See Two-loop sunset.

5. Precision and printing#

A result carries the precision you requested, but print and repr show it at mpmath’s global precision, mp.mp.dps, which is 15 by default. Use mp.nstr(x, n) or a with mp.workdps(n): block to see all the digits:

>>> x = B0(10, 1, 2, dps=40)
>>> print(x)
(0.622592073090542 + 2.01160080643418j)
>>> mp.nstr(x, 40)
'(0.6225920730905419577001843621004278217297 + 2.01160080643417843946062487127894859004j)'
>>> with mp.workdps(40):
...     print(x)
(0.6225920730905419577001843621004278217297 + 2.01160080643417843946062487127894859004j)

Inputs are taken as exact numbers. A Python float such as 0.1 is the binary number nearest to 0.1. To mean the decimal value, pass a string. (An mpf is binary too: mp.mpf("0.1") made at the default 15 digits is the same number as the float.)

>>> mp.nstr(B0(0.1, 1, 2, dps=30), 30)
'(-0.374845190552788401789865766787 + 0.0j)'
>>> mp.nstr(B0('0.1', 1, 2, dps=30), 30)
'(-0.374845190552788402429829239118 + 0.0j)'

The one-loop and sunset functions are precision-checked: each is evaluated at two working precisions, which are raised until the results agree to the requested number of digits. Precision and numerical checks explains what this does and does not guarantee.

Next steps#