AMFlow (external)
The content on this page was written by AI under human supervision.
AMFlow is public software by Xiao Liu and Yan-Qing Ma that evaluates Feynman integrals numerically to as many digits as you ask for. BootLoops runs amflow-cpp, the C++ reimplementation by the GitHub user chang18, from a patched fork kept in BootLoops' amflow-cpp-dev repository (GitHub organization BootLoops-ai), which carries upstream's history with the BootLoops changes on the branch bootloops and the release tag bootloops-1.0. Given an integral family, numerical kinematics and a precision goal, it returns the expansion of each requested integral in the dimensional regulator $\varepsilon$, order by order. BootLoops uses these numbers as the independent reference values its proposed closed forms are checked against. It adds patches to the solver, a validation suite, and one Python package, amflow-kit, which checks the solver's output files, predicts its reduction-cache keys, and sets the memory limit its launch scripts run it under.
What it does
A multi-loop Feynman integral is usually needed as a series in $\varepsilon=(4-d)/2$, where $d$ is the spacetime dimension. The auxiliary-mass-flow method adds a fictitious mass $\eta$ to chosen propagators, $1/(q^2-m^2)\to 1/(q^2-m^2-\eta)$. At very large $\eta$ the integral reduces to vacuum integrals whose values are known exactly. The solver sets up the differential equation in $\eta$ obeyed by the master integralsthe small set of basis integrals in terms of which every integral of the family can be written. It then solves that equation numerically, as a chain of power-series expansions from large $\eta$ down to $\eta=0$, and reads off the physical value there. Building the equation requires an IBP reductionintegration-by-parts reduction: the linear-algebra step that rewrites every integral of a family as a combination of the master integrals, which amflow-cpp delegates to the external programs Kira and Fermat.
The input is a JSON job file naming the family (momenta, propagators, kinematic replacement rules), the target integrals by their propagator powers, a value for every kinematic symbol, goal_digits and eps_order. The output JSON has a result array with one entry per target, whose coefficients give each power of $\varepsilon$ with real and imaginary parts printed as balls [midpoint +/- radius]; the radius is the solver's own error estimate. The result is a numerical value with an error estimate, not a proof. In practice BootLoops runs the same point at two precision goals and trusts the digits on which the two runs agree.
One convention needs care. For an $L$-loop integral, eps_order = K + 2L returns the series through $\varepsilon^K$, so the finite part needs eps_order of at least $2L$. In the stock code, asking for too little could return a lone zero that looks like a vanishing finite part. The patched solver prints the range of powers it fitted, raises a too-small eps_order to $2L$ with a warning, and stops with an error rather than return such a zero. The preferred key target_eps_power states the wanted power $K$ directly and sets eps_order for you.
The BootLoops patches leave the algorithm unchanged. The main fix is in the step that matches solutions at $\eta\to 0$: at very fine $\varepsilon$ grids the stock code could merge two distinct power behaviors and return wrong coefficients with no error. Other changes make the embedded Kira reduction recover from crashes and stalls and resume after a kill, add checkpoint and resume to the $\eta$-flow solves, run the $\varepsilon$ grid in parallel, and cache reductions across jobs. PATCHES.md at the root of the fork lists every change with its reason.
Everything BootLoops runs around the solver rather than inside it is collected in the package amflow-kit (tools/amflow-kit/, Python import name amflow_kit), which builds nothing and whose tests need no solver binary. Three scripts check solve_integrals output: a per-file sanity check, a smoke test for a freshly built binary, and a digit-by-digit comparison of two outputs of the same job. The module amflow_kit.keypred predicts, without running anything, the key under which the solver's reduction cache will store the reduction of a given Kira input directory. A reduction computed elsewhere can then be placed where the run will look for it. The predictor hashes the same canonical form of jobs.yaml as the solver (thread-count lines dropped, the seed cap the solver appends at write time stripped), so either byte convention of that file gives the key the run computes. A directory whose content differs from what the solver writes, with other sectors, another reduction mode or other targets, correctly gets a different key; in that case read the key the run logs.
The module amflow_kit.memfence is the memory-limit policy that BootLoops' launch scripts, PMflow and the shift_opt.py script of Numkin, apply to every solver process they start; a launch script of your own can import it the same way. The memory budget AMFLOW_KIRA_MEM_CAP_GB (in GiB) used to be applied as an address-space limit (ulimit -v) of the same size. Under the jemalloc allocator a Kira reduction reserves two to five times more address space than the memory it uses, so jobs died at about a third of their intended memory. The policy now depends on where the launcher runs. Inside a cgroupa Linux control group: a set of processes on which the kernel enforces resource limits, here a memory ceiling with a finite memory ceiling, that ceiling is the only limit: no ulimit -v is set and the variable is exported as 0, which the solver reads as no cap. Without such a ceiling an address-space limit of 2.5 times the budget is set and logged in those words (a 48 GiB budget gives a 120 GiB limit); with neither a ceiling nor a budget nothing is limited. Only the launcher's own cgroup counts, never an enclosing one, so a job meant to be held by a cgroup must be launched from inside it. After the launch the module reads back from /proc the environment of the started process, and of every process it started in turn, and compares it with what the launcher intended. On a mismatch the launcher kills exactly the process it started, identified by process id and command line rather than by a name pattern, and stops with an error; an environment that can no longer be read counts as unverified, not as a mismatch.
amflow-cpp does not accept complex-valued kinematics, and on large families at high digit counts the practical limit is the memory and time of the Kira reduction. For a second opinion on an AMFlow number BootLoops uses pySecDec and FIESTA; closed forms are compared against AMFlow's digits with the PSLQ and LLL tools; the reduction programs have their own Kira page.
Examples
Evaluate the one-loop massive bubble. The fork includes this job as examples/bubble_solve_integrals.json:
{
"mode": "solve_integrals",
"options": {
"chop_pre": 20,
"silent_mode": true
},
"family": {
"name": "bubblefam",
"loops": ["l"],
"legs": ["p"],
"replacement": {
"p^2": "s"
},
"propagators": [
"l^2 - msq",
"(l - p)^2 - msq"
]
},
"integrals": [
{ "indices": [1, 1] }
],
"goal_digits": 25,
"eps_order": 2,
"work_dir": "/tmp/desolver_example_bubble",
"amf_options": {
"blackbox": {
"numeric_values": {
"s": "5",
"msq": "1"
}
}
}
}
Run it from the build tree (Kira and Fermat must be installed):
./build/src/cli/amflow_cli examples/bubble_solve_integrals.json
The run takes a few seconds and Kira's intermediate files go under work_dir. In the printed JSON, result[0].integral echoes the family and indices and result[0].coefficients holds one {order, value} pair per power of $\varepsilon$. With eps_order 2 and $L=1$ the series runs through $\varepsilon^0$, that is the $1/\varepsilon$ pole and the finite part, each good to about 25 digits.
Check output files before using them. amflow_output_lint.py stops with a nonzero exit code if a file is all zeros or if a nonzero integral carries fewer than two nonzero orders. It also fails when a value does not parse as a ball with a finite midpoint, and, with --class vacuum, when no integral has a pole (any multi-loop vacuum family must).
python3 amflow_output_lint.py out_*.json [--class vacuum|kinematic] [--allow-shallow] [--min-nonzero-frac F] [--in job.json] [-q]
An integral that comes out exactly zero among nonzero ones only produces a warning. To compare two outputs of the same job, for example one point at two precision goals:
python3 compare_amflow_json.py REF.json TEST.json [--min-digits 25] [--n-integrals N] [--goal G|GREF,GTEST | --goal-from-config REF_CFG[,TEST_CFG] | --radii-only] [--strict-window]
The comparator needs the precision goal $G$ of the runs to decide what counts as zero. A pair of coefficients that are both smaller than $T=\max(\text{radii},\,10^{-(G-2)})$, with $G$ the lower of the two goals, is reported as a numerical zero and left out of the digit count; a zero against a value above $T$ fails. Give the goal with --goal (one value, or GREF,GTEST) or with --goal-from-config naming the job file or files, whose goal_digits is read. With neither flag the job file is looked up by name: an output at out/<name>.json pairs with configs/<name>.json under the same parent directory. With no goal from any of these the script refuses and compares nothing (exit code 2); --radii-only instead takes the threshold from the two error radii alone. An $\varepsilon$ order present in only one file, which happens because the window a run returns depends on its goal, is reported as not compared rather than failed unless --strict-window is given. The script prints the matched decimal digits for every coefficient, marks each PASS or FAIL against --min-digits, and ends with a line such as OVERALL: PASS (10 compared, 4 numerical zeros excluded, 2 orders not compared), exit code 0 or 1.
Smoke-test a freshly built binary.
amflow_smoke.sh BINARY_PATH [JOB_TIMEOUT_SECONDS]
The script runs two fixture jobs with known answers through the binary, a two-loop vacuum probe with five integrals and a one-loop triangle job restricted to its first two integrals. Each binary gets its own reduction cache, so a broken build cannot reuse a healthy build's cached work. Both outputs go through amflow_output_lint.py and are compared with the reference outputs to at least 20 significant digits; exit code 0 means both passed. The default per-job timeout is 1800 seconds. FERMATPATH must point at a fer64 binary, and AMFLOW_SMOKE_VENDOR_BIN names a reference binary the first time the triangle reference is generated.
Routines
The solver (patched amflow-cpp, the amflow-cpp-dev repository)
amflow_cli input.json [output.json]— runs one job; themodekey selectssolve_integrals(family in, $\varepsilon$-series out),black_box_amflow(values at listedeps_samples),amflow(a user-supplied differential system with boundaries) ordiffeq(the differential equation itself).ibp_cache_dir/AMFLOW_IBP_CACHE,symbolic_ibp/AMFLOW_SYMBOLIC_IBP— cache reductions across jobs; reduce once with the kinematics kept symbolic so new points reuse the reduction.AMFLOW_KIRA_FFSAVE=1,AMFLOW_KIRA_STALL_SECS— keep reduction work directories across a relaunch so FireFly resumes from its saved state (off by default); seconds of zero CPU progress before a reduction is declared stalled and restaged (default 180).target_eps_power,eps_parallel/AMFLOW_EPS_PARALLEL,node_parallel/AMFLOW_NODE_PARALLEL,explicit_boundary— job-file keys for the wanted $\varepsilon$ power, parallel $\varepsilon$ samples, parallel solves of the independent sub-systems at one node of the auxiliary-mass tree (off by default), and a boundary value supplied from outside when the built-in large-$\eta$ recursion has no known ending.
Output checks (tools/amflow-kit/)
amflow_output_lint.py— per-file check of asolve_integralsoutput: nonzero fraction, series depth, pole structure for vacuum families, ball parsing.compare_amflow_json.py— matched decimal digits between two outputs, coefficient by coefficient, against a--min-digitsthreshold; the precision goal (--goal,--goal-from-config, or the job file found by name) sets the size below which a pair counts as a numerical zero, and without one the script refuses unless--radii-onlyis given.amflow_smoke.sh— two known-answer jobs through a given binary, the output check plus a 20-digit comparison against the fixtures intools/fixtures/amflow_smoke/.selftest.py— the package's quick self-test: the output check and the comparator on the included fixture (the goal read from the fixture's own job file with--goal-from-config), the smoke script stopping with aFATALmessage when no usable binary is given, the key predictor's self-test, and a check thatimport amflow_kitexposes both modules with the memory policy turning a 48 GiB budget into a 120 GiB address-space limit.tests/holds the pytest suites for the comparator (two pinned pairs of real outputs plus synthetic pairs) and for the memory-limit module.
Validation suite (bootloops-wrappers/ in amflow-cpp-dev)
run_tier1.sh [jobs]— runs all 228 benchmark cases under the fork'stools/bench/against the committed Mathematica reference values (jobsis how many run at once, default 20).gen_tier23_configs.py,run_tier23.sh— write and runsolve_integralsjobs for the massless bubble and box, the massive bubble (including above threshold) and the equal-mass sunrise at 30, 100 and 300 goal digits, with timing.ref_closed_forms.py --out refs.json,ref_sunrise_bessel.py --out FILE— solver-independent reference values (mpmath): bubble and box from Gamma-function and hypergeometric closed forms, the sunrise from Bessel moments.compare_orders.py engine_out.json reference.json [CASE_KEY]— compares a solver output with a reference file from the two scripts above, order by order, printing the true error beside the solver's own error radius;CASE_KEYselects the case inside aref_closed_forms.pyfile.run_tier5.sh— run time and memory against requested digits and againsteps_order, with scaling fits.
Reduction-cache key predictor (amflow_kit.keypred, in tools/amflow-kit/)
python3 -m amflow_kit.keypred key DIR [--raw] [--explain] [--salt=S | --numd[=P/Q]],python3 -m amflow_kit.keypred predict DIR [--explain] [--salt=S | --numd[=P/Q]]— print the 16-hexadecimal-digit cache key the solver will compute for the Kira input directoryDIR(config/integralfamilies.yaml,config/kinematics.yaml,jobs.yaml,jobs_kira2math.yaml,preferred,target; missing files are skipped as the solver skips them), so a pre-computed reduction can be placed where the run will look for it.predictfirst repairs Windows line endings and a missing final newline in a hand-built directory;--rawprints the uncorrected byte-level key, for diagnosing a cache miss only, never for placing a cache entry;--explainalso prints the repairs and the bytes that were hashed on standard error.--numd=P/Qadds the extra string the solver hashes when a reduction runs at a numeric dimension (AMFLOW_DIFFEQ_NUMD=P/Q; a bare--numdreads that variable, as the run does), and--salt=Sadds an arbitrary string the same way. Run fromtools/amflow-kit/;python3 amflow_kit/keypred.pytakes the same arguments.python3 -m amflow_kit.keypred --selftest— fixture, mutation and salt tests on the synthetic Kira input directories intests/fixtures/keypred/, under a second; rerun it whenever the solver's cache code changes, since the predictor must track that code byte for byte.keypred.ibp_cache_key(dirpath, raw=False, pin=False, explain=None, salt=""),keypred.numd_salt("P/Q"),keypred.validate_numd(v),keypred.canonicalize_jobs_yaml(text),keypred.pin_jobs_yaml(text)(from amflow_kit import keypred) — the same as library calls: the key of a directory, the salt string for a numeric dimension and the check that its value has the formPorP/Q, the canonicaljobs.yamltext that is hashed, and the line-ending repair.
Memory limit for launch scripts (amflow_kit.memfence, in tools/amflow-kit/)
memfence.fuse_policy(env_cap_gb, budget_bytes=None, leaf=None)— decide the policy for one launch from the budget (theAMFLOW_KIRA_MEM_CAP_GBvalue, parsed as the solver parses it) and the calling process's own cgroup: modeleaf(finite cgroup ceiling, no address-space limit, variable exported as0),fuse(address-space limit of 2.5 times the budget) orunfenced; the returned dictionary carries the numbers, the exports and a sentence stating them for the launch log.memfence.apply_policy_to_env(env, policy),memfence.ulimit_prefix(policy)— write the exports into the child's environment (the budget variable plus the informationalMEMFENCE_MODE,MEMFENCE_FUSE_GIB,MEMFENCE_BUDGET_GIB) and return theulimit -vprefix for the launch line, empty when no address-space limit applies.memfence.intended_from_env(env),memfence.env_readback(pid, intended),memfence.env_readback_tree(pid, intended)— read/proc/<pid>/environof the launched process (the tree version also of every descendant, which is where a wrapper that drops an export shows up) and reportmatchandverified.memfence.abort_by_name(pid, argv),memfence.abort_tree_by_pid(pid, argv, reason=...)— on a mismatch, kill exactly the launched process id (the tree version its descendants too, deepest first) after checking that its command line is the one launched, SIGTERM then SIGKILL; a process with the wrong identity is refused, and nothing is ever matched by pattern.memfence.find_spawned_pid(argv_tail, t0),memfence.new_launch_id(),memfence.leaf_memory_max()— resolve a child that a wrapper started on the launcher's behalf by the unique tail of its command line and its start time, make the per-launch nonce a launcher may export asMEMFENCE_LAUNCH_IDso the process it later kills is provably its own, and read the calling process's cgroup record.python3 -m amflow_kit.memfence— print the calling shell's own cgroup record and the policy it would get, as JSON (run fromtools/amflow-kit/).- Import from another package in the repository's
tools/tree withsys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "amflow-kit")); from amflow_kit import memfence, the line PMflow and Numkin use.
Used on this site
- Three-loop light-by-light — independent values at two kinematic points kept out of the construction.
- Ice-cream cone — the derived boundary value checked against AMFlow evaluations at two working precisions.
- Kite — the values the equal-mass coefficients were fit to, with two points kept out of the fit as checks.
Requirements and source
The solver builds with a C++17 compiler, CMake 3.16 or newer, FLINT/Arb, GoogleTest and nlohmann/json. Kira and Fermat must be installed at run time for any job with a reduction; amflow-cpp/BUILD.md explains why Kira should be built against jemalloc. Build and run the tests with
cmake -S . -B build -DAMFLOW_BUILD_DRIVER=ON cmake --build build -j32 ctest --test-dir build --output-on-failure -j 4
amflow-kit needs Python 3 (the two modules use only the standard library; the comparator's tests need mpmath and the suites need pytest) and bash 4 or newer for the smoke script; the memory-limit module needs Linux with cgroup v2. One command from tools/amflow-kit/ tests the whole package with no built solver:
python3 selftest.py && python3 -m pytest tests -q -rs -p no:cacheprovider
The first part runs in a couple of seconds, the second in about ten; two tests that create a cgroup of their own are skipped, with the reason printed, where the kernel refuses, and the exit status stays 0. The key predictor's tests alone are python3 -m amflow_kit.keypred --selftest. The full smoke test needs a built amflow_cli, FERMATPATH, and a reference binary in AMFLOW_SMOKE_VENDOR_BIN the first time; AMFLOW_SMOKE_FIX, AMFLOW_SMOKE_ROOT and AMFLOW_SMOKE_WRAP move its fixtures, scratch directory and optional launch wrapper. The validation suite also needs kira on PATH and GNU parallel; AMFLOW_REPO points it at a build.
amflow-kit is tools/amflow-kit/ in the bootloops-dev repository (same organization), released under the MIT license. It holds the three check scripts and selftest.py at the top, the importable amflow_kit/ package with keypred.py and memfence.py, the manuals GUIDE.md, KEYPRED.md and MEMFENCE.md, and tests/ (the key predictor's synthetic Kira directories are under tests/fixtures/keypred/); the smoke fixtures are in tools/fixtures/amflow_smoke/. Keep the repository's tools/ tree together, since PMflow, Numkin and Holonomic find the package as a sibling directory. The patched solver and its validation suite (bootloops-wrappers/) are the separate amflow-cpp-dev repository, which carries upstream's MIT license and notice files intact, with the BootLoops changes contributed under the same license and listed in its PATCHES.md; build it as its BUILD.md says. Kira and Fermat, which the solver runs as separate programs, are distributed under their own terms. Upstream: github.com/chang18/amflow-cpp and the original Mathematica package gitlab.com/multiloop-pku/amflow. Work that uses the solver should cite X. Liu and Y.-Q. Ma, arXiv:2201.11669, together with the amflow-cpp repository; the method originates in X. Liu, Y.-Q. Ma and C.-Y. Wang, arXiv:1711.09572.